Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions docs/configure/channels/now-playing-overlay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Now Playing Overlay

Channels can display a "now playing" lower-third overlay on the stream, showing the current program's title, artist, album, and year. This is especially useful for music video channels where viewers want to know what's playing.

The overlay appears as a semi-transparent bar at the bottom of the video with text that fades in and out.

There are several ways to customize the overlay for a channel. Here are some details on specific options:

### Position

Controls whether the overlay appears at the bottom-left or bottom-right of the video.

### Show at Start

How long (in seconds) the overlay is visible at the beginning of each program. For example, a value of 8 means the overlay will appear for the first 8 seconds.

### Show at End

How long (in seconds) the overlay reappears before the program ends. Set to 0 to disable the closing overlay. This is useful for giving viewers a heads-up that the current program is about to end.

### Start Padding

Adds a delay (in seconds) before the opening overlay appears. For example, a value of 2 means the overlay won't appear until 2 seconds into the program. This can help avoid showing the overlay during intro sequences.

### End Padding

Adds a gap (in seconds) between the closing overlay and the end of the program. For example, a value of 2 means the closing overlay will disappear 2 seconds before the program ends.

### Fade Duration

Controls how long (in seconds) the text takes to fade in and out. Set to 0 for instant appearance. A value of 0.5 provides a subtle, smooth transition.

## Coming Up Next

The overlay can also display a "coming up next" card showing the next program's title and metadata. This appears as a separate overlay before the closing card.

### Duration

How long (in seconds) the "coming up next" card is shown. Set to 0 to disable this feature entirely.

### Offset from End

How far from the end of the program (in seconds) the "coming up next" card starts. For example, a value of 30 means it will appear 30 seconds before the program ends.

## Things to consider

- The overlay requires video transcoding. It will not appear when using HLS Direct or HLS Direct v2 stream modes, since those modes pass the video through without modification.
- If a program is too short to fit all configured overlays without overlapping, the "coming up next" card is skipped first, followed by the closing card.
- Program metadata (title, artist, album, year) comes from the media source (Plex, Jellyfin, Emby, or local file tags). For local files, embedded metadata from the file itself is preferred when available.
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ nav:
- EPG: configure/channels/epg.md
- Transcoding: configure/channels/transcoding.md
- Watermarks: configure/channels/watermarks.md
- Now Playing Overlay: configure/channels/now-playing-overlay.md
- Programming: configure/channels/programming.md
- Library:
- configure/library/index.md
Expand Down
23 changes: 20 additions & 3 deletions server/src/db/channel/BasicChannelRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { MarkRequired } from 'ts-essentials';
import { v4 } from 'uuid';
import { isDefined, isNonEmptyString } from '../../util/index.ts';
import { ChannelAndLineup } from '../interfaces/IChannelDB.ts';
import type { ChannelTranscodingSettings } from '../schema/base.ts';
import {
Channel,
ChannelOrm,
Expand Down Expand Up @@ -59,11 +60,23 @@ function sanitizeChannelWatermark(
};
}

function updateRequestToChannel(updateReq: SaveableChannel): ChannelUpdate {
function updateRequestToChannel(
updateReq: SaveableChannel,
existingTranscoding?: ChannelTranscodingSettings | null,
): ChannelUpdate {
const sanitizedWatermark = sanitizeChannelWatermark(updateReq.watermark);

let transcoding: string | undefined;
if (isDefined(updateReq.transcoding?.nowPlayingOverlay)) {
transcoding = JSON.stringify({
...(existingTranscoding ?? {}),
nowPlayingOverlay: updateReq.transcoding.nowPlayingOverlay,
});
}

return {
number: updateReq.number,
transcoding,
watermark: sanitizedWatermark
? JSON.stringify(sanitizedWatermark)
: undefined,
Expand All @@ -86,12 +99,16 @@ function updateRequestToChannel(updateReq: SaveableChannel): ChannelUpdate {

function createRequestToChannel(saveReq: SaveableChannel): NewChannel {
const now = +dayjs();
const transcoding = isDefined(saveReq.transcoding)
? JSON.stringify(saveReq.transcoding)
: null;

return {
uuid: v4(),
createdAt: now,
updatedAt: now,
number: saveReq.number,
transcoding,
watermark: saveReq.watermark ? JSON.stringify(saveReq.watermark) : null,
icon: JSON.stringify(saveReq.icon),
guideMinimumDuration: saveReq.guideMinimumDuration,
Expand Down Expand Up @@ -274,7 +291,7 @@ export class BasicChannelRepository {
throw new ChannelNotFoundError(id);
}

const update = updateRequestToChannel(updateReq);
const update = updateRequestToChannel(updateReq, channel.transcoding);

if (
isNonEmptyString(updateReq.watermark?.url) &&
Expand Down Expand Up @@ -426,10 +443,10 @@ export class BasicChannelRepository {
number: maxId + 1,
icon: JSON.stringify(channel.icon),
offline: JSON.stringify(channel.offline),
transcoding: null,
watermark: JSON.stringify(channel.watermark),
createdAt: now,
updatedAt: now,
transcoding: null,
})
.returningAll()
.executeTakeFirstOrThrow();
Expand Down
9 changes: 9 additions & 0 deletions server/src/db/derived_types/StreamLineup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,20 @@ import type {
} from '../schema/derivedTypes.ts';
import type { ProgramType } from '../schema/Program.ts';

export type NextProgramMetadata = {
title?: string;
artist?: string;
album?: string;
year?: string;
filePath?: string;
};

type BaseStreamLineupItem = {
streamDuration: number;
startOffset?: number;
programBeginMs: number;
duration: number;
nextProgramMetadata?: NextProgramMetadata;
};

export type StreamLineupProgram = MarkNotNilable<
Expand Down
13 changes: 13 additions & 0 deletions server/src/db/schema/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,19 @@ export const ChannelTranscodingSettingsSchema = z.object({
targetResolution: ResolutionSchema.optional().catch(undefined),
videoBitrate: z.number().nonnegative().optional().catch(undefined),
videoBufferSize: z.number().nonnegative().optional().catch(undefined),
nowPlayingOverlay: z
.object({
enabled: z.boolean().default(false).catch(false),
showForSeconds: z.number().positive().default(8).catch(8),
showAtEndForSeconds: z.number().nonnegative().default(0).catch(0),
startPaddingSeconds: z.number().nonnegative().default(0).catch(0),
endPaddingSeconds: z.number().nonnegative().default(0).catch(0),
comingUpNextForSeconds: z.number().nonnegative().default(0).catch(0),
comingUpNextOffsetSeconds: z.number().nonnegative().default(30).catch(30),
fadeDurationSeconds: z.number().nonnegative().default(0.5).catch(0.5),
})
.optional()
.catch(undefined),
});

export type ChannelTranscodingSettings = z.infer<
Expand Down
Loading