@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.
- package/README.md +294 -0
- package/dist/client/GatewayEvents.d.ts +27 -0
- package/dist/client/GatewayEvents.js +1 -0
- package/dist/client/GatewayOpCodes.d.ts +40 -0
- package/dist/client/GatewayOpCodes.js +41 -0
- package/dist/client/Streamer.d.ts +41 -0
- package/dist/client/Streamer.js +189 -0
- package/dist/client/encryptor/TransportEncryptor.d.ts +16 -0
- package/dist/client/encryptor/TransportEncryptor.js +38 -0
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.js +4 -0
- package/dist/client/packet/AudioPacketizer.d.ts +8 -0
- package/dist/client/packet/AudioPacketizer.js +22 -0
- package/dist/client/packet/BaseMediaPacketizer.d.ts +109 -0
- package/dist/client/packet/BaseMediaPacketizer.js +243 -0
- package/dist/client/packet/VideoPacketizerAnnexB.d.ts +132 -0
- package/dist/client/packet/VideoPacketizerAnnexB.js +231 -0
- package/dist/client/packet/VideoPacketizerVP8.d.ts +15 -0
- package/dist/client/packet/VideoPacketizerVP8.js +56 -0
- package/dist/client/packet/index.d.ts +4 -0
- package/dist/client/packet/index.js +4 -0
- package/dist/client/processing/AnnexBHelper.d.ts +93 -0
- package/dist/client/processing/AnnexBHelper.js +132 -0
- package/dist/client/voice/BaseMediaConnection.d.ts +118 -0
- package/dist/client/voice/BaseMediaConnection.js +319 -0
- package/dist/client/voice/MediaUdp.d.ts +26 -0
- package/dist/client/voice/MediaUdp.js +140 -0
- package/dist/client/voice/StreamConnection.d.ts +10 -0
- package/dist/client/voice/StreamConnection.js +30 -0
- package/dist/client/voice/VoiceConnection.d.ts +7 -0
- package/dist/client/voice/VoiceConnection.js +10 -0
- package/dist/client/voice/VoiceMessageTypes.d.ts +136 -0
- package/dist/client/voice/VoiceMessageTypes.js +1 -0
- package/dist/client/voice/VoiceOpCodes.d.ts +21 -0
- package/dist/client/voice/VoiceOpCodes.js +22 -0
- package/dist/client/voice/index.d.ts +5 -0
- package/dist/client/voice/index.js +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/media/AudioStream.d.ts +25 -0
- package/dist/media/AudioStream.js +63 -0
- package/dist/media/BaseMediaStream.d.ts +31 -0
- package/dist/media/BaseMediaStream.js +145 -0
- package/dist/media/LibavCodecId.d.ts +541 -0
- package/dist/media/LibavCodecId.js +552 -0
- package/dist/media/LibavDecoder.d.ts +5 -0
- package/dist/media/LibavDecoder.js +63 -0
- package/dist/media/LibavDemuxer.d.ts +23 -0
- package/dist/media/LibavDemuxer.js +295 -0
- package/dist/media/VideoStream.d.ts +7 -0
- package/dist/media/VideoStream.js +10 -0
- package/dist/media/index.d.ts +1 -0
- package/dist/media/index.js +1 -0
- package/dist/media/newApi.d.ts +126 -0
- package/dist/media/newApi.js +387 -0
- package/dist/media/utils.d.ts +1 -0
- package/dist/media/utils.js +4 -0
- package/dist/utils.d.ts +28 -0
- package/dist/utils.js +54 -0
- package/package.json +69 -0
- package/src/client/GatewayEvents.ts +41 -0
- package/src/client/GatewayOpCodes.ts +40 -0
- package/src/client/Streamer.ts +279 -0
- package/src/client/encryptor/TransportEncryptor.ts +62 -0
- package/src/client/index.ts +4 -0
- package/src/client/packet/AudioPacketizer.ts +28 -0
- package/src/client/packet/BaseMediaPacketizer.ts +307 -0
- package/src/client/packet/VideoPacketizerAnnexB.ts +263 -0
- package/src/client/packet/VideoPacketizerVP8.ts +73 -0
- package/src/client/packet/index.ts +4 -0
- package/src/client/processing/AnnexBHelper.ts +142 -0
- package/src/client/voice/BaseMediaConnection.ts +407 -0
- package/src/client/voice/MediaUdp.ts +171 -0
- package/src/client/voice/StreamConnection.ts +33 -0
- package/src/client/voice/VoiceConnection.ts +15 -0
- package/src/client/voice/VoiceMessageTypes.ts +164 -0
- package/src/client/voice/VoiceOpCodes.ts +21 -0
- package/src/client/voice/index.ts +5 -0
- package/src/index.ts +5 -0
- package/src/media/AudioStream.ts +81 -0
- package/src/media/BaseMediaStream.ts +173 -0
- package/src/media/LibavCodecId.ts +566 -0
- package/src/media/LibavDecoder.ts +82 -0
- package/src/media/LibavDemuxer.ts +348 -0
- package/src/media/VideoStream.ts +15 -0
- package/src/media/index.ts +1 -0
- package/src/media/newApi.ts +618 -0
- package/src/media/utils.ts +6 -0
- package/src/utils.ts +77 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type CodecParameters } from "@lng2004/libav.js-variant-webcodecs-avf-with-decoders";
|
|
2
|
+
import { AVCodecID } from "./LibavCodecId.js";
|
|
3
|
+
import type { Readable } from "node:stream";
|
|
4
|
+
export declare function demux(input: Readable, cancelSignal?: AbortSignal): Promise<{
|
|
5
|
+
video: {
|
|
6
|
+
stream: Readable;
|
|
7
|
+
index: number;
|
|
8
|
+
codec: AVCodecID;
|
|
9
|
+
codecpar: CodecParameters;
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
framerate_num: number;
|
|
13
|
+
framerate_den: number;
|
|
14
|
+
extradata?: unknown;
|
|
15
|
+
} | undefined;
|
|
16
|
+
audio: {
|
|
17
|
+
stream: Readable;
|
|
18
|
+
index: number;
|
|
19
|
+
codec: AVCodecID;
|
|
20
|
+
codecpar: CodecParameters;
|
|
21
|
+
sample_rate: number;
|
|
22
|
+
} | undefined;
|
|
23
|
+
}>;
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import pDebounce from "p-debounce";
|
|
2
|
+
import LibAV 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 { H264Helpers, H264NalUnitTypes, H265Helpers, H265NalUnitTypes, splitNalu, mergeNalu } from "../client/processing/AnnexBHelper.js";
|
|
7
|
+
import { PassThrough } from "node:stream";
|
|
8
|
+
const allowedVideoCodec = new Set([
|
|
9
|
+
AVCodecID.AV_CODEC_ID_H264,
|
|
10
|
+
AVCodecID.AV_CODEC_ID_H265,
|
|
11
|
+
AVCodecID.AV_CODEC_ID_VP8,
|
|
12
|
+
AVCodecID.AV_CODEC_ID_VP9,
|
|
13
|
+
AVCodecID.AV_CODEC_ID_AV1
|
|
14
|
+
]);
|
|
15
|
+
const allowedAudioCodec = new Set([
|
|
16
|
+
AVCodecID.AV_CODEC_ID_OPUS
|
|
17
|
+
]);
|
|
18
|
+
// Parse the avcC atom, which contains SPS and PPS
|
|
19
|
+
function parseavcC(input) {
|
|
20
|
+
let buf = input;
|
|
21
|
+
if (buf[0] !== 1)
|
|
22
|
+
throw new Error("Only configurationVersion 1 is supported");
|
|
23
|
+
// Skip a bunch of stuff we don't care about
|
|
24
|
+
buf = buf.subarray(5);
|
|
25
|
+
const sps = [];
|
|
26
|
+
const pps = [];
|
|
27
|
+
// Read the SPS
|
|
28
|
+
const spsCount = buf[0] & 0b11111;
|
|
29
|
+
buf = buf.subarray(1);
|
|
30
|
+
for (let i = 0; i < spsCount; ++i) {
|
|
31
|
+
const spsLength = buf.readUInt16BE();
|
|
32
|
+
buf = buf.subarray(2);
|
|
33
|
+
sps.push(buf.subarray(0, spsLength));
|
|
34
|
+
buf = buf.subarray(spsLength);
|
|
35
|
+
}
|
|
36
|
+
// Read the PPS
|
|
37
|
+
const ppsCount = buf[0];
|
|
38
|
+
buf = buf.subarray(1);
|
|
39
|
+
for (let i = 0; i < ppsCount; ++i) {
|
|
40
|
+
const ppsLength = buf.readUInt16BE();
|
|
41
|
+
buf = buf.subarray(2);
|
|
42
|
+
pps.push(buf.subarray(0, ppsLength));
|
|
43
|
+
buf = buf.subarray(ppsLength);
|
|
44
|
+
}
|
|
45
|
+
return { sps, pps };
|
|
46
|
+
}
|
|
47
|
+
// Parse the hvcC atom, which contains VPS, SPS, PPS
|
|
48
|
+
function parsehvcC(input) {
|
|
49
|
+
let buf = input;
|
|
50
|
+
if (buf[0] !== 1)
|
|
51
|
+
throw new Error("Only configurationVersion 1 is supported");
|
|
52
|
+
// Skip a bunch of stuff we don't care about
|
|
53
|
+
buf = buf.subarray(22);
|
|
54
|
+
const vps = [];
|
|
55
|
+
const sps = [];
|
|
56
|
+
const pps = [];
|
|
57
|
+
const numOfArrays = buf[0];
|
|
58
|
+
buf = buf.subarray(1);
|
|
59
|
+
for (let i = 0; i < numOfArrays; ++i) {
|
|
60
|
+
const naluType = buf[0] & 0b111111;
|
|
61
|
+
buf = buf.subarray(1);
|
|
62
|
+
const naluCount = buf.readUInt16BE();
|
|
63
|
+
buf = buf.subarray(2);
|
|
64
|
+
for (let j = 0; j < naluCount; ++j) {
|
|
65
|
+
const naluLength = buf.readUInt16BE();
|
|
66
|
+
buf = buf.subarray(2);
|
|
67
|
+
const nalu = buf.subarray(0, naluLength);
|
|
68
|
+
buf = buf.subarray(naluLength);
|
|
69
|
+
if (naluType === H265NalUnitTypes.VPS_NUT)
|
|
70
|
+
vps.push(nalu);
|
|
71
|
+
else if (naluType === H265NalUnitTypes.SPS_NUT)
|
|
72
|
+
sps.push(nalu);
|
|
73
|
+
else if (naluType === H265NalUnitTypes.PPS_NUT)
|
|
74
|
+
pps.push(nalu);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { vps, sps, pps };
|
|
78
|
+
}
|
|
79
|
+
function h264AddParamSets(frame, paramSets) {
|
|
80
|
+
const { sps, pps } = paramSets;
|
|
81
|
+
const nalus = splitNalu(frame);
|
|
82
|
+
// Technically non-IDR I frames exist ("open GOP"), but they're exceedingly
|
|
83
|
+
// rare in the wild, and no encoder produces it by default
|
|
84
|
+
let isIDR = false;
|
|
85
|
+
let hasSPS = false;
|
|
86
|
+
let hasPPS = false;
|
|
87
|
+
for (const nalu of nalus) {
|
|
88
|
+
const naluType = H264Helpers.getUnitType(nalu);
|
|
89
|
+
if (naluType === H264NalUnitTypes.CodedSliceIdr)
|
|
90
|
+
isIDR = true;
|
|
91
|
+
else if (naluType === H264NalUnitTypes.SPS)
|
|
92
|
+
hasSPS = true;
|
|
93
|
+
else if (naluType === H264NalUnitTypes.PPS)
|
|
94
|
+
hasPPS = true;
|
|
95
|
+
}
|
|
96
|
+
if (!isIDR) {
|
|
97
|
+
// Not an IDR, return as is
|
|
98
|
+
return frame;
|
|
99
|
+
}
|
|
100
|
+
const chunks = [];
|
|
101
|
+
if (!hasPPS)
|
|
102
|
+
chunks.push(...sps);
|
|
103
|
+
if (!hasSPS)
|
|
104
|
+
chunks.push(...pps);
|
|
105
|
+
return mergeNalu([...chunks, ...nalus]);
|
|
106
|
+
}
|
|
107
|
+
function h265AddParamSets(frame, paramSets) {
|
|
108
|
+
const { vps, sps, pps } = paramSets;
|
|
109
|
+
const nalus = splitNalu(frame);
|
|
110
|
+
// Technically non-IDR I frames exist ("open GOP"), but they're exceedingly
|
|
111
|
+
// rare in the wild, and no encoder produces it by default
|
|
112
|
+
let isIDR = false;
|
|
113
|
+
let hasVPS = false;
|
|
114
|
+
let hasSPS = false;
|
|
115
|
+
let hasPPS = false;
|
|
116
|
+
for (const nalu of nalus) {
|
|
117
|
+
const naluType = H265Helpers.getUnitType(nalu);
|
|
118
|
+
if (naluType === H265NalUnitTypes.IDR_N_LP || naluType === H265NalUnitTypes.IDR_W_RADL)
|
|
119
|
+
isIDR = true;
|
|
120
|
+
else if (naluType === H265NalUnitTypes.VPS_NUT)
|
|
121
|
+
hasVPS = true;
|
|
122
|
+
else if (naluType === H265NalUnitTypes.SPS_NUT)
|
|
123
|
+
hasSPS = true;
|
|
124
|
+
else if (naluType === H265NalUnitTypes.PPS_NUT)
|
|
125
|
+
hasPPS = true;
|
|
126
|
+
}
|
|
127
|
+
if (!isIDR) {
|
|
128
|
+
// Not an IDR, return as is
|
|
129
|
+
return frame;
|
|
130
|
+
}
|
|
131
|
+
const chunks = [];
|
|
132
|
+
if (!hasVPS)
|
|
133
|
+
chunks.push(...vps);
|
|
134
|
+
if (!hasPPS)
|
|
135
|
+
chunks.push(...sps);
|
|
136
|
+
if (!hasSPS)
|
|
137
|
+
chunks.push(...pps);
|
|
138
|
+
return mergeNalu([...chunks, ...nalus]);
|
|
139
|
+
}
|
|
140
|
+
const idToStream = new Map();
|
|
141
|
+
const libavInstance = LibAV.LibAV();
|
|
142
|
+
libavInstance.then((libav) => {
|
|
143
|
+
libav.onread = (id) => {
|
|
144
|
+
idToStream.get(id)?.resume();
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
export async function demux(input, cancelSignal) {
|
|
148
|
+
const loggerInput = new Log("demux:input");
|
|
149
|
+
const loggerFormat = new Log("demux:format");
|
|
150
|
+
const loggerFrameCommon = new Log("demux:frame:common");
|
|
151
|
+
const loggerFrameVideo = new Log("demux:frame:video");
|
|
152
|
+
const loggerFrameAudio = new Log("demux:frame:audio");
|
|
153
|
+
const libav = await libavInstance;
|
|
154
|
+
const filename = uid();
|
|
155
|
+
await libav.mkreaderdev(filename);
|
|
156
|
+
idToStream.set(filename, input);
|
|
157
|
+
const ondata = (chunk) => {
|
|
158
|
+
loggerInput.trace(`Received ${chunk.length} bytes of data for input ${filename}`);
|
|
159
|
+
libav.ff_reader_dev_send(filename, chunk);
|
|
160
|
+
};
|
|
161
|
+
const onend = () => {
|
|
162
|
+
loggerInput.trace(`Reached the end of input ${filename}`);
|
|
163
|
+
libav.ff_reader_dev_send(filename, null);
|
|
164
|
+
};
|
|
165
|
+
input.on("data", ondata);
|
|
166
|
+
input.on("end", onend);
|
|
167
|
+
const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(filename, "matroska");
|
|
168
|
+
const pkt = await libav.av_packet_alloc();
|
|
169
|
+
const cleanup = () => {
|
|
170
|
+
vPipe.off("drain", readFrame);
|
|
171
|
+
aPipe.off("drain", readFrame);
|
|
172
|
+
input.off("data", ondata);
|
|
173
|
+
input.off("end", onend);
|
|
174
|
+
idToStream.delete(filename);
|
|
175
|
+
libav.avformat_close_input_js(fmt_ctx);
|
|
176
|
+
libav.av_packet_free(pkt);
|
|
177
|
+
libav.unlink(filename);
|
|
178
|
+
};
|
|
179
|
+
const vStream = streams.find((stream) => stream.codec_type === libav.AVMEDIA_TYPE_VIDEO);
|
|
180
|
+
const aStream = streams.find((stream) => stream.codec_type === libav.AVMEDIA_TYPE_AUDIO);
|
|
181
|
+
let vInfo;
|
|
182
|
+
let aInfo;
|
|
183
|
+
const vPipe = new PassThrough({ objectMode: true, writableHighWaterMark: 128 });
|
|
184
|
+
const aPipe = new PassThrough({ objectMode: true, writableHighWaterMark: 128 });
|
|
185
|
+
if (vStream) {
|
|
186
|
+
if (!allowedVideoCodec.has(vStream.codec_id)) {
|
|
187
|
+
const codecName = await libav.avcodec_get_name(vStream.codec_id);
|
|
188
|
+
cleanup();
|
|
189
|
+
throw new Error(`Video codec ${codecName} is not allowed`);
|
|
190
|
+
}
|
|
191
|
+
const codecpar = await libav.ff_copyout_codecpar(vStream.codecpar);
|
|
192
|
+
vInfo = {
|
|
193
|
+
index: vStream.index,
|
|
194
|
+
codec: vStream.codec_id,
|
|
195
|
+
codecpar,
|
|
196
|
+
width: codecpar.width ?? 0,
|
|
197
|
+
height: codecpar.height ?? 0,
|
|
198
|
+
framerate_num: await libav.AVCodecParameters_framerate_num(vStream.codecpar),
|
|
199
|
+
framerate_den: await libav.AVCodecParameters_framerate_den(vStream.codecpar),
|
|
200
|
+
};
|
|
201
|
+
if (vStream.codec_id === AVCodecID.AV_CODEC_ID_H264) {
|
|
202
|
+
const { extradata } = codecpar;
|
|
203
|
+
vInfo = {
|
|
204
|
+
...vInfo,
|
|
205
|
+
// biome-ignore lint/style/noNonNullAssertion: will always be non-null for our use case
|
|
206
|
+
extradata: parseavcC(Buffer.from(extradata))
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
else if (vStream.codec_id === AVCodecID.AV_CODEC_ID_H265) {
|
|
210
|
+
const { extradata } = codecpar;
|
|
211
|
+
vInfo = {
|
|
212
|
+
...vInfo,
|
|
213
|
+
// biome-ignore lint/style/noNonNullAssertion: will always be non-null for our use case
|
|
214
|
+
extradata: parsehvcC(Buffer.from(extradata))
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
loggerFormat.info({
|
|
218
|
+
info: vInfo
|
|
219
|
+
}, `Found video stream in input ${filename}`);
|
|
220
|
+
}
|
|
221
|
+
if (aStream) {
|
|
222
|
+
if (!allowedAudioCodec.has(aStream.codec_id)) {
|
|
223
|
+
const codecName = await libav.avcodec_get_name(aStream.codec_id);
|
|
224
|
+
cleanup();
|
|
225
|
+
throw new Error(`Audio codec ${codecName} is not allowed`);
|
|
226
|
+
}
|
|
227
|
+
const codecpar = await libav.ff_copyout_codecpar(aStream.codecpar);
|
|
228
|
+
aInfo = {
|
|
229
|
+
index: aStream.index,
|
|
230
|
+
codec: aStream.codec_id,
|
|
231
|
+
codecpar,
|
|
232
|
+
sample_rate: codecpar.sample_rate ?? 0
|
|
233
|
+
};
|
|
234
|
+
loggerFormat.info({
|
|
235
|
+
info: aInfo
|
|
236
|
+
}, `Found audio stream in input ${filename}`);
|
|
237
|
+
}
|
|
238
|
+
const readFrame = pDebounce.promise(async () => {
|
|
239
|
+
let resume = true;
|
|
240
|
+
while (resume) {
|
|
241
|
+
const [status, streams] = await libav.ff_read_frame_multi(fmt_ctx, pkt, {
|
|
242
|
+
limit: 1,
|
|
243
|
+
unify: true
|
|
244
|
+
});
|
|
245
|
+
for (const packet of streams[0] ?? []) {
|
|
246
|
+
if (vInfo && vInfo.index === packet.stream_index) {
|
|
247
|
+
if (vInfo.codec === AVCodecID.AV_CODEC_ID_H264) {
|
|
248
|
+
packet.data = h264AddParamSets(Buffer.from(packet.data),
|
|
249
|
+
// biome-ignore lint/style/noNonNullAssertion: will always be non-null for our use case
|
|
250
|
+
vInfo.extradata);
|
|
251
|
+
}
|
|
252
|
+
else if (vInfo.codec === AVCodecID.AV_CODEC_ID_H265) {
|
|
253
|
+
packet.data = h265AddParamSets(Buffer.from(packet.data),
|
|
254
|
+
// biome-ignore lint/style/noNonNullAssertion: will always be non-null for our use case
|
|
255
|
+
vInfo.extradata);
|
|
256
|
+
}
|
|
257
|
+
resume &&= vPipe.write(packet);
|
|
258
|
+
loggerFrameVideo.trace("Pushed a frame into the video pipe");
|
|
259
|
+
}
|
|
260
|
+
else if (aInfo && aInfo.index === packet.stream_index) {
|
|
261
|
+
resume &&= aPipe.write(packet);
|
|
262
|
+
loggerFrameAudio.trace("Pushed a frame into the audio pipe");
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (status < 0 && status !== -libav.EAGAIN) {
|
|
266
|
+
// End of file, or some error happened
|
|
267
|
+
cleanup();
|
|
268
|
+
vPipe.end();
|
|
269
|
+
aPipe.end();
|
|
270
|
+
if (status === LibAV.AVERROR_EOF)
|
|
271
|
+
loggerFrameCommon.info("Reached end of stream. Stopping");
|
|
272
|
+
else
|
|
273
|
+
loggerFrameCommon.info({ status }, "Received an error during frame extraction. Stopping");
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (!resume) {
|
|
277
|
+
input.pause();
|
|
278
|
+
loggerInput.trace("Input stream paused");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
vPipe.on("drain", () => {
|
|
283
|
+
loggerFrameVideo.trace("Video pipe drained");
|
|
284
|
+
readFrame();
|
|
285
|
+
});
|
|
286
|
+
aPipe.on("drain", () => {
|
|
287
|
+
loggerFrameAudio.trace("Audio pipe drained");
|
|
288
|
+
readFrame();
|
|
289
|
+
});
|
|
290
|
+
readFrame();
|
|
291
|
+
return {
|
|
292
|
+
video: vInfo ? { ...vInfo, stream: vPipe } : undefined,
|
|
293
|
+
audio: aInfo ? { ...aInfo, stream: aPipe } : undefined
|
|
294
|
+
};
|
|
295
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { MediaUdp } from "../client/voice/MediaUdp.js";
|
|
2
|
+
import { BaseMediaStream } from "./BaseMediaStream.js";
|
|
3
|
+
export declare class VideoStream extends BaseMediaStream {
|
|
4
|
+
udp: MediaUdp;
|
|
5
|
+
constructor(udp: MediaUdp, noSleep?: boolean);
|
|
6
|
+
protected _sendFrame(frame: Buffer, frametime: number): Promise<void>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { BaseMediaStream } from "./BaseMediaStream.js";
|
|
2
|
+
export class VideoStream extends BaseMediaStream {
|
|
3
|
+
constructor(udp, noSleep = false) {
|
|
4
|
+
super("video", noSleep);
|
|
5
|
+
this.udp = udp;
|
|
6
|
+
}
|
|
7
|
+
async _sendFrame(frame, frametime) {
|
|
8
|
+
await this.udp.sendVideoFrame(frame, frametime);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './LibavDemuxer.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './LibavDemuxer.js';
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import ffmpeg from 'fluent-ffmpeg';
|
|
2
|
+
import { PassThrough, type Readable } from "node:stream";
|
|
3
|
+
import type { SupportedVideoCodec } from '../utils.js';
|
|
4
|
+
import type { MediaUdp, Streamer } from '../client/index.js';
|
|
5
|
+
export type EncoderOptions = {
|
|
6
|
+
/**
|
|
7
|
+
* Disable video transcoding
|
|
8
|
+
* If enabled, all video related settings have no effects, and the input
|
|
9
|
+
* video stream is used as-is.
|
|
10
|
+
*
|
|
11
|
+
* You need to ensure that the video stream has the right properties
|
|
12
|
+
* (keyframe every 1s, B-frames disabled). Failure to do so will result in
|
|
13
|
+
* a glitchy stream, or degraded performance
|
|
14
|
+
*/
|
|
15
|
+
noTranscoding: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Video width
|
|
18
|
+
*/
|
|
19
|
+
width: number;
|
|
20
|
+
/**
|
|
21
|
+
* Video height
|
|
22
|
+
*/
|
|
23
|
+
height: number;
|
|
24
|
+
/**
|
|
25
|
+
* Video frame rate
|
|
26
|
+
*/
|
|
27
|
+
frameRate?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Video codec
|
|
30
|
+
*/
|
|
31
|
+
videoCodec: SupportedVideoCodec;
|
|
32
|
+
/**
|
|
33
|
+
* Video average bitrate in kbps
|
|
34
|
+
*/
|
|
35
|
+
bitrateVideo: number;
|
|
36
|
+
/**
|
|
37
|
+
* Video max bitrate in kbps
|
|
38
|
+
*/
|
|
39
|
+
bitrateVideoMax: number;
|
|
40
|
+
/**
|
|
41
|
+
* Audio bitrate in kbps
|
|
42
|
+
*/
|
|
43
|
+
bitrateAudio: number;
|
|
44
|
+
/**
|
|
45
|
+
* Enable audio output
|
|
46
|
+
*/
|
|
47
|
+
includeAudio: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Enable hardware accelerated decoding
|
|
50
|
+
*/
|
|
51
|
+
hardwareAcceleratedDecoding: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Add some options to minimize latency
|
|
54
|
+
*/
|
|
55
|
+
minimizeLatency: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Preset for x264 and x265
|
|
58
|
+
*/
|
|
59
|
+
h26xPreset: "ultrafast" | "superfast" | "veryfast" | "faster" | "fast" | "medium" | "slow" | "slower" | "veryslow" | "placebo";
|
|
60
|
+
/**
|
|
61
|
+
* Custom headers for HTTP requests
|
|
62
|
+
*/
|
|
63
|
+
customHeaders: Record<string, string>;
|
|
64
|
+
/**
|
|
65
|
+
* Input for seeking time
|
|
66
|
+
*/
|
|
67
|
+
seekTime?: number;
|
|
68
|
+
/**
|
|
69
|
+
* Custom ffmpeg flags/options to pass directly to ffmpeg
|
|
70
|
+
* These will be added to the command after other options
|
|
71
|
+
*/
|
|
72
|
+
customFfmpegFlags: string[];
|
|
73
|
+
};
|
|
74
|
+
export interface AudioController {
|
|
75
|
+
mute(): void;
|
|
76
|
+
unmute(): void;
|
|
77
|
+
isMuted(): boolean;
|
|
78
|
+
setVolume(volume: number): void;
|
|
79
|
+
getVolume(): number;
|
|
80
|
+
}
|
|
81
|
+
export declare function prepareStream(input: string | Readable, options?: Partial<EncoderOptions>, cancelSignal?: AbortSignal): {
|
|
82
|
+
command: ffmpeg.FfmpegCommand;
|
|
83
|
+
output: PassThrough;
|
|
84
|
+
promise: Promise<void>;
|
|
85
|
+
};
|
|
86
|
+
export type PlayStreamOptions = {
|
|
87
|
+
/**
|
|
88
|
+
* Set stream type as \"Go Live\" or camera stream
|
|
89
|
+
*/
|
|
90
|
+
type: "go-live" | "camera";
|
|
91
|
+
/**
|
|
92
|
+
* Override video width sent to Discord.
|
|
93
|
+
*
|
|
94
|
+
* DO NOT SPECIFY UNLESS YOU KNOW WHAT YOU'RE DOING!
|
|
95
|
+
*/
|
|
96
|
+
width: number;
|
|
97
|
+
/**
|
|
98
|
+
* Override video height sent to Discord.
|
|
99
|
+
*
|
|
100
|
+
* DO NOT SPECIFY UNLESS YOU KNOW WHAT YOU'RE DOING!
|
|
101
|
+
*/
|
|
102
|
+
height: number;
|
|
103
|
+
/**
|
|
104
|
+
* Override video frame rate sent to Discord.
|
|
105
|
+
*
|
|
106
|
+
* DO NOT SPECIFY UNLESS YOU KNOW WHAT YOU'RE DOING!
|
|
107
|
+
*/
|
|
108
|
+
frameRate: number;
|
|
109
|
+
/**
|
|
110
|
+
* Same as ffmpeg's `readrate_initial_burst` command line flag
|
|
111
|
+
*
|
|
112
|
+
* See https://ffmpeg.org/ffmpeg.html#:~:text=%2Dreadrate_initial_burst
|
|
113
|
+
*/
|
|
114
|
+
readrateInitialBurst: number | undefined;
|
|
115
|
+
/**
|
|
116
|
+
* Enable stream preview from input stream (experimental)
|
|
117
|
+
*/
|
|
118
|
+
streamPreview: boolean;
|
|
119
|
+
seekTime?: number;
|
|
120
|
+
customHeaders?: Record<string, string>;
|
|
121
|
+
initialMuted?: boolean;
|
|
122
|
+
};
|
|
123
|
+
export declare function playStream(input: Readable, streamer: Streamer, options?: Partial<PlayStreamOptions>, cancelSignal?: AbortSignal, existingUdp?: MediaUdp | null): Promise<{
|
|
124
|
+
audioController?: AudioController;
|
|
125
|
+
done: Promise<void>;
|
|
126
|
+
}>;
|