@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.
- package/dist/media/newApi.js +83 -18
- package/package.json +1 -1
package/dist/media/newApi.js
CHANGED
|
@@ -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("
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|