@m7mdxzx1/discord-video-strem 0.0.1

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.
Files changed (89) hide show
  1. package/README.md +294 -0
  2. package/dist/client/GatewayEvents.d.ts +27 -0
  3. package/dist/client/GatewayEvents.js +1 -0
  4. package/dist/client/GatewayOpCodes.d.ts +40 -0
  5. package/dist/client/GatewayOpCodes.js +41 -0
  6. package/dist/client/Streamer.d.ts +41 -0
  7. package/dist/client/Streamer.js +189 -0
  8. package/dist/client/encryptor/TransportEncryptor.d.ts +16 -0
  9. package/dist/client/encryptor/TransportEncryptor.js +38 -0
  10. package/dist/client/index.d.ts +4 -0
  11. package/dist/client/index.js +4 -0
  12. package/dist/client/packet/AudioPacketizer.d.ts +8 -0
  13. package/dist/client/packet/AudioPacketizer.js +22 -0
  14. package/dist/client/packet/BaseMediaPacketizer.d.ts +109 -0
  15. package/dist/client/packet/BaseMediaPacketizer.js +243 -0
  16. package/dist/client/packet/VideoPacketizerAnnexB.d.ts +132 -0
  17. package/dist/client/packet/VideoPacketizerAnnexB.js +231 -0
  18. package/dist/client/packet/VideoPacketizerVP8.d.ts +15 -0
  19. package/dist/client/packet/VideoPacketizerVP8.js +56 -0
  20. package/dist/client/packet/index.d.ts +4 -0
  21. package/dist/client/packet/index.js +4 -0
  22. package/dist/client/processing/AnnexBHelper.d.ts +93 -0
  23. package/dist/client/processing/AnnexBHelper.js +132 -0
  24. package/dist/client/voice/BaseMediaConnection.d.ts +118 -0
  25. package/dist/client/voice/BaseMediaConnection.js +319 -0
  26. package/dist/client/voice/MediaUdp.d.ts +26 -0
  27. package/dist/client/voice/MediaUdp.js +140 -0
  28. package/dist/client/voice/StreamConnection.d.ts +10 -0
  29. package/dist/client/voice/StreamConnection.js +30 -0
  30. package/dist/client/voice/VoiceConnection.d.ts +7 -0
  31. package/dist/client/voice/VoiceConnection.js +10 -0
  32. package/dist/client/voice/VoiceMessageTypes.d.ts +136 -0
  33. package/dist/client/voice/VoiceMessageTypes.js +1 -0
  34. package/dist/client/voice/VoiceOpCodes.d.ts +21 -0
  35. package/dist/client/voice/VoiceOpCodes.js +22 -0
  36. package/dist/client/voice/index.d.ts +5 -0
  37. package/dist/client/voice/index.js +5 -0
  38. package/dist/index.d.ts +5 -0
  39. package/dist/index.js +5 -0
  40. package/dist/media/AudioStream.d.ts +25 -0
  41. package/dist/media/AudioStream.js +63 -0
  42. package/dist/media/BaseMediaStream.d.ts +31 -0
  43. package/dist/media/BaseMediaStream.js +145 -0
  44. package/dist/media/LibavCodecId.d.ts +541 -0
  45. package/dist/media/LibavCodecId.js +552 -0
  46. package/dist/media/LibavDecoder.d.ts +5 -0
  47. package/dist/media/LibavDecoder.js +63 -0
  48. package/dist/media/LibavDemuxer.d.ts +23 -0
  49. package/dist/media/LibavDemuxer.js +295 -0
  50. package/dist/media/VideoStream.d.ts +7 -0
  51. package/dist/media/VideoStream.js +10 -0
  52. package/dist/media/index.d.ts +1 -0
  53. package/dist/media/index.js +1 -0
  54. package/dist/media/newApi.d.ts +126 -0
  55. package/dist/media/newApi.js +387 -0
  56. package/dist/media/utils.d.ts +1 -0
  57. package/dist/media/utils.js +4 -0
  58. package/dist/utils.d.ts +28 -0
  59. package/dist/utils.js +54 -0
  60. package/package.json +69 -0
  61. package/src/client/GatewayEvents.ts +41 -0
  62. package/src/client/GatewayOpCodes.ts +40 -0
  63. package/src/client/Streamer.ts +279 -0
  64. package/src/client/encryptor/TransportEncryptor.ts +62 -0
  65. package/src/client/index.ts +4 -0
  66. package/src/client/packet/AudioPacketizer.ts +28 -0
  67. package/src/client/packet/BaseMediaPacketizer.ts +307 -0
  68. package/src/client/packet/VideoPacketizerAnnexB.ts +263 -0
  69. package/src/client/packet/VideoPacketizerVP8.ts +73 -0
  70. package/src/client/packet/index.ts +4 -0
  71. package/src/client/processing/AnnexBHelper.ts +142 -0
  72. package/src/client/voice/BaseMediaConnection.ts +407 -0
  73. package/src/client/voice/MediaUdp.ts +171 -0
  74. package/src/client/voice/StreamConnection.ts +33 -0
  75. package/src/client/voice/VoiceConnection.ts +15 -0
  76. package/src/client/voice/VoiceMessageTypes.ts +164 -0
  77. package/src/client/voice/VoiceOpCodes.ts +21 -0
  78. package/src/client/voice/index.ts +5 -0
  79. package/src/index.ts +5 -0
  80. package/src/media/AudioStream.ts +81 -0
  81. package/src/media/BaseMediaStream.ts +173 -0
  82. package/src/media/LibavCodecId.ts +566 -0
  83. package/src/media/LibavDecoder.ts +82 -0
  84. package/src/media/LibavDemuxer.ts +348 -0
  85. package/src/media/VideoStream.ts +15 -0
  86. package/src/media/index.ts +1 -0
  87. package/src/media/newApi.ts +618 -0
  88. package/src/media/utils.ts +6 -0
  89. package/src/utils.ts +77 -0
@@ -0,0 +1,618 @@
1
+ import ffmpeg from 'fluent-ffmpeg';
2
+ import pDebounce from 'p-debounce';
3
+ import sharp from 'sharp';
4
+ import Log from 'debug-level';
5
+ import { PassThrough, type Readable } from "node:stream";
6
+ import { demux } from './LibavDemuxer.js';
7
+ import { setTimeout as delay } from 'node:timers/promises';
8
+ import { VideoStream } from './VideoStream.js';
9
+ import { AudioStream } from './AudioStream.js';
10
+ import { isFiniteNonZero } from '../utils.js';
11
+ import { AVCodecID } from './LibavCodecId.js';
12
+ import { createDecoder } from './LibavDecoder.js';
13
+
14
+ import LibAV from '@lng2004/libav.js-variant-webcodecs-avf-with-decoders';
15
+ import type { SupportedVideoCodec } from '../utils.js';
16
+ import type { MediaUdp, Streamer } from '../client/index.js';
17
+
18
+ export type EncoderOptions = {
19
+ /**
20
+ * Disable video transcoding
21
+ * If enabled, all video related settings have no effects, and the input
22
+ * video stream is used as-is.
23
+ *
24
+ * You need to ensure that the video stream has the right properties
25
+ * (keyframe every 1s, B-frames disabled). Failure to do so will result in
26
+ * a glitchy stream, or degraded performance
27
+ */
28
+ noTranscoding: boolean,
29
+
30
+ /**
31
+ * Video width
32
+ */
33
+ width: number,
34
+
35
+ /**
36
+ * Video height
37
+ */
38
+ height: number,
39
+
40
+ /**
41
+ * Video frame rate
42
+ */
43
+ frameRate?: number,
44
+
45
+ /**
46
+ * Video codec
47
+ */
48
+ videoCodec: SupportedVideoCodec,
49
+
50
+ /**
51
+ * Video average bitrate in kbps
52
+ */
53
+ bitrateVideo: number,
54
+
55
+ /**
56
+ * Video max bitrate in kbps
57
+ */
58
+ bitrateVideoMax: number,
59
+
60
+ /**
61
+ * Audio bitrate in kbps
62
+ */
63
+ bitrateAudio: number,
64
+
65
+ /**
66
+ * Enable audio output
67
+ */
68
+ includeAudio: boolean,
69
+
70
+ /**
71
+ * Enable hardware accelerated decoding
72
+ */
73
+ hardwareAcceleratedDecoding: boolean,
74
+
75
+ /**
76
+ * Add some options to minimize latency
77
+ */
78
+ minimizeLatency: boolean,
79
+
80
+ /**
81
+ * Preset for x264 and x265
82
+ */
83
+ h26xPreset: "ultrafast" | "superfast" | "veryfast" | "faster" | "fast" | "medium" | "slow" | "slower" | "veryslow" | "placebo",
84
+
85
+ /**
86
+ * Custom headers for HTTP requests
87
+ */
88
+ customHeaders: Record<string, string>,
89
+
90
+ /**
91
+ * Input for seeking time
92
+ */
93
+ seekTime?: number;
94
+
95
+ /**
96
+ * Custom ffmpeg flags/options to pass directly to ffmpeg
97
+ * These will be added to the command after other options
98
+ */
99
+ customFfmpegFlags: string[]
100
+ }
101
+
102
+ export interface AudioController {
103
+ mute(): void;
104
+ unmute(): void;
105
+ isMuted(): boolean;
106
+ setVolume(volume: number): void;
107
+ getVolume(): number;
108
+ }
109
+
110
+ export function prepareStream(
111
+ input: string | Readable,
112
+ options: Partial<EncoderOptions> = {},
113
+ cancelSignal?: AbortSignal
114
+ ) {
115
+ cancelSignal?.throwIfAborted();
116
+ const defaultOptions = {
117
+ noTranscoding: false,
118
+ // negative values = resize by aspect ratio, see https://trac.ffmpeg.org/wiki/Scaling
119
+ width: -2,
120
+ height: -2,
121
+ frameRate: undefined,
122
+ videoCodec: "H264",
123
+ bitrateVideo: 5000,
124
+ bitrateVideoMax: 7000,
125
+ bitrateAudio: 128,
126
+ includeAudio: true,
127
+ hardwareAcceleratedDecoding: false,
128
+ minimizeLatency: false,
129
+ h26xPreset: "ultrafast",
130
+ customHeaders: {
131
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.3",
132
+ "Connection": "keep-alive",
133
+ },
134
+ seekTime: 0,
135
+ customFfmpegFlags: []
136
+ } satisfies EncoderOptions;
137
+
138
+ function mergeOptions(opts: Partial<EncoderOptions>) {
139
+ return {
140
+ noTranscoding:
141
+ opts.noTranscoding ?? defaultOptions.noTranscoding,
142
+
143
+ width:
144
+ isFiniteNonZero(opts.width) ? Math.round(opts.width) : defaultOptions.width,
145
+
146
+ height:
147
+ isFiniteNonZero(opts.height) ? Math.round(opts.height) : defaultOptions.height,
148
+
149
+ frameRate:
150
+ isFiniteNonZero(opts.frameRate) && opts.frameRate > 0
151
+ ? opts.frameRate
152
+ : defaultOptions.frameRate,
153
+
154
+ videoCodec:
155
+ opts.videoCodec ?? defaultOptions.videoCodec,
156
+
157
+ bitrateVideo:
158
+ isFiniteNonZero(opts.bitrateVideo) && opts.bitrateVideo > 0
159
+ ? Math.round(opts.bitrateVideo)
160
+ : defaultOptions.bitrateVideo,
161
+
162
+ bitrateVideoMax:
163
+ isFiniteNonZero(opts.bitrateVideoMax) && opts.bitrateVideoMax > 0
164
+ ? Math.round(opts.bitrateVideoMax)
165
+ : defaultOptions.bitrateVideoMax,
166
+
167
+ bitrateAudio:
168
+ isFiniteNonZero(opts.bitrateAudio) && opts.bitrateAudio > 0
169
+ ? Math.round(opts.bitrateAudio)
170
+ : defaultOptions.bitrateAudio,
171
+
172
+ includeAudio:
173
+ opts.includeAudio ?? defaultOptions.includeAudio,
174
+
175
+ hardwareAcceleratedDecoding:
176
+ opts.hardwareAcceleratedDecoding ?? defaultOptions.hardwareAcceleratedDecoding,
177
+
178
+ minimizeLatency:
179
+ opts.minimizeLatency ?? defaultOptions.minimizeLatency,
180
+
181
+ h26xPreset:
182
+ opts.h26xPreset ?? defaultOptions.h26xPreset,
183
+
184
+ customHeaders: {
185
+ ...defaultOptions.customHeaders, ...opts.customHeaders
186
+ },
187
+
188
+ seekTime: opts.seekTime ?? defaultOptions.seekTime,
189
+ customFfmpegFlags:
190
+ opts.customFfmpegFlags ?? defaultOptions.customFfmpegFlags
191
+ } satisfies EncoderOptions
192
+ }
193
+
194
+ const mergedOptions = mergeOptions(options);
195
+
196
+ let isHttpUrl = false;
197
+ let isHls = false;
198
+
199
+ if (typeof input === "string") {
200
+ isHttpUrl = input.startsWith('http') || input.startsWith('https');
201
+ isHls = input.includes('m3u');
202
+ }
203
+
204
+ const output = new PassThrough();
205
+
206
+ // command creation
207
+ const command = ffmpeg();
208
+
209
+ // Seeking if applicable (Must be applied before input for fast seek)
210
+ if (mergedOptions.seekTime && mergedOptions.seekTime > 0) {
211
+ command.inputOption('-ss', String(mergedOptions.seekTime));
212
+ }
213
+
214
+ command.input(input)
215
+ .addOption('-loglevel', 'info');
216
+
217
+ // input options
218
+ const { hardwareAcceleratedDecoding, minimizeLatency, customHeaders } = mergedOptions;
219
+ if (hardwareAcceleratedDecoding)
220
+ command.inputOption('-hwaccel', 'auto');
221
+
222
+ if (minimizeLatency) {
223
+ command.addOptions([
224
+ '-fflags nobuffer',
225
+ '-analyzeduration 0'
226
+ ]);
227
+ }
228
+
229
+ if (isHttpUrl) {
230
+ command.inputOption('-headers',
231
+ Object.entries(customHeaders).map(([k, v]) => `${k}: ${v}`).join("\\r\\n") // Corrected escape sequence
232
+ );
233
+ if (!isHls) {
234
+ command.inputOptions([
235
+ '-reconnect 1',
236
+ '-reconnect_at_eof 1',
237
+ '-reconnect_streamed 1',
238
+ '-reconnect_delay_max 4294'
239
+ ]);
240
+ }
241
+ }
242
+
243
+ // general output options
244
+ command
245
+ .output(output)
246
+ .outputFormat("matroska");
247
+
248
+ // video setup
249
+ const {
250
+ noTranscoding, width, height, frameRate, bitrateVideo, bitrateVideoMax, videoCodec, h26xPreset
251
+ } = mergedOptions;
252
+ command.addOutputOption("-map 0:v");
253
+
254
+ if (noTranscoding)
255
+ {
256
+ command.videoCodec("copy");
257
+ }
258
+ else
259
+ {
260
+ command.videoFilter(`scale=${width}:${height}`);
261
+
262
+ if (frameRate)
263
+ command.fpsOutput(frameRate);
264
+
265
+ command.addOutputOption([
266
+ "-b:v", `${bitrateVideo}k`,
267
+ "-maxrate:v", `${bitrateVideoMax}k`,
268
+ "-bf", "0",
269
+ "-pix_fmt", "yuv420p",
270
+ "-force_key_frames", "expr:gte(t,n_forced*1)"
271
+ ]);
272
+
273
+ switch (videoCodec) {
274
+ case 'AV1':
275
+ command
276
+ .videoCodec("libsvtav1");
277
+ break;
278
+ case 'VP8':
279
+ command
280
+ .videoCodec("libvpx")
281
+ .outputOption('-deadline', 'realtime');
282
+ break;
283
+ case 'VP9':
284
+ command
285
+ .videoCodec("libvpx-vp9")
286
+ .outputOption('-deadline', 'realtime');
287
+ break;
288
+ case 'H264':
289
+ command
290
+ .videoCodec("libx264")
291
+ .outputOptions([
292
+ '-tune zerolatency',
293
+ `-preset ${h26xPreset}`,
294
+ '-profile:v baseline',
295
+ ]);
296
+ break;
297
+ case 'H265':
298
+ command
299
+ .videoCodec("libx265")
300
+ .outputOptions([
301
+ '-tune zerolatency',
302
+ `-preset ${h26xPreset}`,
303
+ '-profile:v main',
304
+ ]);
305
+ break;
306
+ }
307
+ }
308
+
309
+ // audio setup
310
+ const { includeAudio, bitrateAudio } = mergedOptions;
311
+ if (includeAudio)
312
+ command
313
+ .addOutputOption("-map 0:a?")
314
+ .audioChannels(2)
315
+ /*
316
+ * I don't have much surround sound material to test this with,
317
+ * if you do and you have better settings for this, feel free to
318
+ * contribute!
319
+ */
320
+ .addOutputOption("-lfe_mix_level 1")
321
+ .audioFrequency(48000)
322
+ .audioCodec("libopus")
323
+ .audioBitrate(`${bitrateAudio}k`)
324
+ .audioFilters("volume@internal_lib=1.0")
325
+
326
+ // Add custom ffmpeg flags
327
+ if (mergedOptions.customFfmpegFlags && mergedOptions.customFfmpegFlags.length > 0) {
328
+ command.addOptions(mergedOptions.customFfmpegFlags);
329
+ }
330
+
331
+ // exit handling
332
+ const promise = new Promise<void>((resolve, reject) => {
333
+ command.on("error", (err) => {
334
+ if (cancelSignal?.aborted)
335
+ /**
336
+ * fluent-ffmpeg might throw an error when SIGTERM is sent to
337
+ * the process, so we check if the abort signal is triggered
338
+ * and throw that instead
339
+ */
340
+ reject(cancelSignal.reason);
341
+ else
342
+ reject(err);
343
+ });
344
+ command.on("end", () => resolve());
345
+ });
346
+ promise.catch(() => {});
347
+ cancelSignal?.addEventListener("abort", () => command.kill("SIGTERM"), { once: true });
348
+ command.run();
349
+
350
+ return { command, output, promise };
351
+ }
352
+
353
+
354
+ export type PlayStreamOptions = {
355
+ /**
356
+ * Set stream type as \"Go Live\" or camera stream
357
+ */
358
+ type: "go-live" | "camera",
359
+
360
+ /**
361
+ * Override video width sent to Discord.
362
+ *
363
+ * DO NOT SPECIFY UNLESS YOU KNOW WHAT YOU'RE DOING!
364
+ */
365
+ width: number,
366
+
367
+ /**
368
+ * Override video height sent to Discord.
369
+ *
370
+ * DO NOT SPECIFY UNLESS YOU KNOW WHAT YOU'RE DOING!
371
+ */
372
+ height: number,
373
+
374
+ /**
375
+ * Override video frame rate sent to Discord.
376
+ *
377
+ * DO NOT SPECIFY UNLESS YOU KNOW WHAT YOU'RE DOING!
378
+ */
379
+ frameRate: number,
380
+
381
+ /**
382
+ * Same as ffmpeg's `readrate_initial_burst` command line flag
383
+ *
384
+ * See https://ffmpeg.org/ffmpeg.html#:~:text=%2Dreadrate_initial_burst
385
+ */
386
+ readrateInitialBurst: number | undefined,
387
+
388
+ /**
389
+ * Enable stream preview from input stream (experimental)
390
+ */
391
+ streamPreview: boolean,
392
+ seekTime?: number;
393
+ customHeaders?: Record<string, string>;
394
+ initialMuted?: boolean;
395
+ }
396
+
397
+ export async function playStream(
398
+ input: Readable, streamer: Streamer,
399
+ options: Partial<PlayStreamOptions> = {},
400
+ cancelSignal?: AbortSignal,
401
+ existingUdp: MediaUdp | null = null,
402
+ ): Promise<{ audioController?: AudioController; done: Promise<void> }>
403
+ {
404
+ const logger = new Log("playStream");
405
+ cancelSignal?.throwIfAborted();
406
+ if (!streamer.voiceConnection)
407
+ throw new Error("Bot is not connected to a voice channel");
408
+
409
+ logger.debug("Initializing demuxer");
410
+ const { video, audio } = await demux(input);
411
+ cancelSignal?.throwIfAborted();
412
+
413
+ if (!video)
414
+ throw new Error("No video stream in media");
415
+
416
+ const cleanupFuncs: (() => unknown)[] = [];
417
+ const videoCodecMap: Record<number, SupportedVideoCodec> = {
418
+ [AVCodecID.AV_CODEC_ID_H264]: "H264",
419
+ [AVCodecID.AV_CODEC_ID_H265]: "H265",
420
+ [AVCodecID.AV_CODEC_ID_VP8]: "VP8",
421
+ [AVCodecID.AV_CODEC_ID_VP9]: "VP9",
422
+ [AVCodecID.AV_CODEC_ID_AV1]: "AV1"
423
+ };
424
+ const defaultOptions = {
425
+ type: "go-live",
426
+ width: video.width,
427
+ height: video.height,
428
+ frameRate: video.framerate_num / video.framerate_den,
429
+ readrateInitialBurst: undefined,
430
+ streamPreview: false,
431
+ seekTime: 0,
432
+ customHeaders: {},
433
+ initialMuted: false,
434
+ } satisfies PlayStreamOptions;
435
+
436
+ function mergeOptions(opts: Partial<PlayStreamOptions>)
437
+ {
438
+ return {
439
+ type:
440
+ opts.type ?? defaultOptions.type,
441
+
442
+ width:
443
+ isFiniteNonZero(opts.width) && opts.width > 0
444
+ ? Math.round(opts.width)
445
+ : defaultOptions.width,
446
+
447
+ height:
448
+ isFiniteNonZero(opts.height) && opts.height > 0
449
+ ? Math.round(opts.height)
450
+ : defaultOptions.height,
451
+
452
+ frameRate: Math.round(
453
+ isFiniteNonZero(opts.frameRate) && opts.frameRate > 0
454
+ ? Math.round(opts.frameRate)
455
+ : defaultOptions.frameRate
456
+ ),
457
+
458
+ readrateInitialBurst:
459
+ isFiniteNonZero(opts.readrateInitialBurst) && opts.readrateInitialBurst > 0
460
+ ? opts.readrateInitialBurst
461
+ : defaultOptions.readrateInitialBurst,
462
+
463
+ streamPreview:
464
+ opts.streamPreview ?? defaultOptions.streamPreview,
465
+
466
+ seekTime: opts.seekTime ?? defaultOptions.seekTime,
467
+ customHeaders: {
468
+ ...defaultOptions.customHeaders, ...opts.customHeaders
469
+ },
470
+ initialMuted:
471
+ opts.initialMuted ?? defaultOptions.initialMuted,
472
+ } satisfies PlayStreamOptions;
473
+ }
474
+
475
+ const mergedOptions = mergeOptions(options);
476
+ logger.debug({ options: mergedOptions }, "Merged options");
477
+
478
+ let udp: MediaUdp;
479
+ let stopStream: () => unknown;
480
+ if (!existingUdp) {
481
+ udp = await streamer.createStream();
482
+ } else {
483
+ udp = existingUdp;
484
+ udp.mediaConnection.setSpeaking(false);
485
+ udp.mediaConnection.setVideoAttributes(false);
486
+ await delay(250);
487
+ }
488
+ // stopStream = () => streamer.stopStream();
489
+ stopStream = () => console.log("Aborted")
490
+ udp.setPacketizer(videoCodecMap[video.codec]);
491
+ udp.mediaConnection.setSpeaking(true);
492
+ udp.mediaConnection.setVideoAttributes(true, {
493
+ width: mergedOptions.width,
494
+ height: mergedOptions.height,
495
+ fps: mergedOptions.frameRate
496
+ });
497
+
498
+ const vStream = new VideoStream(udp);
499
+ video.stream.pipe(vStream);
500
+ let audioStreamInstance: AudioStream | undefined;
501
+
502
+ if (audio)
503
+ {
504
+ audioStreamInstance = new AudioStream(udp, false, mergedOptions.initialMuted);
505
+ audio.stream.pipe(audioStreamInstance);
506
+ vStream.syncStream = audioStreamInstance;
507
+ audioStreamInstance.syncStream = vStream;
508
+
509
+ const burstTime = mergedOptions.readrateInitialBurst;
510
+ if (typeof burstTime === "number")
511
+ {
512
+ vStream.sync = false;
513
+ vStream.noSleep = audioStreamInstance.noSleep = true;
514
+ const stopBurst = (pts: number) => {
515
+ if (pts < burstTime * 1000)
516
+ return;
517
+ vStream.sync = true;
518
+ if (audioStreamInstance) {
519
+ vStream.noSleep = audioStreamInstance.noSleep = false;
520
+ }
521
+ vStream.off("pts", stopBurst);
522
+ };
523
+ vStream.on("pts", stopBurst);
524
+ }
525
+ }
526
+ if (mergedOptions.streamPreview && mergedOptions.type === "go-live")
527
+ {
528
+ (async () => {
529
+ const logger = new Log("playStream:preview");
530
+ logger.debug("Initializing decoder for stream preview");
531
+ const decoder = await createDecoder(video.codec, video.codecpar);
532
+ if (!decoder)
533
+ {
534
+ logger.warn("Failed to initialize decoder. Stream preview will be disabled");
535
+ return;
536
+ }
537
+ cleanupFuncs.push(() => {
538
+ logger.debug("Freeing decoder");
539
+ decoder.free();
540
+ });
541
+ const updatePreview = pDebounce.promise(async (packet: LibAV.Packet) => {
542
+ if (!(packet.flags !== undefined && packet.flags & LibAV.AV_PKT_FLAG_KEY))
543
+ return;
544
+ const decodeStart = performance.now();
545
+ const [frame] = await decoder.decode([packet]).catch(() => []);
546
+ if (!frame)
547
+ return;
548
+ const decodeEnd = performance.now();
549
+ logger.debug(`Decoding a frame took ${decodeEnd - decodeStart}ms`);
550
+
551
+ return sharp(frame.data, {
552
+ raw: {
553
+ width: frame.width ?? 0,
554
+ height: frame.height ?? 0,
555
+ channels: 4
556
+ }
557
+ })
558
+ .resize(1024, 576, { fit: "inside" })
559
+ .jpeg()
560
+ .toBuffer()
561
+ .then(image => streamer.setStreamPreview(image))
562
+ .catch(() => {});
563
+ });
564
+ video.stream.on("data", updatePreview);
565
+ cleanupFuncs.push(() => video.stream.off("data", updatePreview));
566
+ })();
567
+ }
568
+ const streamPromise = new Promise<void>((resolve, reject) => {
569
+ cleanupFuncs.push(() => {
570
+ stopStream();
571
+ udp.mediaConnection.setSpeaking(false);
572
+ udp.mediaConnection.setVideoAttributes(false);
573
+ });
574
+ let cleanedUp = false;
575
+ const cleanup = () => {
576
+ if (cleanedUp)
577
+ return;
578
+ cleanedUp = true;
579
+ for (const f of cleanupFuncs)
580
+ f();
581
+ }
582
+ cancelSignal?.addEventListener("abort", () => {
583
+ cleanup();
584
+ reject(cancelSignal.reason);
585
+ }, { once: true })
586
+ vStream.once("finish", () => {
587
+ if (cancelSignal?.aborted)
588
+ return;
589
+ cleanup();
590
+ resolve();
591
+ });
592
+ vStream.once("error", (err) => {
593
+ if (cancelSignal?.aborted)
594
+ return;
595
+ cleanup();
596
+ reject(err);
597
+ });
598
+ if (audio) {
599
+ audio.stream.once("error", (err) => {
600
+ if (cancelSignal?.aborted)
601
+ return;
602
+ cleanup();
603
+ reject(err);
604
+ });
605
+ }
606
+ }).catch((err) => {
607
+ if (!cancelSignal?.aborted) {
608
+ logger.error("Error during playStream:", err);
609
+ }
610
+ if (err !== cancelSignal?.reason) {
611
+ throw err;
612
+ }
613
+ });
614
+ return {
615
+ audioController: audioStreamInstance,
616
+ done: streamPromise,
617
+ };
618
+ }
@@ -0,0 +1,6 @@
1
+ import LibAV from "@lng2004/libav.js-variant-webcodecs-avf-with-decoders";
2
+
3
+ export function combineLoHi(hi: number, lo: number): number
4
+ {
5
+ return LibAV.i64tof64(lo, hi);
6
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,77 @@
1
+ import type { AnyChannel, DMChannel, GroupDMChannel, VoiceBasedChannel } from "discord.js-selfbot-v13";
2
+
3
+ export function normalizeVideoCodec(
4
+ codec: string
5
+ ): "H264" | "H265" | "VP8" | "VP9" | "AV1" {
6
+ if (/H\.?264|AVC/i.test(codec)) return "H264";
7
+ if (/H\.?265|HEVC/i.test(codec)) return "H265";
8
+ if (/VP(8|9)/i.test(codec)) return codec.toUpperCase() as "VP8" | "VP9";
9
+ if (/AV1/i.test(codec)) return "AV1";
10
+ throw new Error(`Unknown codec: ${codec}`);
11
+ }
12
+
13
+ // The available video streams are sent by client on connection to voice gateway using OpCode Identify (0)
14
+ // The server then replies with the ssrc and rtxssrc for each available stream using OpCode Ready (2)
15
+ // RID is used specifically to distinguish between different simulcast streams of the same video source,
16
+ // but we don't really care about sending multiple quality streams, so we hardcode a single one
17
+ export const STREAMS_SIMULCAST = [{ type: "screen", rid: "100", quality: 100 }];
18
+
19
+ export enum SupportedEncryptionModes {
20
+ AES256 = "aead_aes256_gcm_rtpsize",
21
+ XCHACHA20 = "aead_xchacha20_poly1305_rtpsize",
22
+ }
23
+
24
+ export type SupportedVideoCodec = "H264" | "H265" | "VP8" | "VP9" | "AV1";
25
+
26
+ // RTP extensions
27
+ export const extensions = [{ id: 5, len: 2, val: 0 }];
28
+
29
+ export const max_int16bit = 2 ** 16;
30
+ export const max_int32bit = 2 ** 32;
31
+
32
+ export function isFiniteNonZero(n: number | undefined): n is number {
33
+ return !!n && Number.isFinite(n);
34
+ }
35
+
36
+ export function parseStreamKey(
37
+ streamKey: string
38
+ ): {
39
+ type: "guild" | "call";
40
+ channelId: string;
41
+ guildId: string | null;
42
+ userId: string;
43
+ }
44
+ {
45
+ const streamKeyArray = streamKey.split(":");
46
+
47
+ const type = streamKeyArray.shift();
48
+
49
+ if (type !== "guild" && type !== "call") {
50
+ throw new Error(`Invalid stream key type: ${type}`);
51
+ }
52
+
53
+ if ((type === "guild" && streamKeyArray.length < 3) || (type === "call" && streamKey.length < 2))
54
+ throw new Error(`Invalid stream key: ${streamKey}`); // invalid stream key
55
+
56
+ let guildId: string | null = null;
57
+ if (type === "guild") {
58
+ guildId = streamKeyArray.shift() ?? null;
59
+ }
60
+ const channelId = streamKeyArray.shift();
61
+ const userId = streamKeyArray.shift();
62
+
63
+ if (!channelId || !userId) {
64
+ throw new Error(`Invalid stream key: ${streamKey}`);
65
+ }
66
+ return { type, channelId, guildId, userId };
67
+ }
68
+
69
+ export function generateStreamKey(type: "guild" | "call", guildId: string | null, channelId: string, userId: string): string {
70
+ const streamKey = `${type}${type === "guild" ? `:${guildId}` : ""}:${channelId}:${userId}`;
71
+
72
+ return streamKey;
73
+ }
74
+
75
+ export function isVoiceChannel(channel: AnyChannel): channel is DMChannel | GroupDMChannel | VoiceBasedChannel {
76
+ return (channel.type === "DM" || channel.type === "GROUP_DM" || channel.type === "GUILD_STAGE_VOICE" || channel.type === "GUILD_VOICE")
77
+ }