@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,348 @@
1
+ import pDebounce from "p-debounce";
2
+ import LibAV, { type CodecParameters } from "@lng2004/libav.js-variant-webcodecs-avf-with-decoders";
3
+ import { Log } from "debug-level";
4
+ import { uid } from "uid";
5
+ import { AVCodecID } from "./LibavCodecId.js";
6
+ import {
7
+ H264Helpers, H264NalUnitTypes,
8
+ H265Helpers, H265NalUnitTypes,
9
+ splitNalu, mergeNalu
10
+ } from "../client/processing/AnnexBHelper.js";
11
+ import { PassThrough } from "node:stream";
12
+ import type { Readable } from "node:stream";
13
+
14
+ type MediaStreamInfoCommon = {
15
+ index: number,
16
+ codec: AVCodecID,
17
+ codecpar: CodecParameters,
18
+ }
19
+ type VideoStreamInfo = MediaStreamInfoCommon & {
20
+ width: number,
21
+ height: number,
22
+ framerate_num: number,
23
+ framerate_den: number,
24
+ extradata?: unknown
25
+ }
26
+ type AudioStreamInfo = MediaStreamInfoCommon & {
27
+ sample_rate: number
28
+ };
29
+
30
+ type H264ParamSets = Record<"sps" | "pps", Buffer[]>
31
+ type H265ParamSets = Record<"vps" | "sps" | "pps", Buffer[]>
32
+
33
+ const allowedVideoCodec = new Set([
34
+ AVCodecID.AV_CODEC_ID_H264,
35
+ AVCodecID.AV_CODEC_ID_H265,
36
+ AVCodecID.AV_CODEC_ID_VP8,
37
+ AVCodecID.AV_CODEC_ID_VP9,
38
+ AVCodecID.AV_CODEC_ID_AV1
39
+ ]);
40
+
41
+ const allowedAudioCodec = new Set([
42
+ AVCodecID.AV_CODEC_ID_OPUS
43
+ ]);
44
+
45
+ // Parse the avcC atom, which contains SPS and PPS
46
+ function parseavcC(input: Buffer) {
47
+ let buf = input;
48
+ if (buf[0] !== 1)
49
+ throw new Error("Only configurationVersion 1 is supported");
50
+ // Skip a bunch of stuff we don't care about
51
+ buf = buf.subarray(5);
52
+
53
+ const sps: Buffer[] = [];
54
+ const pps: Buffer[] = [];
55
+
56
+ // Read the SPS
57
+ const spsCount = buf[0] & 0b11111;
58
+ buf = buf.subarray(1);
59
+ for (let i = 0; i < spsCount; ++i) {
60
+ const spsLength = buf.readUInt16BE();
61
+ buf = buf.subarray(2);
62
+ sps.push(buf.subarray(0, spsLength));
63
+ buf = buf.subarray(spsLength);
64
+ }
65
+
66
+ // Read the PPS
67
+ const ppsCount = buf[0];
68
+ buf = buf.subarray(1);
69
+ for (let i = 0; i < ppsCount; ++i) {
70
+ const ppsLength = buf.readUInt16BE();
71
+ buf = buf.subarray(2);
72
+ pps.push(buf.subarray(0, ppsLength));
73
+ buf = buf.subarray(ppsLength);
74
+ }
75
+ return { sps, pps }
76
+ }
77
+
78
+ // Parse the hvcC atom, which contains VPS, SPS, PPS
79
+ function parsehvcC(input: Buffer) {
80
+ let buf = input;
81
+ if (buf[0] !== 1)
82
+ throw new Error("Only configurationVersion 1 is supported");
83
+ // Skip a bunch of stuff we don't care about
84
+ buf = buf.subarray(22);
85
+
86
+ const vps: Buffer[] = [];
87
+ const sps: Buffer[] = [];
88
+ const pps: Buffer[] = [];
89
+
90
+ const numOfArrays = buf[0];
91
+ buf = buf.subarray(1);
92
+
93
+ for (let i = 0; i < numOfArrays; ++i) {
94
+ const naluType = buf[0] & 0b111111;
95
+ buf = buf.subarray(1);
96
+
97
+ const naluCount = buf.readUInt16BE();
98
+ buf = buf.subarray(2);
99
+
100
+ for (let j = 0; j < naluCount; ++j) {
101
+ const naluLength = buf.readUInt16BE();
102
+ buf = buf.subarray(2);
103
+
104
+ const nalu = buf.subarray(0, naluLength);
105
+ buf = buf.subarray(naluLength);
106
+
107
+ if (naluType === H265NalUnitTypes.VPS_NUT)
108
+ vps.push(nalu);
109
+ else if (naluType === H265NalUnitTypes.SPS_NUT)
110
+ sps.push(nalu);
111
+ else if (naluType === H265NalUnitTypes.PPS_NUT)
112
+ pps.push(nalu);
113
+ }
114
+ }
115
+ return { vps, sps, pps }
116
+ }
117
+
118
+ function h264AddParamSets(frame: Buffer, paramSets: H264ParamSets) {
119
+ const { sps, pps } = paramSets;
120
+ const nalus = splitNalu(frame);
121
+ // Technically non-IDR I frames exist ("open GOP"), but they're exceedingly
122
+ // rare in the wild, and no encoder produces it by default
123
+ let isIDR = false;
124
+ let hasSPS = false;
125
+ let hasPPS = false;
126
+ for (const nalu of nalus) {
127
+ const naluType = H264Helpers.getUnitType(nalu);
128
+ if (naluType === H264NalUnitTypes.CodedSliceIdr)
129
+ isIDR = true;
130
+ else if (naluType === H264NalUnitTypes.SPS)
131
+ hasSPS = true;
132
+ else if (naluType === H264NalUnitTypes.PPS)
133
+ hasPPS = true;
134
+ }
135
+ if (!isIDR) {
136
+ // Not an IDR, return as is
137
+ return frame;
138
+ }
139
+ const chunks = [];
140
+ if (!hasPPS)
141
+ chunks.push(...sps);
142
+ if (!hasSPS)
143
+ chunks.push(...pps);
144
+ return mergeNalu([...chunks, ...nalus]);
145
+ }
146
+
147
+ function h265AddParamSets(frame: Buffer, paramSets: H265ParamSets) {
148
+ const { vps, sps, pps } = paramSets;
149
+ const nalus = splitNalu(frame);
150
+ // Technically non-IDR I frames exist ("open GOP"), but they're exceedingly
151
+ // rare in the wild, and no encoder produces it by default
152
+ let isIDR = false;
153
+ let hasVPS = false;
154
+ let hasSPS = false;
155
+ let hasPPS = false;
156
+ for (const nalu of nalus) {
157
+ const naluType = H265Helpers.getUnitType(nalu);
158
+ if (naluType === H265NalUnitTypes.IDR_N_LP || naluType === H265NalUnitTypes.IDR_W_RADL)
159
+ isIDR = true;
160
+ else if (naluType === H265NalUnitTypes.VPS_NUT)
161
+ hasVPS = true;
162
+ else if (naluType === H265NalUnitTypes.SPS_NUT)
163
+ hasSPS = true;
164
+ else if (naluType === H265NalUnitTypes.PPS_NUT)
165
+ hasPPS = true;
166
+ }
167
+ if (!isIDR) {
168
+ // Not an IDR, return as is
169
+ return frame;
170
+ }
171
+ const chunks = [];
172
+ if (!hasVPS)
173
+ chunks.push(...vps);
174
+ if (!hasPPS)
175
+ chunks.push(...sps);
176
+ if (!hasSPS)
177
+ chunks.push(...pps);
178
+ return mergeNalu([...chunks, ...nalus]);
179
+ }
180
+
181
+ const idToStream = new Map<string, Readable>();
182
+ const libavInstance = LibAV.LibAV();
183
+ libavInstance.then((libav) => {
184
+ libav.onread = (id) => {
185
+ idToStream.get(id)?.resume();
186
+ }
187
+ })
188
+
189
+ export async function demux(input: Readable, cancelSignal?: AbortSignal) {
190
+ const loggerInput = new Log("demux:input");
191
+ const loggerFormat = new Log("demux:format");
192
+ const loggerFrameCommon = new Log("demux:frame:common");
193
+ const loggerFrameVideo = new Log("demux:frame:video");
194
+ const loggerFrameAudio = new Log("demux:frame:audio");
195
+
196
+ const libav = await libavInstance;
197
+ const filename = uid();
198
+ await libav.mkreaderdev(filename);
199
+ idToStream.set(filename, input);
200
+
201
+ const ondata = (chunk: Buffer) => {
202
+ loggerInput.trace(`Received ${chunk.length} bytes of data for input ${filename}`);
203
+ libav.ff_reader_dev_send(filename, chunk)
204
+ };
205
+ const onend = () => {
206
+ loggerInput.trace(`Reached the end of input ${filename}`);
207
+ libav.ff_reader_dev_send(filename, null);
208
+ }
209
+ input.on("data", ondata);
210
+ input.on("end", onend);
211
+
212
+ const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(filename, "matroska");
213
+ const pkt = await libav.av_packet_alloc();
214
+
215
+ const cleanup = () => {
216
+ vPipe.off("drain", readFrame);
217
+ aPipe.off("drain", readFrame);
218
+ input.off("data", ondata);
219
+ input.off("end", onend);
220
+ idToStream.delete(filename);
221
+ libav.avformat_close_input_js(fmt_ctx);
222
+ libav.av_packet_free(pkt);
223
+ libav.unlink(filename);
224
+ }
225
+
226
+ const vStream = streams.find((stream) => stream.codec_type === libav.AVMEDIA_TYPE_VIDEO)
227
+ const aStream = streams.find((stream) => stream.codec_type === libav.AVMEDIA_TYPE_AUDIO)
228
+ let vInfo: VideoStreamInfo | undefined
229
+ let aInfo: AudioStreamInfo | undefined;
230
+ const vPipe = new PassThrough({ objectMode: true, writableHighWaterMark: 128 });
231
+ const aPipe = new PassThrough({ objectMode: true, writableHighWaterMark: 128 });
232
+
233
+ if (vStream) {
234
+ if (!allowedVideoCodec.has(vStream.codec_id)) {
235
+ const codecName = await libav.avcodec_get_name(vStream.codec_id);
236
+ cleanup();
237
+ throw new Error(`Video codec ${codecName} is not allowed`)
238
+ }
239
+ const codecpar = await libav.ff_copyout_codecpar(vStream.codecpar);
240
+ vInfo = {
241
+ index: vStream.index,
242
+ codec: vStream.codec_id,
243
+ codecpar,
244
+ width: codecpar.width ?? 0,
245
+ height: codecpar.height ?? 0,
246
+ framerate_num: await libav.AVCodecParameters_framerate_num(vStream.codecpar),
247
+ framerate_den: await libav.AVCodecParameters_framerate_den(vStream.codecpar),
248
+ }
249
+ if (vStream.codec_id === AVCodecID.AV_CODEC_ID_H264) {
250
+ const { extradata } = codecpar;
251
+ vInfo = {
252
+ ...vInfo,
253
+ // biome-ignore lint/style/noNonNullAssertion: will always be non-null for our use case
254
+ extradata: parseavcC(Buffer.from(extradata!))
255
+ }
256
+ }
257
+ else if (vStream.codec_id === AVCodecID.AV_CODEC_ID_H265) {
258
+ const { extradata } = codecpar;
259
+ vInfo = {
260
+ ...vInfo,
261
+ // biome-ignore lint/style/noNonNullAssertion: will always be non-null for our use case
262
+ extradata: parsehvcC(Buffer.from(extradata!))
263
+ }
264
+ }
265
+ loggerFormat.info({
266
+ info: vInfo
267
+ }, `Found video stream in input ${filename}`)
268
+ }
269
+ if (aStream) {
270
+ if (!allowedAudioCodec.has(aStream.codec_id)) {
271
+ const codecName = await libav.avcodec_get_name(aStream.codec_id);
272
+ cleanup();
273
+ throw new Error(`Audio codec ${codecName} is not allowed`);
274
+ }
275
+ const codecpar = await libav.ff_copyout_codecpar(aStream.codecpar);
276
+ aInfo = {
277
+ index: aStream.index,
278
+ codec: aStream.codec_id,
279
+ codecpar,
280
+ sample_rate: codecpar.sample_rate ?? 0
281
+ }
282
+ loggerFormat.info({
283
+ info: aInfo
284
+ }, `Found audio stream in input ${filename}`)
285
+ }
286
+
287
+ const readFrame = pDebounce.promise(async () => {
288
+ let resume = true;
289
+ while (resume) {
290
+ const [status, streams] = await libav.ff_read_frame_multi(fmt_ctx, pkt, {
291
+ limit: 1,
292
+ unify: true
293
+ });
294
+ for (const packet of streams[0] ?? []) {
295
+ if (vInfo && vInfo.index === packet.stream_index) {
296
+ if (vInfo.codec === AVCodecID.AV_CODEC_ID_H264) {
297
+ packet.data = h264AddParamSets(
298
+ Buffer.from(packet.data),
299
+ // biome-ignore lint/style/noNonNullAssertion: will always be non-null for our use case
300
+ vInfo.extradata! as H264ParamSets
301
+ );
302
+ }
303
+ else if (vInfo.codec === AVCodecID.AV_CODEC_ID_H265) {
304
+ packet.data = h265AddParamSets(
305
+ Buffer.from(packet.data),
306
+ // biome-ignore lint/style/noNonNullAssertion: will always be non-null for our use case
307
+ vInfo.extradata! as H265ParamSets
308
+ );
309
+ }
310
+ resume &&= vPipe.write(packet);
311
+ loggerFrameVideo.trace("Pushed a frame into the video pipe");
312
+ }
313
+ else if (aInfo && aInfo.index === packet.stream_index) {
314
+ resume &&= aPipe.write(packet);
315
+ loggerFrameAudio.trace("Pushed a frame into the audio pipe");
316
+ }
317
+ }
318
+ if (status < 0 && status !== -libav.EAGAIN) {
319
+ // End of file, or some error happened
320
+ cleanup();
321
+ vPipe.end();
322
+ aPipe.end();
323
+ if (status === LibAV.AVERROR_EOF)
324
+ loggerFrameCommon.info("Reached end of stream. Stopping");
325
+ else
326
+ loggerFrameCommon.info({ status }, "Received an error during frame extraction. Stopping");
327
+ return;
328
+ }
329
+ if (!resume) {
330
+ input.pause();
331
+ loggerInput.trace("Input stream paused");
332
+ }
333
+ }
334
+ });
335
+ vPipe.on("drain", () => {
336
+ loggerFrameVideo.trace("Video pipe drained");
337
+ readFrame();
338
+ });
339
+ aPipe.on("drain", () => {
340
+ loggerFrameAudio.trace("Audio pipe drained");
341
+ readFrame();
342
+ });
343
+ readFrame();
344
+ return {
345
+ video: vInfo ? { ...vInfo, stream: vPipe as Readable } : undefined,
346
+ audio: aInfo ? { ...aInfo, stream: aPipe as Readable } : undefined
347
+ }
348
+ }
@@ -0,0 +1,15 @@
1
+ import type { MediaUdp } from "../client/voice/MediaUdp.js";
2
+ import { BaseMediaStream } from "./BaseMediaStream.js";
3
+
4
+ export class VideoStream extends BaseMediaStream {
5
+ public udp: MediaUdp;
6
+
7
+ constructor(udp: MediaUdp, noSleep = false) {
8
+ super("video", noSleep);
9
+ this.udp = udp;
10
+ }
11
+
12
+ protected override async _sendFrame(frame: Buffer, frametime: number): Promise<void> {
13
+ await this.udp.sendVideoFrame(frame, frametime);
14
+ }
15
+ }
@@ -0,0 +1 @@
1
+ export * from './LibavDemuxer.js';