@mediabunny/server 1.45.0
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/LICENSE +373 -0
- package/README.md +276 -0
- package/dist/bundles/mediabunny-server.cjs +3361 -0
- package/dist/bundles/mediabunny-server.min.cjs +10 -0
- package/dist/bundles/mediabunny-server.min.mjs +9 -0
- package/dist/bundles/mediabunny-server.mjs +3334 -0
- package/dist/mediabunny-server.d.ts +105 -0
- package/dist/modules/src/audio-decoder.d.ts +21 -0
- package/dist/modules/src/audio-decoder.d.ts.map +1 -0
- package/dist/modules/src/audio-decoder.js +132 -0
- package/dist/modules/src/audio-encoder.d.ts +35 -0
- package/dist/modules/src/audio-encoder.d.ts.map +1 -0
- package/dist/modules/src/audio-encoder.js +329 -0
- package/dist/modules/src/audio-sample.d.ts +38 -0
- package/dist/modules/src/audio-sample.d.ts.map +1 -0
- package/dist/modules/src/audio-sample.js +119 -0
- package/dist/modules/src/index.d.ts +39 -0
- package/dist/modules/src/index.d.ts.map +1 -0
- package/dist/modules/src/index.js +132 -0
- package/dist/modules/src/misc.d.ts +26 -0
- package/dist/modules/src/misc.d.ts.map +1 -0
- package/dist/modules/src/misc.js +255 -0
- package/dist/modules/src/video-decoder.d.ts +30 -0
- package/dist/modules/src/video-decoder.d.ts.map +1 -0
- package/dist/modules/src/video-decoder.js +214 -0
- package/dist/modules/src/video-encoder.d.ts +35 -0
- package/dist/modules/src/video-encoder.d.ts.map +1 -0
- package/dist/modules/src/video-encoder.js +474 -0
- package/dist/modules/src/video-sample.d.ts +45 -0
- package/dist/modules/src/video-sample.d.ts.map +1 -0
- package/dist/modules/src/video-sample.js +276 -0
- package/dist/modules/tsconfig.tsbuildinfo +1 -0
- package/package.json +59 -0
- package/src/audio-decoder.ts +120 -0
- package/src/audio-encoder.ts +396 -0
- package/src/audio-sample.ts +101 -0
- package/src/index.ts +104 -0
- package/src/misc.ts +242 -0
- package/src/video-decoder.ts +224 -0
- package/src/video-encoder.ts +568 -0
- package/src/video-sample.ts +313 -0
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026-present, Vanilagy and contributors
|
|
3
|
+
*
|
|
4
|
+
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
CustomVideoEncoder,
|
|
11
|
+
type MaybePromise,
|
|
12
|
+
QUALITY_MEDIUM,
|
|
13
|
+
VideoCodec,
|
|
14
|
+
VideoSample,
|
|
15
|
+
EncodedPacket,
|
|
16
|
+
EncodedPacketSideData,
|
|
17
|
+
} from 'mediabunny';
|
|
18
|
+
import * as NodeAv from 'node-av';
|
|
19
|
+
import {
|
|
20
|
+
CODEC_TO_CODEC_ID,
|
|
21
|
+
getHardwareEncoderCodec,
|
|
22
|
+
LIBVPX_VP9,
|
|
23
|
+
unmapColorPrimaries,
|
|
24
|
+
unmapMatrixCoefficients,
|
|
25
|
+
unmapTransferCharacteristics,
|
|
26
|
+
} from './misc';
|
|
27
|
+
import { copyVideoSampleToAvFrame, AvFrameVideoSampleResource } from './video-sample';
|
|
28
|
+
import {
|
|
29
|
+
AvcNalUnitType,
|
|
30
|
+
extractAv1CodecInfoFromPacket,
|
|
31
|
+
extractAvcDecoderConfigurationRecord,
|
|
32
|
+
extractHevcDecoderConfigurationRecord,
|
|
33
|
+
extractNalUnitTypeForAvc,
|
|
34
|
+
extractNalUnitTypeForHevc,
|
|
35
|
+
extractVp9CodecInfoFromPacket,
|
|
36
|
+
HevcNalUnitType,
|
|
37
|
+
iterateNalUnitsInAnnexB,
|
|
38
|
+
NalUnitLocation,
|
|
39
|
+
serializeAvcDecoderConfigurationRecord,
|
|
40
|
+
serializeHevcDecoderConfigurationRecord,
|
|
41
|
+
} from '../../../src/codec-data';
|
|
42
|
+
import { extractVideoCodecString } from '../../../src/codec';
|
|
43
|
+
import { assert, binarySearchLessOrEqual, simplifyRational, toUint8Array } from '../../../src/misc';
|
|
44
|
+
|
|
45
|
+
export class NodeAvVideoEncoder extends CustomVideoEncoder {
|
|
46
|
+
frame!: NodeAv.Frame;
|
|
47
|
+
packet!: NodeAv.Packet;
|
|
48
|
+
avCodec!: NodeAv.Codec;
|
|
49
|
+
codecContext: NodeAv.CodecContext | null = null;
|
|
50
|
+
lastBuffer: Buffer | null = null;
|
|
51
|
+
packetEmitted = false;
|
|
52
|
+
|
|
53
|
+
scaler: NodeAv.SoftwareScaleContext | null = null;
|
|
54
|
+
lastScalerKey: string | null = null;
|
|
55
|
+
dstFrame: NodeAv.Frame | null = null;
|
|
56
|
+
|
|
57
|
+
// Bookkeeping to restore the original timing information
|
|
58
|
+
preciseTimings: {
|
|
59
|
+
microsecondTimestamp: number;
|
|
60
|
+
timestamp: number;
|
|
61
|
+
duration: number;
|
|
62
|
+
timestampIsValid: boolean;
|
|
63
|
+
durationIsValid: boolean;
|
|
64
|
+
}[] = [];
|
|
65
|
+
|
|
66
|
+
static override supports(codec: VideoCodec, config: VideoEncoderConfig): boolean {
|
|
67
|
+
return (codec === 'avc' || codec === 'hevc' || codec === 'vp8' || codec === 'vp9' || codec === 'av1')
|
|
68
|
+
&& config.bitrateMode !== 'quantizer';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async init(): Promise<void> {
|
|
72
|
+
this.frame = new NodeAv.Frame();
|
|
73
|
+
this.frame.alloc();
|
|
74
|
+
this.frame.timeBase = new NodeAv.Rational(1, 1e6);
|
|
75
|
+
|
|
76
|
+
this.packet = new NodeAv.Packet();
|
|
77
|
+
this.packet.alloc();
|
|
78
|
+
|
|
79
|
+
const codecId = CODEC_TO_CODEC_ID[this.codec];
|
|
80
|
+
assert(codecId !== undefined);
|
|
81
|
+
|
|
82
|
+
let codec: NodeAv.Codec | null = null;
|
|
83
|
+
if (this.codec === 'vp9' && this.config.alpha === 'keep') {
|
|
84
|
+
codec = NodeAv.Codec.findEncoderByName(LIBVPX_VP9) ?? NodeAv.Codec.findEncoder(codecId);
|
|
85
|
+
} else if (this.config.hardwareAcceleration === 'prefer-software') {
|
|
86
|
+
codec = NodeAv.Codec.findEncoder(codecId);
|
|
87
|
+
} else {
|
|
88
|
+
codec = getHardwareEncoderCodec(codecId) ?? NodeAv.Codec.findEncoder(codecId);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!codec) {
|
|
92
|
+
throw new Error(`Unable to obtain libav codec for '${this.codec}'.`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.avCodec = codec;
|
|
96
|
+
|
|
97
|
+
await this.createCodecContext();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async createCodecContext() {
|
|
101
|
+
assert(this.codecContext === null);
|
|
102
|
+
|
|
103
|
+
const codecContext = new NodeAv.CodecContext();
|
|
104
|
+
codecContext.allocContext3(this.avCodec);
|
|
105
|
+
|
|
106
|
+
let pixelFormat = NodeAv.AV_PIX_FMT_YUV420P;
|
|
107
|
+
|
|
108
|
+
if (this.avCodec.pixelFormats) {
|
|
109
|
+
if (!this.avCodec.pixelFormats.includes(NodeAv.AV_PIX_FMT_YUV420P)) {
|
|
110
|
+
pixelFormat = this.avCodec.pixelFormats[0]!;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (this.config.alpha === 'keep' && this.avCodec.pixelFormats.includes(NodeAv.AV_PIX_FMT_YUVA420P)) {
|
|
114
|
+
pixelFormat = NodeAv.AV_PIX_FMT_YUVA420P;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const pixelAspectRatio = simplifyRational({
|
|
119
|
+
num: (this.config.displayWidth ?? this.config.width) * this.config.height,
|
|
120
|
+
den: (this.config.displayHeight ?? this.config.height) * this.config.width,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
codecContext.width = this.config.width;
|
|
124
|
+
codecContext.height = this.config.height;
|
|
125
|
+
codecContext.pixelFormat = pixelFormat;
|
|
126
|
+
codecContext.timeBase = new NodeAv.Rational(1, 1e6);
|
|
127
|
+
codecContext.gopSize = 60;
|
|
128
|
+
codecContext.framerate = new NodeAv.Rational(Math.round(this.config.framerate ?? 0) || 30, 1);
|
|
129
|
+
codecContext.bitRate = BigInt(
|
|
130
|
+
this.config.bitrate ?? QUALITY_MEDIUM._toVideoBitrate(this.codec, this.config.width, this.config.height),
|
|
131
|
+
);
|
|
132
|
+
codecContext.sampleAspectRatio = new NodeAv.Rational(pixelAspectRatio.num, pixelAspectRatio.den);
|
|
133
|
+
|
|
134
|
+
if (this.config.bitrateMode === 'constant') {
|
|
135
|
+
codecContext.rcMinRate = codecContext.bitRate;
|
|
136
|
+
codecContext.rcMaxRate = codecContext.bitRate;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const isRealtime = this.config.latencyMode === 'realtime';
|
|
140
|
+
|
|
141
|
+
if (this.avCodec.name === 'libx264') {
|
|
142
|
+
if (isRealtime) {
|
|
143
|
+
codecContext.setOption('tune', 'zerolatency');
|
|
144
|
+
codecContext.setOption('preset', 'ultrafast');
|
|
145
|
+
}
|
|
146
|
+
} else if (this.avCodec.name === 'libx265') {
|
|
147
|
+
codecContext.setOption('x265-params', 'log-level=error');
|
|
148
|
+
|
|
149
|
+
if (isRealtime) {
|
|
150
|
+
codecContext.setOption('tune', 'zerolatency');
|
|
151
|
+
codecContext.setOption('preset', 'ultrafast');
|
|
152
|
+
}
|
|
153
|
+
} else if (this.avCodec.name === 'libvpx') {
|
|
154
|
+
if (isRealtime) {
|
|
155
|
+
codecContext.setOption('deadline', 'realtime');
|
|
156
|
+
codecContext.setOption('cpu-used', '8');
|
|
157
|
+
} else {
|
|
158
|
+
codecContext.setOption('cpu-used', '8');
|
|
159
|
+
}
|
|
160
|
+
} else if (this.avCodec.name === 'libvpx-vp9') {
|
|
161
|
+
codecContext.setOption('deadline', 'realtime');
|
|
162
|
+
|
|
163
|
+
if (isRealtime) {
|
|
164
|
+
codecContext.setOption('cpu-used', '8');
|
|
165
|
+
} else {
|
|
166
|
+
codecContext.setOption('cpu-used', '5');
|
|
167
|
+
}
|
|
168
|
+
} else if (this.avCodec.name === 'libsvtav1') {
|
|
169
|
+
// SVTAV1 can be silenced by setting an environment variable:
|
|
170
|
+
// https://superuser.com/questions/1775236/how-to-remove-svt-av1-information-from-ffmpeg-output
|
|
171
|
+
process.env['SVT_LOG'] = '1';
|
|
172
|
+
|
|
173
|
+
if (isRealtime) {
|
|
174
|
+
codecContext.setOption('preset', '12');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const ret = await codecContext.open2();
|
|
179
|
+
NodeAv.FFmpegError.throwIfError(ret, 'Open codec context');
|
|
180
|
+
|
|
181
|
+
this.codecContext = codecContext;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async encode(videoSample: VideoSample, options: VideoEncoderEncodeOptions): Promise<void> {
|
|
185
|
+
if (this.codecContext === null) {
|
|
186
|
+
await this.createCodecContext();
|
|
187
|
+
assert(this.codecContext);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (videoSample._data instanceof AvFrameVideoSampleResource) {
|
|
191
|
+
this.frame.ref(videoSample._data.frame);
|
|
192
|
+
} else {
|
|
193
|
+
if (videoSample.format === null) {
|
|
194
|
+
throw new Error('Cannot encode foreign VideoSample with unknown (null) format.');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
this.lastBuffer = await copyVideoSampleToAvFrame(videoSample, this.frame, this.lastBuffer);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let frameToEncode = this.frame;
|
|
201
|
+
|
|
202
|
+
const requiresScaler
|
|
203
|
+
= this.codecContext.pixelFormat !== this.frame.format
|
|
204
|
+
|| this.codecContext.width !== this.frame.width
|
|
205
|
+
|| this.codecContext.height !== this.frame.height;
|
|
206
|
+
|
|
207
|
+
if (requiresScaler) {
|
|
208
|
+
if (!this.scaler) {
|
|
209
|
+
this.scaler = new NodeAv.SoftwareScaleContext();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const key = `${this.frame.width}x${this.frame.height}:${this.frame.format}`;
|
|
213
|
+
const needsConfigure = key !== this.lastScalerKey;
|
|
214
|
+
|
|
215
|
+
if (needsConfigure) {
|
|
216
|
+
this.scaler.getContext(
|
|
217
|
+
this.frame.width, this.frame.height, this.frame.format as NodeAv.AVPixelFormat,
|
|
218
|
+
this.codecContext.width, this.codecContext.height, this.codecContext.pixelFormat,
|
|
219
|
+
NodeAv.SWS_FAST_BILINEAR,
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
this.lastScalerKey = key;
|
|
223
|
+
|
|
224
|
+
const ret = this.scaler.initContext();
|
|
225
|
+
NodeAv.FFmpegError.throwIfError(ret, 'initContext');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!this.dstFrame) {
|
|
229
|
+
this.dstFrame = new NodeAv.Frame();
|
|
230
|
+
this.dstFrame.alloc();
|
|
231
|
+
this.dstFrame.width = this.codecContext.width;
|
|
232
|
+
this.dstFrame.height = this.codecContext.height;
|
|
233
|
+
this.dstFrame.format = this.codecContext.pixelFormat;
|
|
234
|
+
this.dstFrame.allocBuffer();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
await this.scaler.scaleFrame(this.dstFrame, this.frame);
|
|
238
|
+
this.dstFrame.copyProps(this.frame);
|
|
239
|
+
frameToEncode = this.dstFrame;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
frameToEncode.pts = BigInt(videoSample.microsecondTimestamp);
|
|
243
|
+
frameToEncode.duration = BigInt(videoSample.microsecondDuration);
|
|
244
|
+
frameToEncode.timeBase = new NodeAv.Rational(1, 1e6);
|
|
245
|
+
|
|
246
|
+
// Let's just set both for good measure
|
|
247
|
+
frameToEncode.pictType = options?.keyFrame
|
|
248
|
+
? NodeAv.AV_PICTURE_TYPE_I
|
|
249
|
+
: NodeAv.AV_PICTURE_TYPE_NONE;
|
|
250
|
+
frameToEncode.keyFrame = options?.keyFrame
|
|
251
|
+
? 1
|
|
252
|
+
: 0;
|
|
253
|
+
|
|
254
|
+
const preciseTimingIndex = binarySearchLessOrEqual(
|
|
255
|
+
this.preciseTimings,
|
|
256
|
+
videoSample.microsecondTimestamp,
|
|
257
|
+
x => x.microsecondTimestamp,
|
|
258
|
+
);
|
|
259
|
+
const existingEntry = preciseTimingIndex !== -1
|
|
260
|
+
? this.preciseTimings[preciseTimingIndex]
|
|
261
|
+
: null;
|
|
262
|
+
if (existingEntry && existingEntry.microsecondTimestamp === videoSample.microsecondTimestamp) {
|
|
263
|
+
if (existingEntry.timestamp !== videoSample.timestamp) {
|
|
264
|
+
// Mapping isn't unique, can't use the timestamp
|
|
265
|
+
existingEntry.timestampIsValid = false;
|
|
266
|
+
}
|
|
267
|
+
if (existingEntry.duration !== videoSample.duration) {
|
|
268
|
+
// Mapping isn't unique, can't use the duration
|
|
269
|
+
existingEntry.durationIsValid = false;
|
|
270
|
+
}
|
|
271
|
+
} else {
|
|
272
|
+
this.preciseTimings.splice(preciseTimingIndex + 1, 0, {
|
|
273
|
+
microsecondTimestamp: videoSample.microsecondTimestamp,
|
|
274
|
+
timestamp: videoSample.timestamp,
|
|
275
|
+
duration: videoSample.duration,
|
|
276
|
+
timestampIsValid: true,
|
|
277
|
+
durationIsValid: true,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// Make sure it doesn't grow indefinitely
|
|
281
|
+
if (this.preciseTimings.length > 128) {
|
|
282
|
+
this.preciseTimings.shift();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const ret = await this.codecContext.sendFrame(frameToEncode);
|
|
287
|
+
NodeAv.FFmpegError.throwIfError(ret, 'Send frame');
|
|
288
|
+
|
|
289
|
+
// Keep receiving packets until no more are available for this frame
|
|
290
|
+
while (true) {
|
|
291
|
+
const receiveRet = await this.codecContext.receivePacket(this.packet);
|
|
292
|
+
if (receiveRet === NodeAv.AVERROR_EAGAIN || receiveRet === NodeAv.AVERROR_EOF) {
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
this.receivePacket(receiveRet);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
receivePacket(ret: number) {
|
|
301
|
+
assert(this.codecContext);
|
|
302
|
+
NodeAv.FFmpegError.throwIfError(ret, 'Receive packet');
|
|
303
|
+
|
|
304
|
+
if (!this.packet.data) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
let packetData = toUint8Array(this.packet.data);
|
|
308
|
+
|
|
309
|
+
let timestamp = Number(this.packet.pts) / 1e6;
|
|
310
|
+
let duration = Number(this.packet.duration) / 1e6;
|
|
311
|
+
|
|
312
|
+
const preciseTimingIndex = binarySearchLessOrEqual(
|
|
313
|
+
this.preciseTimings,
|
|
314
|
+
Number(this.packet.pts),
|
|
315
|
+
x => x.microsecondTimestamp,
|
|
316
|
+
);
|
|
317
|
+
const entry = preciseTimingIndex !== -1
|
|
318
|
+
? this.preciseTimings[preciseTimingIndex]
|
|
319
|
+
: null;
|
|
320
|
+
|
|
321
|
+
// If there's a relevant timing entry, refine the packet's timing data to get better accuracy than
|
|
322
|
+
// microseconds
|
|
323
|
+
if (entry && entry.microsecondTimestamp === Number(this.packet.pts)) {
|
|
324
|
+
if (entry.timestampIsValid) {
|
|
325
|
+
timestamp = entry.timestamp;
|
|
326
|
+
}
|
|
327
|
+
if (entry.durationIsValid) {
|
|
328
|
+
duration = entry.duration;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const metadata: EncodedVideoChunkMetadata = {};
|
|
333
|
+
let decoderConfigCodecString: string | null = null;
|
|
334
|
+
let decoderConfigDescription: Uint8Array | null = null;
|
|
335
|
+
|
|
336
|
+
if (this.codec === 'avc' || this.codec === 'hevc') {
|
|
337
|
+
let expectsAnnexB = false;
|
|
338
|
+
if (this.codec === 'avc') {
|
|
339
|
+
expectsAnnexB = this.config.avc?.format === 'annexb';
|
|
340
|
+
} else {
|
|
341
|
+
// eslint-disable-next-line @stylistic/max-len
|
|
342
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
|
343
|
+
expectsAnnexB = (this.config as any).hevc?.format === 'annexb';
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (!this.packetEmitted) {
|
|
347
|
+
let serializedRecord: Uint8Array;
|
|
348
|
+
|
|
349
|
+
if (this.codec === 'avc') {
|
|
350
|
+
const record = extractAvcDecoderConfigurationRecord(this.packet.data);
|
|
351
|
+
if (!record) {
|
|
352
|
+
throw new Error('Invalid AVC data, could not extract decoder configuration record.');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
serializedRecord = serializeAvcDecoderConfigurationRecord(record);
|
|
356
|
+
} else {
|
|
357
|
+
const record = extractHevcDecoderConfigurationRecord(this.packet.data);
|
|
358
|
+
if (!record) {
|
|
359
|
+
throw new Error('Invalid HEVC data, could not extract decoder configuration record.');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
serializedRecord = serializeHevcDecoderConfigurationRecord(record);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
decoderConfigCodecString = extractVideoCodecString({
|
|
366
|
+
width: this.config.width,
|
|
367
|
+
height: this.config.height,
|
|
368
|
+
codec: this.codec,
|
|
369
|
+
codecDescription: serializedRecord,
|
|
370
|
+
colorSpace: null,
|
|
371
|
+
avcType: 1,
|
|
372
|
+
avcCodecInfo: null,
|
|
373
|
+
hevcCodecInfo: null,
|
|
374
|
+
vp9CodecInfo: null,
|
|
375
|
+
av1CodecInfo: null,
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
if (!expectsAnnexB) {
|
|
379
|
+
decoderConfigDescription = serializedRecord;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (!expectsAnnexB) {
|
|
384
|
+
const NAL_UNIT_LENGTH_SIZE = 4;
|
|
385
|
+
|
|
386
|
+
const nalUnits: NalUnitLocation[] = [];
|
|
387
|
+
for (const loc of iterateNalUnitsInAnnexB(packetData)) {
|
|
388
|
+
if (this.codec === 'avc') {
|
|
389
|
+
const naluType = extractNalUnitTypeForAvc(packetData[loc.offset]!);
|
|
390
|
+
|
|
391
|
+
// Certain NALUs get stripped
|
|
392
|
+
if (
|
|
393
|
+
naluType !== AvcNalUnitType.SPS
|
|
394
|
+
&& naluType !== AvcNalUnitType.PPS
|
|
395
|
+
&& naluType !== AvcNalUnitType.SPS_EXT
|
|
396
|
+
) {
|
|
397
|
+
nalUnits.push(loc);
|
|
398
|
+
}
|
|
399
|
+
} else {
|
|
400
|
+
const naluType = extractNalUnitTypeForHevc(packetData[loc.offset]!);
|
|
401
|
+
|
|
402
|
+
// Certain NALUs get stripped
|
|
403
|
+
if (
|
|
404
|
+
naluType !== HevcNalUnitType.SPS_NUT
|
|
405
|
+
&& naluType !== HevcNalUnitType.PPS_NUT
|
|
406
|
+
&& naluType !== HevcNalUnitType.VPS_NUT
|
|
407
|
+
) {
|
|
408
|
+
nalUnits.push(loc);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
let totalSize = 0;
|
|
414
|
+
for (const nalUnit of nalUnits) {
|
|
415
|
+
totalSize += NAL_UNIT_LENGTH_SIZE + nalUnit.length;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const lengthPrefixedData = new Uint8Array(totalSize);
|
|
419
|
+
const dataView = new DataView(lengthPrefixedData.buffer);
|
|
420
|
+
let offset = 0;
|
|
421
|
+
|
|
422
|
+
// Write each NAL unit with its length prefix
|
|
423
|
+
for (const nalUnit of nalUnits) {
|
|
424
|
+
const length = nalUnit.length;
|
|
425
|
+
|
|
426
|
+
dataView.setUint32(offset, length, false);
|
|
427
|
+
offset += 4;
|
|
428
|
+
|
|
429
|
+
lengthPrefixedData.set(
|
|
430
|
+
packetData.subarray(nalUnit.offset, nalUnit.offset + nalUnit.length),
|
|
431
|
+
offset,
|
|
432
|
+
);
|
|
433
|
+
offset += nalUnit.length;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
packetData = lengthPrefixedData;
|
|
437
|
+
}
|
|
438
|
+
} else if (this.codec === 'vp8') {
|
|
439
|
+
if (!this.packetEmitted) {
|
|
440
|
+
decoderConfigCodecString = extractVideoCodecString({
|
|
441
|
+
width: this.config.width,
|
|
442
|
+
height: this.config.height,
|
|
443
|
+
codec: 'vp8',
|
|
444
|
+
codecDescription: null,
|
|
445
|
+
colorSpace: null,
|
|
446
|
+
avcType: null,
|
|
447
|
+
avcCodecInfo: null,
|
|
448
|
+
hevcCodecInfo: null,
|
|
449
|
+
vp9CodecInfo: null,
|
|
450
|
+
av1CodecInfo: null,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
} else if (this.codec === 'vp9') {
|
|
454
|
+
if (!this.packetEmitted) {
|
|
455
|
+
const vp9CodecInfo = extractVp9CodecInfoFromPacket(packetData);
|
|
456
|
+
|
|
457
|
+
decoderConfigCodecString = extractVideoCodecString({
|
|
458
|
+
width: this.config.width,
|
|
459
|
+
height: this.config.height,
|
|
460
|
+
codec: 'vp9',
|
|
461
|
+
codecDescription: null,
|
|
462
|
+
colorSpace: null,
|
|
463
|
+
avcType: null,
|
|
464
|
+
avcCodecInfo: null,
|
|
465
|
+
hevcCodecInfo: null,
|
|
466
|
+
vp9CodecInfo,
|
|
467
|
+
av1CodecInfo: null,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
} else if (this.codec === 'av1') {
|
|
471
|
+
if (!this.packetEmitted) {
|
|
472
|
+
const av1CodecInfo = extractAv1CodecInfoFromPacket(packetData);
|
|
473
|
+
|
|
474
|
+
decoderConfigCodecString = extractVideoCodecString({
|
|
475
|
+
width: this.config.width,
|
|
476
|
+
height: this.config.height,
|
|
477
|
+
codec: 'av1',
|
|
478
|
+
codecDescription: null,
|
|
479
|
+
colorSpace: null,
|
|
480
|
+
avcType: null,
|
|
481
|
+
avcCodecInfo: null,
|
|
482
|
+
hevcCodecInfo: null,
|
|
483
|
+
vp9CodecInfo: null,
|
|
484
|
+
av1CodecInfo,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
} else {
|
|
488
|
+
throw new Error('Unreachable.');
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const sideData: EncodedPacketSideData = {};
|
|
492
|
+
const matroskaBlockAdditional = this.packet.getSideData(NodeAv.AV_PKT_DATA_MATROSKA_BLOCKADDITIONAL);
|
|
493
|
+
if (matroskaBlockAdditional) {
|
|
494
|
+
sideData.alpha = toUint8Array(matroskaBlockAdditional).subarray(8); // Skip the BlockAddId
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const packet = new EncodedPacket(
|
|
498
|
+
packetData,
|
|
499
|
+
this.packet.isKeyframe ? 'key' : 'delta',
|
|
500
|
+
timestamp,
|
|
501
|
+
duration,
|
|
502
|
+
undefined,
|
|
503
|
+
undefined,
|
|
504
|
+
sideData,
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
if (decoderConfigCodecString !== null) {
|
|
508
|
+
// Create the decoder config
|
|
509
|
+
metadata.decoderConfig = {
|
|
510
|
+
codec: decoderConfigCodecString,
|
|
511
|
+
codedWidth: this.codecContext.width,
|
|
512
|
+
codedHeight: this.codecContext.height,
|
|
513
|
+
displayAspectWidth: this.config.displayWidth ?? this.codecContext.width,
|
|
514
|
+
displayAspectHeight: this.config.displayHeight ?? this.codecContext.height,
|
|
515
|
+
description: decoderConfigDescription ?? undefined,
|
|
516
|
+
colorSpace: {
|
|
517
|
+
primaries:
|
|
518
|
+
unmapColorPrimaries(this.codecContext.colorPrimaries) as VideoColorPrimaries,
|
|
519
|
+
matrix:
|
|
520
|
+
unmapMatrixCoefficients(this.codecContext.colorSpace) as VideoMatrixCoefficients,
|
|
521
|
+
transfer:
|
|
522
|
+
unmapTransferCharacteristics(this.codecContext.colorTrc) as VideoTransferCharacteristics,
|
|
523
|
+
fullRange: this.codecContext.colorRange === NodeAv.AVCOL_RANGE_JPEG
|
|
524
|
+
? true
|
|
525
|
+
: this.codecContext.colorRange === NodeAv.AVCOL_RANGE_MPEG
|
|
526
|
+
? false
|
|
527
|
+
: undefined,
|
|
528
|
+
},
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
this.packetEmitted = true;
|
|
533
|
+
this.onPacket(packet, metadata);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async flush(): Promise<void> {
|
|
537
|
+
if (this.codecContext) {
|
|
538
|
+
// Send null frame to signal flush
|
|
539
|
+
const ret = await this.codecContext.sendFrame(null);
|
|
540
|
+
NodeAv.FFmpegError.throwIfError(ret, 'Send frame');
|
|
541
|
+
|
|
542
|
+
// Keep receiving packets until no more are available
|
|
543
|
+
while (true) {
|
|
544
|
+
const receiveRet = await this.codecContext.receivePacket(this.packet);
|
|
545
|
+
if (receiveRet === NodeAv.AVERROR_EAGAIN || receiveRet === NodeAv.AVERROR_EOF) {
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
this.receivePacket(receiveRet);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
this.codecContext.freeContext();
|
|
553
|
+
this.codecContext = null;
|
|
554
|
+
// The codec is done now and can't be reused. Any subsequent encode call will first need to recreate a
|
|
555
|
+
// codec context.
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
this.packetEmitted = false;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
close(): MaybePromise<void> {
|
|
562
|
+
this.codecContext?.freeContext();
|
|
563
|
+
this.frame.free();
|
|
564
|
+
this.packet.free();
|
|
565
|
+
this.scaler?.freeContext();
|
|
566
|
+
this.dstFrame?.free();
|
|
567
|
+
}
|
|
568
|
+
}
|