@norskvideo/norsk-sdk 0.0.322

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/sdk.ts ADDED
@@ -0,0 +1,726 @@
1
+ import * as grpc from "@grpc/grpc-js";
2
+ import { MediaClient } from "@norskvideo/norsk-api/lib/media_grpc_pb";
3
+ import {
4
+ Version,
5
+ CurrentLoad,
6
+ Log_Level,
7
+ } from "@norskvideo/norsk-api/lib/shared/common_pb";
8
+ import {
9
+ Wave,
10
+ NorskStatusEvent,
11
+ SineWave,
12
+ } from "@norskvideo/norsk-api/lib/media_pb";
13
+ import { Empty } from "@bufbuild/protobuf";
14
+ import { debuglog, norskHost, norskPort } from "./shared/utils";
15
+ import { MediaNodeState, PinToKey } from "./media_nodes/common";
16
+ import {
17
+ AudioSignalGeneratorNode,
18
+ AudioSignalGeneratorSettings,
19
+ BrowserInputNode,
20
+ BrowserInputSettings,
21
+ DeckLinkInputNode,
22
+ DeckLinkInputSettings,
23
+ ImageFileInputNode,
24
+ ImageFileInputSettings,
25
+ LocalFileInputSettings,
26
+ M3u8InputNode,
27
+ M3u8MediaInputSettings,
28
+ Mp4FileInputNode,
29
+ Mp4FileInputSettings,
30
+ NorskInput,
31
+ RtmpServerInputNode,
32
+ RtmpServerInputSettings,
33
+ RtpInputNode,
34
+ RtpInputSettings,
35
+ SrtInputNode,
36
+ SrtInputSettings,
37
+ TsFileInputNode,
38
+ UdpTsInputNode,
39
+ UdpTsInputSettings,
40
+ WebVttFileInputNode,
41
+ WhipInputNode,
42
+ WhipInputSettings,
43
+ } from "./media_nodes/input";
44
+ import {
45
+ Gain,
46
+ NorskProcessor,
47
+ LocalWebRTCNode,
48
+ LocalWebRTCSettings,
49
+ NorskDuplex,
50
+ } from "./media_nodes/processor";
51
+ import {
52
+ HlsAudioOutputNode,
53
+ HlsAudioOutputSettings,
54
+ HlsMasterOutputNode,
55
+ HlsMasterOutputSettings,
56
+ HlsMasterPushOutputNode,
57
+ HlsMasterPushOutputSettings,
58
+ HlsTsAudioOutputNode,
59
+ HlsTsAudioOutputSettings,
60
+ HlsTsAudioPushOutputNode,
61
+ HlsTsAudioPushOutputSettings,
62
+ HlsTsCombinedPushOutputNode,
63
+ HlsTsCombinedPushOutputSettings,
64
+ HlsTsVideoOutputNode,
65
+ HlsTsVideoOutputSettings,
66
+ HlsTsVideoPushOutputNode,
67
+ HlsTsVideoPushOutputSettings,
68
+ HlsVideoOutputNode,
69
+ HlsVideoOutputSettings,
70
+ HlsWebVttOutputNode,
71
+ HlsWebVttOutputSettings,
72
+ HlsWebVttPushOutputNode,
73
+ HlsWebVttPushOutputSettings,
74
+ Mp4FileOutputNode,
75
+ Mp4FileOutputSettings,
76
+ NorskOutput,
77
+ RtmpOutputNode,
78
+ RtmpOutputSettings,
79
+ SrtOutputNode,
80
+ SrtOutputSettings,
81
+ TsFileOutputNode,
82
+ TsFileOutputSettings,
83
+ TsUdpOutputNode,
84
+ TsUdpOutputSettings,
85
+ WebRTCWhipOutputNode,
86
+ WebRTCWhipOutputSettings,
87
+ } from "./media_nodes/output";
88
+ import { StreamKey, StreamMetadata } from "./media_nodes/types";
89
+ import { hardwareInfo, NorskSystem } from "./system";
90
+
91
+ export * from "./types";
92
+ export * from "./system";
93
+ export * from "./media_nodes/types";
94
+ export * from "./media_nodes/input";
95
+ export * from "./media_nodes/output";
96
+ export * from "./media_nodes/processor";
97
+ export * from "./media_nodes/common";
98
+ export { AudioCodec } from "@norskvideo/norsk-api/lib/media_pb";
99
+ export * from "./system";
100
+ export { Version } from "@norskvideo/norsk-api/lib/shared/common_pb";
101
+
102
+ /** @public */
103
+ export type Log = {
104
+ level:
105
+ | "emergency"
106
+ | "alert"
107
+ | "critical"
108
+ | "error"
109
+ | "warning"
110
+ | "notice"
111
+ | "info"
112
+ | "debug";
113
+ timestamp: Date;
114
+ message: string;
115
+ };
116
+
117
+ /**
118
+ * @public
119
+ * Top level Norsk configuration
120
+ */
121
+ export interface NorskSettings {
122
+ /**
123
+ * Callback URL to listen on for gRPC session with Norsk Media
124
+ * Defaults to $NORSK_HOST:$NORSK_PORT if the environment variables are set
125
+ * where NORSK_HOST defaults to "127.0.0.1" and NORSK_PORT to "6790"
126
+ * (so "127.0.0.1:6790" if neither variable is set)
127
+ */
128
+ url?: string;
129
+ onAttemptingToConnect?: () => void;
130
+ onConnecting?: () => void;
131
+ onReady?: () => void;
132
+ onFailedToConnect?: () => void;
133
+ /** Code to execute if the Norsk node is shutdown - by default if logs and exits the client application */
134
+ onShutdown?: () => void;
135
+ onCurrentLoad?: (load: CurrentLoad) => void;
136
+ onHello?: (version: Version) => void;
137
+ onLogEvent?: (log: Log) => void;
138
+ /**
139
+ * Manually handle license events, such as missing/invalid licenses and
140
+ * sandbox timeout. (Logs messages to console by default.)
141
+ */
142
+ onLicenseEvent?: (message: string) => void;
143
+ }
144
+
145
+ /**
146
+ * @public
147
+ * The entrypoint for all Norsk Media applications
148
+ *
149
+ * @example
150
+ * ```ts
151
+ * const norsk = new Norsk();
152
+ * ```
153
+ */
154
+ export class Norsk {
155
+ /** @internal */
156
+ client: MediaClient;
157
+ /** @internal */
158
+ nodes: MediaNodeState[];
159
+ /** @internal */
160
+ settings: NorskSettings;
161
+ /** @internal */
162
+ connectivityState: number;
163
+ /** @internal */
164
+ statusStream?: grpc.ClientReadableStream<NorskStatusEvent>;
165
+ /** @internal */
166
+ publicWebPort?: number;
167
+
168
+ /**
169
+ * Implements the {@link NorskInput} interface
170
+ */
171
+ public input: NorskInput;
172
+ /**
173
+ * Implements the {@link NorskOutput} interface
174
+ */
175
+ public output: NorskOutput;
176
+ /**
177
+ * Implements the {@link NorskDuplex} interface
178
+ */
179
+ public duplex: NorskDuplex;
180
+ /**
181
+ * Implements the {@link NorskProcessor} interface
182
+ */
183
+ public processor: NorskProcessor;
184
+ /**
185
+ * Implements the {@link NorskSystem} interface
186
+ */
187
+ public system: NorskSystem;
188
+
189
+ /**
190
+ * Norsk Runtime version informatio
191
+ */
192
+ public version: Version;
193
+
194
+ /* @internal */
195
+ public resolveVersion: () => void;
196
+
197
+ /* @internal */
198
+ public initialised: Promise<void>;
199
+
200
+ /** @internal */
201
+ async registerNode<N extends MediaNodeState>(node: N): Promise<N> {
202
+ this.nodes.push(node);
203
+ return node;
204
+ }
205
+
206
+ public close() {
207
+ this.processor.close();
208
+ for (var n of this.nodes) {
209
+ n.close();
210
+ }
211
+ this.statusStream?.cancel();
212
+ try {
213
+ this.client.close();
214
+ } catch { }
215
+ }
216
+
217
+ /** @internal */
218
+ handleStatusEvent(data: NorskStatusEvent) {
219
+ const messageCase = data.message.case;
220
+ switch (messageCase) {
221
+ case undefined:
222
+ break;
223
+ case "hello": {
224
+ debuglog(
225
+ "Norsk status channel connected: %s",
226
+ data.message.value.version
227
+ );
228
+ this.publicWebPort = data.message.value.publicWebPort;
229
+ if (data.message.value.version === undefined) {
230
+ throw new Error("Norsk version is undefined");
231
+ } else {
232
+ this.version = data.message.value.version;
233
+ }
234
+ this.settings.onHello &&
235
+ data.message.value.version &&
236
+ this.settings.onHello(data.message.value.version);
237
+ this.resolveVersion();
238
+ break;
239
+ }
240
+ case "currentLoad": {
241
+ this.settings.onCurrentLoad &&
242
+ this.settings.onCurrentLoad(data.message.value);
243
+ break;
244
+ }
245
+ case "licenseEvent": {
246
+ if (this.settings.onLicenseEvent) {
247
+ this.settings.onLicenseEvent(data.message.value);
248
+ } else {
249
+ console.log(data.message.value);
250
+ }
251
+ break;
252
+ }
253
+ case "logEvent": {
254
+ var level:
255
+ | "emergency"
256
+ | "alert"
257
+ | "critical"
258
+ | "error"
259
+ | "warning"
260
+ | "notice"
261
+ | "info"
262
+ | "debug";
263
+
264
+ switch (data.message.value.level) {
265
+ case Log_Level.EMERGENCY:
266
+ level = "emergency";
267
+ break;
268
+ case Log_Level.ALERT:
269
+ level = "alert";
270
+ break;
271
+ case Log_Level.CRITICAL:
272
+ level = "critical";
273
+ break;
274
+ case Log_Level.ERROR:
275
+ level = "error";
276
+ break;
277
+ case Log_Level.WARNING:
278
+ level = "warning";
279
+ break;
280
+ case Log_Level.NOTICE:
281
+ level = "notice";
282
+ break;
283
+ case Log_Level.INFO:
284
+ level = "info";
285
+ break;
286
+ case Log_Level.DEBUG:
287
+ level = "debug";
288
+ break;
289
+ }
290
+ var log: Log = {
291
+ level: level,
292
+ message: data.message.value.msg,
293
+ timestamp: new Date(Number(data.message.value.timestamp) / 1000),
294
+ };
295
+ if (this.settings.onLogEvent) {
296
+ this.settings.onLogEvent(log);
297
+ } else {
298
+ debuglog("Norsk log event: %o", log);
299
+ }
300
+ break;
301
+ }
302
+ default:
303
+ const exhaustiveCheck: never = messageCase;
304
+ throw new Error(`Unhandled case: ${exhaustiveCheck}`);
305
+ }
306
+ }
307
+
308
+ /** @internal */
309
+ connectivityStateWatcher() {
310
+ var channel = this.client.getChannel();
311
+ var connectivityState = channel.getConnectivityState(true);
312
+
313
+ switch (connectivityState) {
314
+ case 0: {
315
+ if (this.connectivityState == 1 || this.connectivityState == 2) {
316
+ this.settings.onShutdown && this.settings.onShutdown();
317
+ }
318
+ // Idle
319
+ this.settings.onAttemptingToConnect &&
320
+ this.settings.onAttemptingToConnect();
321
+ break;
322
+ }
323
+ case 1: {
324
+ // Connecting
325
+ this.settings.onConnecting && this.settings.onConnecting();
326
+ break;
327
+ }
328
+ case 2: {
329
+ // Ready
330
+ this.settings.onReady && this.settings.onReady();
331
+ this.statusStream = this.client.createStatusChannel(new Empty());
332
+ this.statusStream.on("data", this.handleStatusEvent.bind(this));
333
+ this.statusStream.on("error", () => {
334
+ return;
335
+ });
336
+ break;
337
+ }
338
+ case 3: {
339
+ // Transient failure
340
+ this.settings.onFailedToConnect && this.settings.onFailedToConnect();
341
+ break;
342
+ }
343
+ case 4: {
344
+ // Shutdown
345
+ this.settings.onShutdown && this.settings.onShutdown();
346
+ break;
347
+ }
348
+ }
349
+ debuglog("Channel connectivity state change: %d", connectivityState);
350
+
351
+ this.connectivityState = connectivityState;
352
+ channel.watchConnectivityState(connectivityState, Infinity, () => {
353
+ this.connectivityStateWatcher();
354
+ });
355
+ }
356
+
357
+ /** @public */
358
+ public static async connect(
359
+ settings?: NorskSettings
360
+ ) {
361
+ settings = settings ?? {};
362
+ if (!settings.onShutdown) {
363
+ settings.onShutdown = () => {
364
+ console.log("Norsk has shutdown");
365
+ process.exit(1)
366
+ }
367
+ }
368
+ let norsk = new Norsk(settings);
369
+ await norsk.initialised;
370
+ return norsk;
371
+ }
372
+
373
+ /** @internal */
374
+ constructor(norskSettings: NorskSettings) {
375
+ this.connectivityState = 0;
376
+ this.client = new MediaClient(
377
+ norskSettings.url ? norskSettings.url : norskHost() + ":" + norskPort(),
378
+ grpc.credentials.createInsecure()
379
+ );
380
+ this.settings = norskSettings;
381
+ this.connectivityStateWatcher();
382
+ this.initialised = new Promise((resolve, reject) => {
383
+ this.resolveVersion = resolve;
384
+ });
385
+ this.nodes = [];
386
+ this.input = {
387
+ rtmpServer: async (settings: RtmpServerInputSettings) =>
388
+ RtmpServerInputNode.create(settings, this.client),
389
+ localTsFile: async (settings: LocalFileInputSettings) =>
390
+ TsFileInputNode.create(settings, this.client),
391
+ srt: async (settings: SrtInputSettings) =>
392
+ SrtInputNode.create(settings, this.client),
393
+ whip: async (settings: WhipInputSettings) =>
394
+ WhipInputNode.create(settings, this.client),
395
+ m3u8Media: async (settings: M3u8MediaInputSettings) =>
396
+ M3u8InputNode.create(settings, this.client),
397
+ udpTs: async (settings: UdpTsInputSettings) =>
398
+ UdpTsInputNode.create(settings, this.client),
399
+ localWebVttFile: async (settings: LocalFileInputSettings) =>
400
+ WebVttFileInputNode.create(settings, this.client),
401
+ imageFile: async (settings: ImageFileInputSettings) =>
402
+ ImageFileInputNode.create(settings, this.client),
403
+ localMp4File: async (settings: Mp4FileInputSettings) =>
404
+ Mp4FileInputNode.create(settings, this.client),
405
+ rtp: async (settings: RtpInputSettings) =>
406
+ RtpInputNode.create(settings, this.client),
407
+ audioSignal: async (settings: AudioSignalGeneratorSettings) =>
408
+ AudioSignalGeneratorNode.create(settings, this.client),
409
+ browser: async (settings: BrowserInputSettings) =>
410
+ BrowserInputNode.create(settings, this.client),
411
+ deckLink: async (settings: DeckLinkInputSettings) =>
412
+ DeckLinkInputNode.create(settings, this.client),
413
+ };
414
+
415
+ this.output = {
416
+ hlsVideo: async (settings: HlsVideoOutputSettings) =>
417
+ HlsVideoOutputNode.create(settings, this.client),
418
+ hlsAudio: async (
419
+ settings: HlsAudioOutputSettings
420
+ ): Promise<HlsAudioOutputNode> =>
421
+ HlsAudioOutputNode.create(settings, this.client),
422
+ hlsWebVtt: async (settings: HlsWebVttOutputSettings) =>
423
+ HlsWebVttOutputNode.create(settings, this.client),
424
+ hlsWebVttPush: async (settings: HlsWebVttPushOutputSettings) =>
425
+ HlsWebVttPushOutputNode.create(settings, this.client),
426
+ hlsTsVideo: async (settings: HlsTsVideoOutputSettings) =>
427
+ HlsTsVideoOutputNode.create(settings, this.client),
428
+ tsUdp: async (settings: TsUdpOutputSettings) =>
429
+ TsUdpOutputNode.create(settings, this.client),
430
+ srt: async (settings: SrtOutputSettings) =>
431
+ SrtOutputNode.create(settings, this.client),
432
+ hlsTsAudio: async (settings: HlsTsAudioOutputSettings) =>
433
+ HlsTsAudioOutputNode.create(settings, this.client),
434
+ hlsTsVideoPush: async (settings: HlsTsVideoPushOutputSettings) =>
435
+ HlsTsVideoPushOutputNode.create(settings, this.client),
436
+ hlsTsAudioPush: async (settings: HlsTsAudioPushOutputSettings) =>
437
+ HlsTsAudioPushOutputNode.create(settings, this.client),
438
+ hlsTsCombinedPush: async (settings: HlsTsCombinedPushOutputSettings) =>
439
+ HlsTsCombinedPushOutputNode.create(settings, this.client),
440
+ hlsMaster: async (settings: HlsMasterOutputSettings) =>
441
+ HlsMasterOutputNode.create(
442
+ settings,
443
+ this.client
444
+ ),
445
+ hlsMasterPush: async (settings: HlsMasterPushOutputSettings) =>
446
+ HlsMasterPushOutputNode.create(settings, this.client),
447
+ webRTCWhip: async (settings: WebRTCWhipOutputSettings) =>
448
+ WebRTCWhipOutputNode.create(settings, this.client),
449
+
450
+ rtmp: async (settings: RtmpOutputSettings) =>
451
+ RtmpOutputNode.create(settings, this.client),
452
+ localTsFile: async (settings: TsFileOutputSettings) =>
453
+ TsFileOutputNode.create(settings, this.client),
454
+ localMp4File: async (settings: Mp4FileOutputSettings) =>
455
+ Mp4FileOutputNode.create(settings, this.client),
456
+ };
457
+ this.processor = new NorskProcessor(this.client);
458
+ this.system = {
459
+ hardwareInfo: async () => hardwareInfo(this.client),
460
+ };
461
+ this.duplex = {
462
+ localWebRTC: async (settings: LocalWebRTCSettings) =>
463
+ LocalWebRTCNode.create(
464
+ settings,
465
+ this.client
466
+ ),
467
+ };
468
+
469
+ // Rob's notes: Help, I need somebody to do this properly without 'any' and 'ignore'
470
+ for (var i in this.output) {
471
+ let k = i as keyof NorskOutput;
472
+ let original = this.output[k];
473
+ let fn = async (s: any) => {
474
+ let n = await original(s);
475
+ return this.registerNode(n);
476
+ };
477
+
478
+ //@ts-ignore
479
+ this.output[k] = fn;
480
+ }
481
+ for (var i in this.input) {
482
+ let k = i as keyof NorskInput;
483
+ let original = this.input[k];
484
+ let fn = async (s: any) => {
485
+ let n = await original(s);
486
+ return this.registerNode(n);
487
+ };
488
+
489
+ //@ts-ignore
490
+ this.input[k] = fn;
491
+ }
492
+ }
493
+ }
494
+
495
+ /**
496
+ * @public
497
+ * Filters a context to only the audio streams within it
498
+ * @param streams - The media context from which to return the streams
499
+ * @returns The audio streams in the media context
500
+ */
501
+ export function audioStreams(
502
+ streams: readonly StreamMetadata[]
503
+ ): StreamMetadata[] {
504
+ return streams.filter((stream) => stream.message.case === "audio");
505
+ }
506
+
507
+ /**
508
+ * @public
509
+ * Filters a context to only the video streams within it
510
+ * @param streams - The media context from which to return the streams
511
+ * @returns The video streams in the media context
512
+ */
513
+ export function videoStreams(
514
+ streams: readonly StreamMetadata[]
515
+ ): StreamMetadata[] {
516
+ return streams.filter((stream) => stream.message.case === "video");
517
+ }
518
+
519
+ /**
520
+ * @public
521
+ * Filters a context to only the subtitle streams within it
522
+ * @param streams - The media context from which to return the streams
523
+ * @returns The subtitle streams in the media context
524
+ */
525
+ export function subtitleStreams(
526
+ streams: readonly StreamMetadata[]
527
+ ): StreamMetadata[] {
528
+ return streams.filter((stream) => stream.message.case === "subtitle");
529
+ }
530
+
531
+ /**
532
+ * @public
533
+ * Returns the stream keys for audio streams in a media context
534
+ * @param streams - The media context from which to return the stream keys
535
+ * @returns The audio stream keys in the media context
536
+ */
537
+ export function audioStreamKeys(
538
+ streams: readonly StreamMetadata[]
539
+ ): StreamKey[] {
540
+ return removeUndefined(
541
+ audioStreams(streams).map((stream) => stream.streamKey)
542
+ );
543
+ }
544
+
545
+ /**
546
+ * @public
547
+ * Returns the stream keys for video streams in a media context
548
+ * @param streams - The media context from which to return the stream keys
549
+ * @returns The video stream keys in the media context
550
+ */
551
+ export function videoStreamKeys(
552
+ streams: readonly StreamMetadata[]
553
+ ): StreamKey[] {
554
+ return removeUndefined(
555
+ videoStreams(streams).map((stream) => stream.streamKey)
556
+ );
557
+ }
558
+
559
+ /**
560
+ * @public
561
+ * Returns the stream keys for subtitle streams in a media context
562
+ * @param streams - The media context from which to return the stream keys
563
+ * @returns The subtitle stream keys in the media context
564
+ */
565
+ export function subtitleStreamKeys(
566
+ streams: readonly StreamMetadata[]
567
+ ): StreamKey[] {
568
+ return removeUndefined(
569
+ subtitleStreams(streams).map((stream) => stream.streamKey)
570
+ );
571
+ }
572
+
573
+ function removeUndefined<T>(values: readonly (T | undefined)[]): T[] {
574
+ // This isn't really typechecked as you would like :(
575
+ return values.filter((v): v is T => v !== undefined);
576
+ }
577
+
578
+ /** @public */
579
+ export function newSilentMatrix(rows: number, cols: number): Gain[][] {
580
+ return new Array(rows).fill(0).map(() => new Array(cols).fill(null));
581
+ }
582
+
583
+ /** @public */
584
+ export function mkSine(freq: number) {
585
+ return new Wave({ message: { case: "sine", value: new SineWave({ freq }) } });
586
+ }
587
+
588
+ /**
589
+ * @public
590
+ * Select all the audio and video streams from the input
591
+ * @param streams - The streams from the inbound Context
592
+ * @returns Array of selected StreamKeys
593
+ */
594
+ export function selectAV(streams: readonly StreamMetadata[]): StreamKey[] {
595
+ const audio = audioStreamKeys(streams);
596
+ const video = videoStreamKeys(streams);
597
+ return audio.concat(video);
598
+ }
599
+
600
+ /** @public */
601
+ export function selectSubtitles(
602
+ streams: readonly StreamMetadata[]
603
+ ): StreamKey[] {
604
+ return subtitleStreamKeys(streams);
605
+ }
606
+
607
+ /** @public */
608
+ export function selectAudio(streams: readonly StreamMetadata[]): StreamKey[] {
609
+ return audioStreamKeys(streams);
610
+ }
611
+
612
+ /** @public */
613
+ export function selectVideo(streams: readonly StreamMetadata[]): StreamKey[] {
614
+ return videoStreamKeys(streams);
615
+ }
616
+
617
+ /** @public */
618
+ export function selectAllVideos(
619
+ count: number
620
+ ): (streams: readonly StreamMetadata[]) => StreamKey[] {
621
+ return (streams: readonly StreamMetadata[]) => {
622
+ const video = videoStreamKeys(streams);
623
+ if (video.length == count) {
624
+ return video;
625
+ }
626
+ return [];
627
+ };
628
+ }
629
+
630
+ /** @public */
631
+ export function selectAllAudios(
632
+ count: number
633
+ ): (streams: readonly StreamMetadata[]) => StreamKey[] {
634
+ return (streams: readonly StreamMetadata[]) => {
635
+ const audio = audioStreamKeys(streams);
636
+ if (audio.length == count) {
637
+ return audio;
638
+ }
639
+ return [];
640
+ };
641
+ }
642
+
643
+ /**
644
+ * @public
645
+ * Generate encryption parameters from from an encryption KeyID and Key,
646
+ * in the form KEYID:KEY, both 16byte hexadecimal
647
+ */
648
+ export function mkEncryption(
649
+ encryption: string | undefined
650
+ ): { encryptionKey: string; encryptionKeyId: string } | undefined {
651
+ let encryption_params = encryption
652
+ ? encryption.split(":").map((s) => s.trim())
653
+ : undefined;
654
+ if (encryption_params && encryption_params.length !== 2) {
655
+ console.log(
656
+ "Warning: bad encryption format, must have two fields (hexadecimal key id and hexadecimal key)"
657
+ );
658
+ encryption_params = undefined;
659
+ }
660
+ return encryption_params
661
+ ? {
662
+ encryptionKeyId: encryption_params[0],
663
+ encryptionKey: encryption_params[1],
664
+ }
665
+ : undefined;
666
+ }
667
+
668
+ /** @public */
669
+ export const videoToPin = <Pins extends string>(pin: Pins) => {
670
+ return (streams: StreamMetadata[]): PinToKey<Pins> => {
671
+ const video = videoStreamKeys(streams);
672
+ if (video.length == 1) {
673
+ // I want to do this, but it loses the types
674
+ // return { [pin]: video };
675
+ let o: PinToKey<Pins> = {};
676
+ o[pin] = video;
677
+ return o;
678
+ }
679
+ return undefined;
680
+ };
681
+ };
682
+
683
+ /** @public */
684
+ export const audioToPin = <Pins extends string>(pin: Pins) => {
685
+ return (streams: StreamMetadata[]): PinToKey<Pins> => {
686
+ const audio = audioStreamKeys(streams);
687
+ if (audio.length >= 1) {
688
+ // I want to do this, but it loses the types
689
+ // return { [pin]: video };
690
+ let o: PinToKey<Pins> = {};
691
+ o[pin] = audio;
692
+ return o;
693
+ }
694
+ return undefined;
695
+ };
696
+ };
697
+
698
+ /** @public */
699
+ export const avToPin = <Pins extends string>(pin: Pins) => {
700
+ return (streams: StreamMetadata[]): PinToKey<Pins> => {
701
+ const audio = audioStreamKeys(streams);
702
+ const video = videoStreamKeys(streams);
703
+ const keys = audio.concat(video);
704
+ if (keys.length > 1) {
705
+ // I want to do this, but it loses the types
706
+ // return { [pin]: video };
707
+ let o: PinToKey<Pins> = {};
708
+ o[pin] = keys;
709
+ return o;
710
+ }
711
+ return undefined;
712
+ };
713
+ };
714
+
715
+ /** @public */
716
+ export const subtitlesToPin = <Pins extends string>(pin: Pins) => {
717
+ return (streams: StreamMetadata[]): PinToKey<Pins> => {
718
+ const subs = subtitleStreamKeys(streams);
719
+ if (subs.length == 1) {
720
+ let o: PinToKey<Pins> = {};
721
+ o[pin] = subs;
722
+ return o;
723
+ }
724
+ return undefined;
725
+ };
726
+ };