import * as grpc from "@grpc/grpc-js"; import { MediaClient } from "@norskvideo/norsk-api/lib/media_grpc_pb"; import { Version, CurrentLoad, Log_Level, } from "@norskvideo/norsk-api/lib/shared/common_pb"; import { NorskStatusEvent, AmdMA35DLoad, SubscriptionChannelResponse, LicenseEvent } from "@norskvideo/norsk-api/lib/media_pb"; import { Empty } from "@bufbuild/protobuf"; import { debuglog, errorlog, exhaustiveCheck, norskHost, norskPort } from "./shared/utils"; import { MediaNodeState, PinToKey, MediaClient as SharedMediaClient } from "./media_nodes/common"; import { AudioSignalGeneratorNode, AudioSignalGeneratorSettings, BrowserInputNode, BrowserInputSettings, DeckLinkInputNode, DeckLinkInputSettings, DeltacastInputNode, DeltacastInputSettings, FileImageInputNode, FileImageInputSettings, LocalFileInputSettings, M3u8InputNode, M3u8MediaInputSettings, FileMp4InputNode, FileMp4InputSettings, NorskInput, RtmpServerInputNode, RtmpServerInputSettings, RtpInputNode, RtpInputSettings, SrtInputNode, SrtInputSettings, FileTsInputNode, UdpTsInputNode, UdpTsInputSettings, FileWebVttInputNode, WhipInputNode, WhipInputSettings, FileTsInputSettings, VideoTestcardGeneratorSettings, VideoTestcardGeneratorNode, StreamWebVttInputSettings, StreamWebVttInputNode, FileWavInputSettings, FileWavInputNode, NdiInputSettings, NdiInputNode, } from "./media_nodes/input"; import { Gain, NorskProcessor, WebRTCBrowserNode, WebRTCBrowserSettings, NorskDuplex, SipSettings, SipNode, } from "./media_nodes/processor"; import { CmafAudioOutputNode, CmafMultiVariantOutputNode, CmafMultiVariantOutputSettings, CmafOutputSettings, CmafVideoOutputNode, CmafWebVttOutputNode, CmafWebVttOutputSettings, FileMp4OutputNode, FileMp4OutputSettings, FileTsOutputNode, FileTsOutputSettings, FileWavOutputNode, FileWavOutputSettings, FileWebVttOutputNode, FileWebVttOutputSettings, HlsTsAudioOutputNode, HlsTsAudioOutputSettings, HlsTsCombinedPushOutputNode, HlsTsCombinedPushOutputSettings, HlsTsMultiVariantOutputNode, HlsTsMultiVariantOutputSettings, HlsTsVideoOutputNode, HlsTsVideoOutputSettings, ImagePreviewOutputNode, ImagePreviewOutputSettings, MoqOutputNode, MoqOutputSettings, NdiOutputNode, NdiOutputSettings, NorskOutput, RtmpOutputNode, RtmpOutputSettings, SrtOutputNode, SrtOutputSettings, UdpTsOutputNode, UdpTsOutputSettings, WhepOutputNode, WhepOutputSettings, WhipOutputNode, WhipOutputSettings, } from "./media_nodes/output"; import { EncryptionSettings, StreamKey, StreamMetadata, Context, Wave } from "./media_nodes/types"; import { hardwareInfo, NdiDiscovery, NdiDiscoverySettings, NorskSystem } from "./system"; import { InspectSubtitlesNode, InspectSubtitlesSettings, MetricsNode, MetricsSettings, NorskInspect, StreamTimestampReportNode, StreamTimestampReportSettings } from "./media_nodes/inspect"; import { MediaStorePlayerNode, MediaStorePlayerSettings, NorskMediaStore, MediaStoreRecorderNode, MediaStoreRecorderSettings, MediaStoreCutRequest, MediaStoreActiveCut, MediaStoreAssetSettings, MediaStoreAsset, MediaStoreSnapshotSettings, MediaStoreSnapshot, } from "./media_nodes/mediaStore"; // eslint-disable-next-line @typescript-eslint/no-require-imports import pj = require("../package.json"); import { ConnectivityState } from "@grpc/grpc-js/build/src/connectivity-state"; export * from "./types"; export * from "./system"; export * from "./media_nodes/types"; export * from "./media_nodes/input"; export * from "./media_nodes/output"; export * from "./media_nodes/inspect"; export * from "./media_nodes/processor"; export * from "./media_nodes/common"; export * from "./media_nodes/mediaStore"; export { CurrentLoad, } from "@norskvideo/norsk-api/lib/shared/common_pb"; export { AmdMA35DLoad } from "@norskvideo/norsk-api/lib/media_pb"; export { AudioCodec } from "@norskvideo/norsk-api/lib/media_pb"; export * from "./system"; export { Version } from "@norskvideo/norsk-api/lib/shared/common_pb"; /** @public */ export type Log = { level: | "emergency" | "alert" | "critical" | "error" | "warning" | "notice" | "info" | "debug"; timestamp: Date; message: string; metadata: string; }; /** * @public * Top level Norsk configuration */ export interface NorskSettings { /** * Callback URL to listen on for gRPC session with Norsk Media * Defaults to $NORSK_HOST:$NORSK_PORT if the environment variables are set * where NORSK_HOST defaults to "127.0.0.1" and NORSK_PORT to "6790" * (so "127.0.0.1:6790" if neither variable is set) */ url?: string; onAttemptingToConnect?: () => void; onConnecting?: () => void; onReady?: () => void; onFailedToConnect?: () => void; /** Code to execute if the Norsk node is shutdown - by default it logs and nothing else */ onShutdown?: () => void; onCurrentLoad?: (load: CurrentLoad) => void; onAmdMA35DLoad?: (load: AmdMA35DLoad) => void; onHello?: (version: Version) => void; onLogEvent?: (log: Log) => void; /** * Manually handle license events, such as missing/invalid licenses and * sandbox timeout. (Logs messages to console by default.) */ onLicenseEvent?: (message: string, running: boolean, fullEvent: LicenseEvent) => void; } /** * @public * The entrypoint for all Norsk Media applications * * @example * ```ts * const norsk = new Norsk(); * ``` */ export class Norsk { /** @internal */ client: MediaClient; /** @internal */ innerClient: Partial; /** @internal */ nodes: MediaNodeState[]; /** @internal */ connectivityState: number; /** @internal */ statusStream?: grpc.ClientReadableStream; /** @internal */ publicWebPort?: number; /** * Implements the {@link NorskInput} interface */ public input: NorskInput; /** * Implements the {@link NorskOutput} interface */ public output: NorskOutput; /** * Implements the {@link NorskDuplex} interface */ public duplex: NorskDuplex; /** * Implements the {@link NorskProcessor} interface */ public processor: NorskProcessor; /** * Implements the {@link NorskMediaStore} interface */ public mediaStore: NorskMediaStore; /** * Implements the {@link NorskInspect} interface */ public inspect: NorskInspect; /** * Implements the {@link NorskSystem} interface */ public system: NorskSystem; /** /* The settings used to create this Norsk instance */ public settings: NorskSettings; /** * Norsk Runtime version information */ public version: Version; /** * Norsk SDK version information */ public sdkVersion: string; /** * Norsk license information */ public license: { activeFeaturePacks: string[], expiredFeaturePacks: string[], }; /* @internal */ public resolveVersion: () => void; /* @internal */ public initialised: Promise; /* @internal */ closing: boolean = false; /* @internal */ timeout?: NodeJS.Timeout; /** @internal */ registerNode(node: N): N { this.nodes.push(node); if (this.closing) { void node.close(); } return node; } /** @internal */ unregisterNode(node: N): void { this.nodes = this.nodes.filter(n => n != node); // This resolves the promise if somebody called \.close on the node node.finalise(); } public async close() { this.closing = true; let timeout = 100; debuglog("Closing Norsk (%d nodes active)", this.processor.nodes.length + this.nodes.length); await this.processor.close(); for (const n of this.nodes) { await n.close(); } await new Promise((resolve) => { const t = setInterval(() => { const stillOpen = this.processor.nodes.length + this.nodes.length; if (stillOpen > 0) { if (timeout-- <= 0) { debuglog("Closing Norsk cleanly failed (%d nodes active), not waiting", stillOpen); clearInterval(t); resolve(); } { debuglog("Closing Norsk (%d nodes active)", stillOpen); } return; } else { clearInterval(t); resolve(); } }, 10.0); }) this.statusStream?.cancel(); this.innerClient.subscriptions?.cancel(); try { this.client.close(); } catch { /* empty */ } return new Promise((r) => { const t = setInterval(() => { if (this.connectivityState != 2 || timeout-- <= 0) { clearInterval(t); r(); } }, 10.0) }); } /** @internal */ handleSubscriptionChannelEvent(_data: SubscriptionChannelResponse) { // There are none } /** @internal */ handleStatusEvent(data: NorskStatusEvent) { const messageCase = data.message.case; switch (messageCase) { case undefined: break; case "hello": { debuglog( "Norsk status channel connected: %s", data.message.value.version ); this.publicWebPort = data.message.value.publicWebPort; if (data.message.value.version === undefined) { throw new Error("Norsk version is undefined"); } else { this.version = data.message.value.version; } this.license = { activeFeaturePacks: data.message.value.activeFeaturePacks, expiredFeaturePacks: data.message.value.expiredFeaturePacks, }; this.settings.onHello && data.message.value.version && this.settings.onHello(data.message.value.version); const stringVersion = `${this.version.majorVersion}.${this.version.minorVersion}.${this.version.buildNumber}`; if (stringVersion != this.sdkVersion) { const warning = `Norsk version mismatch: SDK=${this.sdkVersion}, runtime=${stringVersion}\nBEWARE POTENTIAL GRPC API CHANGES\nFunctionality may be partially or completely broken`; console.error(warning); errorlog(warning) } if (this.timeout) clearInterval(this.timeout); this.timeout = undefined; this.resolveVersion(); break; } case "currentLoad": { this.settings.onCurrentLoad && this.settings.onCurrentLoad(data.message.value); break; } case "ma35dLoad": { this.settings.onAmdMA35DLoad && this.settings.onAmdMA35DLoad(data.message.value); break; } case "licenseEvent": { if (this.settings.onLicenseEvent) { this.settings.onLicenseEvent(data.message.value.message, data.message.value.running, data.message.value); } else { if (data.message.value.running) { console.warn(data.message.value.message); debuglog(data.message.value.message); } else { console.error(data.message.value.message); errorlog(data.message.value.message); } } break; } case "logEvent": { let level: | "emergency" | "alert" | "critical" | "error" | "warning" | "notice" | "info" | "debug"; switch (data.message.value.level) { case Log_Level.EMERGENCY: level = "emergency"; break; case Log_Level.ALERT: level = "alert"; break; case Log_Level.CRITICAL: level = "critical"; break; case Log_Level.ERROR: level = "error"; break; case Log_Level.WARNING: level = "warning"; break; case Log_Level.NOTICE: level = "notice"; break; case Log_Level.INFO: level = "info"; break; case Log_Level.DEBUG: level = "debug"; break; default: exhaustiveCheck(data.message.value.level); } const log: Log = { level: level, message: data.message.value.msg, timestamp: new Date(Number(data.message.value.timestamp) / 1000), metadata: JSON.parse(data.message.value.metadata) }; if (this.settings.onLogEvent) { this.settings.onLogEvent(log); } else { debuglog("Norsk log event: %o", log); } break; } default: exhaustiveCheck(messageCase); } } /** @internal */ connectivityStateWatcher() { const channel = this.client.getChannel(); const connectivityState = channel.getConnectivityState(!this.closing); switch (connectivityState) { case 0: { if (this.connectivityState == 1 || this.connectivityState == 2) { this.settings.onShutdown && this.settings.onShutdown(); } // Idle this.settings.onAttemptingToConnect && this.settings.onAttemptingToConnect(); break; } case 1: { // Connecting this.settings.onConnecting && this.settings.onConnecting(); break; } case 2: { // Ready this.settings.onReady && this.settings.onReady(); this.statusStream = this.client.createStatusChannel(new Empty()); this.statusStream.on("data", this.handleStatusEvent.bind(this)); this.statusStream.on("error", () => { return; }); this.innerClient.subscriptions = this.client.createSubscriptionChannel(); this.innerClient.subscriptions.on("data", this.handleSubscriptionChannelEvent.bind(this)); this.innerClient.subscriptions.on("error", () => { return; }); break; } case 3: { // Transient failure this.settings.onFailedToConnect && this.settings.onFailedToConnect(); break; } case 4: { // Shutdown this.settings.onShutdown && this.settings.onShutdown(); break; } } debuglog("Channel connectivity state change: %d (%s)", connectivityState, ConnectivityState[connectivityState]); this.connectivityState = connectivityState; channel.watchConnectivityState(connectivityState, Infinity, () => { this.connectivityStateWatcher(); }); } /** @public */ public static async connect(settings?: NorskSettings) { debuglog("Norsk SDK: %s", pj.version); settings = settings ?? {}; if (!settings.onShutdown) { settings.onShutdown = () => { debuglog("Norsk has shutdown"); }; } const norsk = new Norsk(settings); await norsk.initialised; return norsk; } /** @internal */ private _client(): SharedMediaClient { return this.innerClient as SharedMediaClient; // with apologies } private onConnectionTimeout() { debuglog("Timeout on connect"); this.settings.onFailedToConnect && this.settings.onFailedToConnect(); } /** @internal */ constructor(norskSettings: NorskSettings) { this.connectivityState = 0; this.sdkVersion = pj.version; this.client = new MediaClient( norskSettings.url ? norskSettings.url : norskHost() + ":" + norskPort(), grpc.credentials.createInsecure() ); this.innerClient = { media: this.client, }; this.settings = norskSettings; this.connectivityStateWatcher(); this.timeout = setTimeout(this.onConnectionTimeout.bind(this), 60000); this.initialised = new Promise((resolve, _reject) => { this.resolveVersion = resolve; }); this.nodes = []; const registerNode = this.registerNode.bind(this); const unregisterNode = this.unregisterNode.bind(this); this.input = { rtmpServer: async (settings: RtmpServerInputSettings) => RtmpServerInputNode.create(settings, this._client(), unregisterNode).then(registerNode), fileTs: async (settings: FileTsInputSettings) => FileTsInputNode.create(settings, this._client(), unregisterNode).then(registerNode), srt: async (settings: SrtInputSettings) => SrtInputNode.create(settings, this._client(), unregisterNode).then(registerNode), whip: async (settings: WhipInputSettings) => WhipInputNode.create(settings, this._client(), unregisterNode).then(registerNode), m3u8Media: async (settings: M3u8MediaInputSettings) => M3u8InputNode.create(settings, this._client(), unregisterNode).then(registerNode), udpTs: async (settings: UdpTsInputSettings) => UdpTsInputNode.create(settings, this._client(), unregisterNode).then(registerNode), fileWebVtt: async (settings: LocalFileInputSettings) => FileWebVttInputNode.create(settings, this._client(), unregisterNode).then(registerNode), streamWebVtt: async (settings: StreamWebVttInputSettings) => StreamWebVttInputNode.create(settings, this._client(), unregisterNode).then(registerNode), fileImage: async (settings: FileImageInputSettings) => FileImageInputNode.create(settings, this._client(), unregisterNode).then(registerNode), fileMp4: async (settings: FileMp4InputSettings) => FileMp4InputNode.create(settings, this._client(), unregisterNode).then(registerNode), fileWav: async (settings: FileWavInputSettings) => FileWavInputNode.create(settings, this._client(), unregisterNode).then(registerNode), rtp: async (settings: RtpInputSettings) => RtpInputNode.create(settings, this._client(), unregisterNode).then(registerNode), audioSignal: async (settings: AudioSignalGeneratorSettings) => AudioSignalGeneratorNode.create(settings, this._client(), unregisterNode).then(registerNode), videoTestCard: async (settings: VideoTestcardGeneratorSettings) => VideoTestcardGeneratorNode.create(settings, this._client(), unregisterNode).then(registerNode), browser: async (settings: BrowserInputSettings) => BrowserInputNode.create(settings, this._client(), unregisterNode).then(registerNode), deckLink: async (settings: DeckLinkInputSettings) => DeckLinkInputNode.create(settings, this._client(), unregisterNode).then(registerNode), deltaCast: async (settings: DeltacastInputSettings) => DeltacastInputNode.create(settings, this._client(), unregisterNode).then(registerNode), ndi: async (settings: NdiInputSettings) => NdiInputNode.create(settings, this._client(), unregisterNode).then(registerNode), }; this.output = { cmafVideo: async (settings: CmafOutputSettings) => CmafVideoOutputNode.create(settings, this._client(), unregisterNode).then(registerNode), cmafAudio: async (settings: CmafOutputSettings) => CmafAudioOutputNode.create(settings, this._client(), unregisterNode).then(registerNode), cmafWebVtt: async (settings: CmafWebVttOutputSettings) => CmafWebVttOutputNode.create(settings, this._client(), unregisterNode).then(registerNode), hlsTsVideo: async (settings: HlsTsVideoOutputSettings) => HlsTsVideoOutputNode.create(settings, this._client(), unregisterNode).then(registerNode), udpTs: async (settings: UdpTsOutputSettings) => UdpTsOutputNode.create(settings, this._client(), unregisterNode).then(registerNode), srt: async (settings: SrtOutputSettings) => SrtOutputNode.create(settings, this._client(), unregisterNode).then(registerNode), hlsTsAudio: async (settings: HlsTsAudioOutputSettings) => HlsTsAudioOutputNode.create(settings, this._client(), unregisterNode).then(registerNode), hlsTsCombinedPush: async (settings: HlsTsCombinedPushOutputSettings) => HlsTsCombinedPushOutputNode.create(settings, this._client(), unregisterNode).then(registerNode), hlsTsMultiVariant: async (settings: HlsTsMultiVariantOutputSettings) => HlsTsMultiVariantOutputNode.create(settings, this._client(), unregisterNode).then(registerNode), cmafMultiVariant: async (settings: CmafMultiVariantOutputSettings) => CmafMultiVariantOutputNode.create(settings, this._client(), unregisterNode).then(registerNode), whip: async (settings: WhipOutputSettings) => WhipOutputNode.create(settings, this._client(), unregisterNode).then(registerNode), whep: async (settings: WhepOutputSettings) => WhepOutputNode.create(settings, this._client(), unregisterNode).then(registerNode), imagePreview: async (settings: ImagePreviewOutputSettings) => ImagePreviewOutputNode.create(settings, this._client(), unregisterNode).then(registerNode), rtmp: async (settings: RtmpOutputSettings) => RtmpOutputNode.create(settings, this._client(), unregisterNode).then(registerNode), fileTs: async (settings: FileTsOutputSettings) => FileTsOutputNode.create(settings, this._client(), unregisterNode).then(registerNode), fileMp4: async (settings: FileMp4OutputSettings) => FileMp4OutputNode.create(settings, this._client(), unregisterNode).then(registerNode), fileWav: async (settings: FileWavOutputSettings) => FileWavOutputNode.create(settings, this._client(), unregisterNode).then(registerNode), fileWebVtt: async (settings: FileWebVttOutputSettings) => FileWebVttOutputNode.create(settings, this._client(), unregisterNode).then(registerNode), moq: async (settings: MoqOutputSettings) => MoqOutputNode.create(settings, this._client(), unregisterNode).then(registerNode), ndi: async (settings: NdiOutputSettings) => NdiOutputNode.create(settings, this._client(), unregisterNode).then(registerNode), }; this.mediaStore = { player: async (settings: MediaStorePlayerSettings) => MediaStorePlayerNode.create(settings, this._client(), unregisterNode).then(registerNode), recorder: async (settings: MediaStoreRecorderSettings) => MediaStoreRecorderNode.create(settings, this._client(), unregisterNode).then(registerNode), makeCut: (cutRequest: MediaStoreCutRequest) => MediaStoreActiveCut.create(cutRequest, this._client()), asyncLoadAsset: async (settings: MediaStoreAssetSettings) => MediaStoreAsset.create(settings, this._client()), snapshot: async (settings: MediaStoreSnapshotSettings) => MediaStoreSnapshot.create(settings, this._client()) } this.inspect = { streamTimestampReport: async (settings: StreamTimestampReportSettings) => StreamTimestampReportNode.create(settings, this._client(), unregisterNode).then(registerNode), subtitles: async (settings: InspectSubtitlesSettings) => InspectSubtitlesNode.create(settings, this._client(), unregisterNode).then(registerNode), streamMetrics: async (settings: MetricsSettings) => MetricsNode.create(settings, this._client(), unregisterNode).then(registerNode), }; this.processor = new NorskProcessor(this._client()); this.system = { hardwareInfo: async () => hardwareInfo(this.client), // does not produce a node... ndiDiscovery: async (settings: NdiDiscoverySettings) => NdiDiscovery.create(settings, this.client), }; this.duplex = { webRtcBrowser: async (settings: WebRTCBrowserSettings) => WebRTCBrowserNode.create(settings, this._client(), unregisterNode).then(registerNode), sip: async (settings: SipSettings) => SipNode.create(settings, this._client(), unregisterNode).then(registerNode) }; } } /** * @public * Filters a context to only the audio streams within it * @param streams - The media context from which to return the streams * @returns The audio streams in the media context */ export function audioStreams( streams: readonly StreamMetadata[] ): StreamMetadata[] { return streams.filter((stream) => stream.message.case === "audio"); } /** * @public * Filters a context to only the video streams within it * @param streams - The media context from which to return the streams * @returns The video streams in the media context */ export function videoStreams( streams: readonly StreamMetadata[] ): StreamMetadata[] { return streams.filter((stream) => stream.message.case === "video"); } /** * @public * Filters a context to only the subtitle streams within it * @param streams - The media context from which to return the streams * @returns The subtitle streams in the media context */ export function subtitleStreams( streams: readonly StreamMetadata[] ): StreamMetadata[] { return streams.filter((stream) => stream.message.case === "subtitle"); } /** * @public * Filters a context to only the playlist streams within it * @param streams - The media context from which to return the streams * @returns The playlist streams in the media context */ export function playlistStreams(streams: readonly StreamMetadata[]): StreamMetadata[] { return streams.filter((stream) => stream.message.case === "playlist"); } /** * @public * Filters a context to only the ancillary streams within it * @param streams - The media context from which to return the streams * @returns The ancillary streams in the media context */ export function ancillaryStreams( streams: readonly StreamMetadata[] ): StreamMetadata[] { return streams.filter((stream) => stream.message.case === "ancillary"); } /** * @public * Returns the stream keys for audio streams in a media context * @param streams - The media context from which to return the stream keys * @returns The audio stream keys in the media context */ export function audioStreamKeys( streams: readonly StreamMetadata[] ): StreamKey[] { return removeUndefined( audioStreams(streams).map((stream) => stream.streamKey) ); } /** * @public * Returns the stream keys for video streams in a media context * @param streams - The media context from which to return the stream keys * @returns The video stream keys in the media context */ export function videoStreamKeys( streams: readonly StreamMetadata[] ): StreamKey[] { return removeUndefined( videoStreams(streams).map((stream) => stream.streamKey) ); } /** * @public * Returns the stream keys for subtitle streams in a media context * @param streams - The media context from which to return the stream keys * @returns The subtitle stream keys in the media context */ export function subtitleStreamKeys( streams: readonly StreamMetadata[] ): StreamKey[] { return removeUndefined( subtitleStreams(streams).map((stream) => stream.streamKey) ); } /** * @public * Returns the stream keys for playlist streams in a media context * @param streams - The media context from which to return the stream keys * @returns The playlist stream keys in the media context */ export function playlistStreamKeys(streams: readonly StreamMetadata[]): StreamKey[] { return removeUndefined( playlistStreams(streams).map((stream) => stream.streamKey) ); } /** * @public * Returns the stream keys for ancillary streams in a media context * @param streams - The media context from which to return the stream keys * @returns The ancillary stream keys in the media context */ export function ancillaryStreamKeys( streams: readonly StreamMetadata[] ): StreamKey[] { return removeUndefined( ancillaryStreams(streams).map((stream) => stream.streamKey) ); } function removeUndefined(values: readonly (T | undefined)[]): T[] { // This isn't really typechecked as you would like :( return values.filter((v): v is T => v !== undefined); } /** @public */ export function newSilentMatrix(rows: number, cols: number): Gain[][] { return new Array(rows).fill(0).map(() => new Array(cols).fill(null)); } /** * @public * * Provided for compatibilty, this is just e.g. `{ type: "sine", freq: 444 }` */ export function mkSine(freq: number): Wave { return { type: "sine", freq } } /** * @public * Select all the streams from the input * @param streams - The streams from the inbound Context * @returns Array of selected StreamKeys */ export function selectAll(streams: readonly StreamMetadata[]): StreamKey[] { return streams.map(s => s.streamKey); } /** * @public * Select all the audio and video streams from the input * @param streams - The streams from the inbound Context * @returns Array of selected StreamKeys */ export function selectAV(streams: readonly StreamMetadata[]): StreamKey[] { const audio = audioStreamKeys(streams); const video = videoStreamKeys(streams); return audio.concat(video); } /** * @public * Select all the subtitle streams from the input * @param streams - The streams from the inbound Context * @returns Array of selected StreamKeys */ export function selectSubtitles(streams: readonly StreamMetadata[]): StreamKey[] { return subtitleStreamKeys(streams); } /** * @public * Select all the audio streams from the input * @param streams - The streams from the inbound Context * @returns Array of selected StreamKeys */ export function selectAudio(streams: readonly StreamMetadata[]): StreamKey[] { return audioStreamKeys(streams); } /** * @public * Select all the video streams from the input * @param streams - The streams from the inbound Context * @returns Array of selected StreamKeys */ export function selectVideo(streams: readonly StreamMetadata[]): StreamKey[] { return videoStreamKeys(streams); } /** * @public * Select all the ancillary data streams from the input * @param streams - The streams from the inbound Context * @returns Array of selected StreamKeys */ export function selectAncillary(streams: readonly StreamMetadata[]): StreamKey[] { return ancillaryStreamKeys(streams); } /** * @public * Create a selector selecting all the video streams from the input with the specified rendition name * @param renditionName - The streams from the inbound Context * @returns Array of selected StreamKeys */ export function selectVideoRendition(renditionName: string) { return (streams: readonly StreamMetadata[]): StreamKey[] => videoStreamKeys(streams).filter((s) => s.renditionName == renditionName); } /** @public */ export function selectExactKey(key: StreamKey) { return (streams: readonly StreamMetadata[]): StreamKey[] => streams.map((s) => s.streamKey).filter((s) => s && streamKeysAreEqual(s, key)) as StreamKey[]; } /** @public */ export function selectPlaylist(streams: readonly StreamMetadata[]): StreamKey[] { return playlistStreamKeys(streams); } /** * @public * Generate encryption parameters from from an encryption KeyID and Key, * in the form KEYID:KEY, both 16byte hexadecimal */ export function mkEncryption( encryption: string | undefined, pssh?: string | undefined, ): EncryptionSettings | undefined { let encryption_params = encryption ? encryption.split(":").map((s) => s.trim()) : undefined; if (encryption_params && encryption_params.length !== 2) { errorlog( "Warning: bad encryption format, must have two fields (hexadecimal key id and hexadecimal key)" ); encryption_params = undefined; } return encryption_params ? { encryptionKeyId: encryption_params[0], encryptionKey: encryption_params[1], encryptionPssh: pssh || "", } : undefined; } /** @public */ export function videoToPin(pin: Pins): (streams: StreamMetadata[]) => PinToKey { return function(streams: StreamMetadata[]): PinToKey { const video = videoStreamKeys(streams); return toPin(pin, video); }; } /** @public */ export function audioToPin(pin: Pins): (streams: StreamMetadata[]) => PinToKey { return function(streams: StreamMetadata[]): PinToKey { const audio = audioStreamKeys(streams); return toPin(pin, audio); }; } /** @public */ export function avToPin(pin: Pins): (streams: StreamMetadata[]) => PinToKey { return function(streams: StreamMetadata[]): PinToKey { const audio = audioStreamKeys(streams); const video = videoStreamKeys(streams); const keys = audio.concat(video); return toPin(pin, keys); }; } /** @public */ export function sourceToPin(source: string, pin: Pins): (streams: StreamMetadata[]) => PinToKey { return function(streams: StreamMetadata[]): PinToKey { const matching = removeUndefined(streams).filter((s) => s.streamKey?.sourceName == source).map((s) => s.streamKey) as StreamKey[]; return toPin(pin, matching); }; } /** @public */ export function subtitlesToPin(pin: Pins): (streams: StreamMetadata[]) => PinToKey { return function(streams: StreamMetadata[]): PinToKey { const subs = subtitleStreamKeys(streams); return toPin(pin, subs); }; } /** @internal */ function toPin(pin: Pins, keys: StreamKey[]): PinToKey { // We want to simply use // return { [pin]: keys }; // but doing so loses the types // let o: PinToKey = {}; // o[pin] = keys; // return o; return { [pin]: keys } as PinToKey; } /** * @public * Validation function to require at least one audio and at least one video stream. Often the default validation * will happen to ensure this, as audio and video are subscribed from separate media nodes, but when one media node * will produce both audio and video, default validation cannot know that both are required. */ export function requireAV(ctx: Context): boolean { const streams = ctx.streams; return ( audioStreamKeys(streams).length >= 1 && videoStreamKeys(streams).length >= 1 ) } /** * @public * Validation function to require exactly N audio and exactly M video streams. Often the default validation * will happen to ensure this, as audio and video are subscribed from separate media nodes, but when one media node * will produce both audio and video, default validation cannot know that both are required. */ export function requireExactAV({ audio, video }: { audio: number, video: number }): (ctx: Context) => boolean { return (ctx: Context) => { const streams = ctx.streams; return ( audioStreamKeys(streams).length == audio && videoStreamKeys(streams).length == video ) } } /** * @public * Compares two stream keys by value, returning true if the stream keys refer to the same stream */ export function streamKeysAreEqual(l: StreamKey, r: StreamKey): unknown { return l.streamId == r.streamId && l.sourceName == r.sourceName && l.programNumber == r.programNumber && l.renditionName == r.renditionName; }