@m7mdxzx1/discord-video-strem 0.0.1 → 0.0.2

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.
@@ -11,6 +11,7 @@ import { isFiniteNonZero } from '../utils.js';
11
11
  import { AVCodecID } from './LibavCodecId.js';
12
12
  import { createDecoder } from './LibavDecoder.js';
13
13
  import LibAV from '@lng2004/libav.js-variant-webcodecs-avf-with-decoders';
14
+
14
15
  export function prepareStream(input, options = {}, cancelSignal) {
15
16
  cancelSignal?.throwIfAborted();
16
17
  const defaultOptions = {
@@ -34,6 +35,7 @@ export function prepareStream(input, options = {}, cancelSignal) {
34
35
  seekTime: 0,
35
36
  customFfmpegFlags: []
36
37
  };
38
+
37
39
  function mergeOptions(opts) {
38
40
  return {
39
41
  noTranscoding: opts.noTranscoding ?? defaultOptions.noTranscoding,
@@ -63,35 +65,45 @@ export function prepareStream(input, options = {}, cancelSignal) {
63
65
  customFfmpegFlags: opts.customFfmpegFlags ?? defaultOptions.customFfmpegFlags
64
66
  };
65
67
  }
68
+
66
69
  const mergedOptions = mergeOptions(options);
70
+
67
71
  let isHttpUrl = false;
68
72
  let isHls = false;
69
73
  if (typeof input === "string") {
70
74
  isHttpUrl = input.startsWith('http') || input.startsWith('https');
71
75
  isHls = input.includes('m3u');
72
76
  }
77
+
73
78
  const output = new PassThrough();
79
+
74
80
  // command creation
75
81
  const command = ffmpeg();
82
+
76
83
  // Seeking if applicable (Must be applied before input for fast seek)
77
84
  if (mergedOptions.seekTime && mergedOptions.seekTime > 0) {
78
85
  command.inputOption('-ss', String(mergedOptions.seekTime));
79
86
  }
87
+
80
88
  command.input(input)
81
89
  .addOption('-loglevel', 'info');
90
+
82
91
  // input options
83
92
  const { hardwareAcceleratedDecoding, minimizeLatency, customHeaders } = mergedOptions;
93
+
84
94
  if (hardwareAcceleratedDecoding)
85
95
  command.inputOption('-hwaccel', 'auto');
96
+
86
97
  if (minimizeLatency) {
87
98
  command.addOptions([
88
99
  '-fflags nobuffer',
89
100
  '-analyzeduration 0'
90
101
  ]);
91
102
  }
103
+
92
104
  if (isHttpUrl) {
93
- command.inputOption('-headers', Object.entries(customHeaders).map(([k, v]) => `${k}: ${v}`).join("\\r\\n") // Corrected escape sequence
94
- );
105
+ command.inputOption('-headers', Object.entries(customHeaders).map(([k, v]) => `${k}: ${v}`).join("\r\n"));
106
+
95
107
  if (!isHls) {
96
108
  command.inputOptions([
97
109
  '-reconnect 1',
@@ -101,13 +113,17 @@ export function prepareStream(input, options = {}, cancelSignal) {
101
113
  ]);
102
114
  }
103
115
  }
116
+
104
117
  // general output options
105
118
  command
106
119
  .output(output)
107
120
  .outputFormat("matroska");
121
+
108
122
  // video setup
109
123
  const { noTranscoding, width, height, frameRate, bitrateVideo, bitrateVideoMax, videoCodec, h26xPreset } = mergedOptions;
124
+
110
125
  command.addOutputOption("-map 0:v");
126
+
111
127
  if (noTranscoding) {
112
128
  command.videoCodec("copy");
113
129
  }
@@ -115,6 +131,7 @@ export function prepareStream(input, options = {}, cancelSignal) {
115
131
  command.videoFilter(`scale=${width}:${height}`);
116
132
  if (frameRate)
117
133
  command.fpsOutput(frameRate);
134
+
118
135
  command.addOutputOption([
119
136
  "-b:v", `${bitrateVideo}k`,
120
137
  "-maxrate:v", `${bitrateVideoMax}k`,
@@ -122,6 +139,7 @@ export function prepareStream(input, options = {}, cancelSignal) {
122
139
  "-pix_fmt", "yuv420p",
123
140
  "-force_key_frames", "expr:gte(t,n_forced*1)"
124
141
  ]);
142
+
125
143
  switch (videoCodec) {
126
144
  case 'AV1':
127
145
  command
@@ -141,24 +159,26 @@ export function prepareStream(input, options = {}, cancelSignal) {
141
159
  command
142
160
  .videoCodec("libx264")
143
161
  .outputOptions([
144
- '-tune zerolatency',
145
- `-preset ${h26xPreset}`,
146
- '-profile:v baseline',
147
- ]);
162
+ '-tune zerolatency',
163
+ `-preset ${h26xPreset}`,
164
+ '-profile:v baseline',
165
+ ]);
148
166
  break;
149
167
  case 'H265':
150
168
  command
151
169
  .videoCodec("libx265")
152
170
  .outputOptions([
153
- '-tune zerolatency',
154
- `-preset ${h26xPreset}`,
155
- '-profile:v main',
156
- ]);
171
+ '-tune zerolatency',
172
+ `-preset ${h26xPreset}`,
173
+ '-profile:v main',
174
+ ]);
157
175
  break;
158
176
  }
159
177
  }
178
+
160
179
  // audio setup
161
180
  const { includeAudio, bitrateAudio } = mergedOptions;
181
+
162
182
  if (includeAudio)
163
183
  command
164
184
  .addOutputOption("-map 0:a?")
@@ -173,15 +193,17 @@ export function prepareStream(input, options = {}, cancelSignal) {
173
193
  .audioCodec("libopus")
174
194
  .audioBitrate(`${bitrateAudio}k`)
175
195
  .audioFilters("volume@internal_lib=1.0");
196
+
176
197
  // Add custom ffmpeg flags
177
198
  if (mergedOptions.customFfmpegFlags && mergedOptions.customFfmpegFlags.length > 0) {
178
199
  command.addOptions(mergedOptions.customFfmpegFlags);
179
200
  }
201
+
180
202
  // exit handling
181
203
  const promise = new Promise((resolve, reject) => {
182
204
  command.on("error", (err) => {
183
205
  if (cancelSignal?.aborted)
184
- /**
206
+ /*
185
207
  * fluent-ffmpeg might throw an error when SIGTERM is sent to
186
208
  * the process, so we check if the abort signal is triggered
187
209
  * and throw that instead
@@ -190,24 +212,35 @@ export function prepareStream(input, options = {}, cancelSignal) {
190
212
  else
191
213
  reject(err);
192
214
  });
215
+
193
216
  command.on("end", () => resolve());
194
217
  });
218
+
195
219
  promise.catch(() => { });
220
+
196
221
  cancelSignal?.addEventListener("abort", () => command.kill("SIGTERM"), { once: true });
222
+
197
223
  command.run();
224
+
198
225
  return { command, output, promise };
199
226
  }
227
+
200
228
  export async function playStream(input, streamer, options = {}, cancelSignal, existingUdp = null) {
201
229
  const logger = new Log("playStream");
202
230
  cancelSignal?.throwIfAborted();
231
+
203
232
  if (!streamer.voiceConnection)
204
233
  throw new Error("Bot is not connected to a voice channel");
234
+
205
235
  logger.debug("Initializing demuxer");
206
236
  const { video, audio } = await demux(input);
207
237
  cancelSignal?.throwIfAborted();
238
+
208
239
  if (!video)
209
240
  throw new Error("No video stream in media");
241
+
210
242
  const cleanupFuncs = [];
243
+
211
244
  const videoCodecMap = {
212
245
  [AVCodecID.AV_CODEC_ID_H264]: "H264",
213
246
  [AVCodecID.AV_CODEC_ID_H265]: "H265",
@@ -215,6 +248,7 @@ export async function playStream(input, streamer, options = {}, cancelSignal, ex
215
248
  [AVCodecID.AV_CODEC_ID_VP9]: "VP9",
216
249
  [AVCodecID.AV_CODEC_ID_AV1]: "AV1"
217
250
  };
251
+
218
252
  const defaultOptions = {
219
253
  type: "go-live",
220
254
  width: video.width,
@@ -226,6 +260,7 @@ export async function playStream(input, streamer, options = {}, cancelSignal, ex
226
260
  customHeaders: {},
227
261
  initialMuted: false,
228
262
  };
263
+
229
264
  function mergeOptions(opts) {
230
265
  return {
231
266
  type: opts.type ?? defaultOptions.type,
@@ -249,21 +284,35 @@ export async function playStream(input, streamer, options = {}, cancelSignal, ex
249
284
  initialMuted: opts.initialMuted ?? defaultOptions.initialMuted,
250
285
  };
251
286
  }
287
+
252
288
  const mergedOptions = mergeOptions(options);
253
289
  logger.debug({ options: mergedOptions }, "Merged options");
290
+
254
291
  let udp;
255
292
  let stopStream;
293
+
256
294
  if (!existingUdp) {
257
- udp = await streamer.createStream();
258
- }
259
- else {
295
+ if (mergedOptions.type === "go-live") {
296
+ udp = await streamer.createStream();
297
+ stopStream = () => streamer.stopStream();
298
+ } else {
299
+ udp = streamer.voiceConnection.udp;
300
+ streamer.signalVideo(true);
301
+ stopStream = () => streamer.signalVideo(false);
302
+ }
303
+ } else {
260
304
  udp = existingUdp;
261
305
  udp.mediaConnection.setSpeaking(false);
262
306
  udp.mediaConnection.setVideoAttributes(false);
263
307
  await delay(250);
308
+ // Determine stopStream based on type
309
+ if (mergedOptions.type === "go-live") {
310
+ stopStream = () => streamer.stopStream();
311
+ } else {
312
+ stopStream = () => streamer.signalVideo(false);
313
+ }
264
314
  }
265
- // stopStream = () => streamer.stopStream();
266
- stopStream = () => console.log("Aborted");
315
+
267
316
  udp.setPacketizer(videoCodecMap[video.codec]);
268
317
  udp.mediaConnection.setSpeaking(true);
269
318
  udp.mediaConnection.setVideoAttributes(true, {
@@ -271,14 +320,16 @@ export async function playStream(input, streamer, options = {}, cancelSignal, ex
271
320
  height: mergedOptions.height,
272
321
  fps: mergedOptions.frameRate
273
322
  });
323
+
274
324
  const vStream = new VideoStream(udp);
275
325
  video.stream.pipe(vStream);
326
+
276
327
  let audioStreamInstance;
277
328
  if (audio) {
278
329
  audioStreamInstance = new AudioStream(udp, false, mergedOptions.initialMuted);
279
330
  audio.stream.pipe(audioStreamInstance);
280
331
  vStream.syncStream = audioStreamInstance;
281
- audioStreamInstance.syncStream = vStream;
332
+
282
333
  const burstTime = mergedOptions.readrateInitialBurst;
283
334
  if (typeof burstTime === "number") {
284
335
  vStream.sync = false;
@@ -295,6 +346,7 @@ export async function playStream(input, streamer, options = {}, cancelSignal, ex
295
346
  vStream.on("pts", stopBurst);
296
347
  }
297
348
  }
349
+
298
350
  if (mergedOptions.streamPreview && mergedOptions.type === "go-live") {
299
351
  (async () => {
300
352
  const logger = new Log("playStream:preview");
@@ -304,19 +356,24 @@ export async function playStream(input, streamer, options = {}, cancelSignal, ex
304
356
  logger.warn("Failed to initialize decoder. Stream preview will be disabled");
305
357
  return;
306
358
  }
359
+
307
360
  cleanupFuncs.push(() => {
308
361
  logger.debug("Freeing decoder");
309
362
  decoder.free();
310
363
  });
364
+
311
365
  const updatePreview = pDebounce.promise(async (packet) => {
312
366
  if (!(packet.flags !== undefined && packet.flags & LibAV.AV_PKT_FLAG_KEY))
313
367
  return;
368
+
314
369
  const decodeStart = performance.now();
315
370
  const [frame] = await decoder.decode([packet]).catch(() => []);
316
371
  if (!frame)
317
372
  return;
373
+
318
374
  const decodeEnd = performance.now();
319
375
  logger.debug(`Decoding a frame took ${decodeEnd - decodeStart}ms`);
376
+
320
377
  return sharp(frame.data, {
321
378
  raw: {
322
379
  width: frame.width ?? 0,
@@ -330,16 +387,19 @@ export async function playStream(input, streamer, options = {}, cancelSignal, ex
330
387
  .then(image => streamer.setStreamPreview(image))
331
388
  .catch(() => { });
332
389
  });
390
+
333
391
  video.stream.on("data", updatePreview);
334
392
  cleanupFuncs.push(() => video.stream.off("data", updatePreview));
335
393
  })();
336
394
  }
395
+
337
396
  const streamPromise = new Promise((resolve, reject) => {
338
397
  cleanupFuncs.push(() => {
339
398
  stopStream();
340
399
  udp.mediaConnection.setSpeaking(false);
341
400
  udp.mediaConnection.setVideoAttributes(false);
342
401
  });
402
+
343
403
  let cleanedUp = false;
344
404
  const cleanup = () => {
345
405
  if (cleanedUp)
@@ -348,22 +408,26 @@ export async function playStream(input, streamer, options = {}, cancelSignal, ex
348
408
  for (const f of cleanupFuncs)
349
409
  f();
350
410
  };
411
+
351
412
  cancelSignal?.addEventListener("abort", () => {
352
413
  cleanup();
353
414
  reject(cancelSignal.reason);
354
415
  }, { once: true });
416
+
355
417
  vStream.once("finish", () => {
356
418
  if (cancelSignal?.aborted)
357
419
  return;
358
420
  cleanup();
359
421
  resolve();
360
422
  });
423
+
361
424
  vStream.once("error", (err) => {
362
425
  if (cancelSignal?.aborted)
363
426
  return;
364
427
  cleanup();
365
428
  reject(err);
366
429
  });
430
+
367
431
  if (audio) {
368
432
  audio.stream.once("error", (err) => {
369
433
  if (cancelSignal?.aborted)
@@ -380,8 +444,9 @@ export async function playStream(input, streamer, options = {}, cancelSignal, ex
380
444
  throw err;
381
445
  }
382
446
  });
447
+
383
448
  return {
384
449
  audioController: audioStreamInstance,
385
450
  done: streamPromise,
386
451
  };
387
- }
452
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m7mdxzx1/discord-video-strem",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "A fork from dank to use self-bot to do video share",
5
5
  "exports": "./dist/index.js",
6
6
  "types": "dist/index.d.ts",