@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,474 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*!
|
|
3
|
+
* Copyright (c) 2026-present, Vanilagy and contributors
|
|
4
|
+
*
|
|
5
|
+
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
6
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
7
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
8
|
+
*/
|
|
9
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
12
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
13
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(o, k2, desc);
|
|
16
|
+
}) : (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
o[k2] = m[k];
|
|
19
|
+
}));
|
|
20
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
21
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
22
|
+
}) : function(o, v) {
|
|
23
|
+
o["default"] = v;
|
|
24
|
+
});
|
|
25
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
26
|
+
var ownKeys = function(o) {
|
|
27
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
28
|
+
var ar = [];
|
|
29
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
30
|
+
return ar;
|
|
31
|
+
};
|
|
32
|
+
return ownKeys(o);
|
|
33
|
+
};
|
|
34
|
+
return function (mod) {
|
|
35
|
+
if (mod && mod.__esModule) return mod;
|
|
36
|
+
var result = {};
|
|
37
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
38
|
+
__setModuleDefault(result, mod);
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
})();
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.NodeAvVideoEncoder = void 0;
|
|
44
|
+
const mediabunny_1 = require("mediabunny");
|
|
45
|
+
const NodeAv = __importStar(require("node-av"));
|
|
46
|
+
const misc_1 = require("./misc");
|
|
47
|
+
const video_sample_1 = require("./video-sample");
|
|
48
|
+
const codec_data_1 = require("../../../src/codec-data");
|
|
49
|
+
const codec_1 = require("../../../src/codec");
|
|
50
|
+
const misc_2 = require("../../../src/misc");
|
|
51
|
+
class NodeAvVideoEncoder extends mediabunny_1.CustomVideoEncoder {
|
|
52
|
+
constructor() {
|
|
53
|
+
super(...arguments);
|
|
54
|
+
this.codecContext = null;
|
|
55
|
+
this.lastBuffer = null;
|
|
56
|
+
this.packetEmitted = false;
|
|
57
|
+
this.scaler = null;
|
|
58
|
+
this.lastScalerKey = null;
|
|
59
|
+
this.dstFrame = null;
|
|
60
|
+
// Bookkeeping to restore the original timing information
|
|
61
|
+
this.preciseTimings = [];
|
|
62
|
+
}
|
|
63
|
+
static supports(codec, config) {
|
|
64
|
+
return (codec === 'avc' || codec === 'hevc' || codec === 'vp8' || codec === 'vp9' || codec === 'av1')
|
|
65
|
+
&& config.bitrateMode !== 'quantizer';
|
|
66
|
+
}
|
|
67
|
+
async init() {
|
|
68
|
+
this.frame = new NodeAv.Frame();
|
|
69
|
+
this.frame.alloc();
|
|
70
|
+
this.frame.timeBase = new NodeAv.Rational(1, 1e6);
|
|
71
|
+
this.packet = new NodeAv.Packet();
|
|
72
|
+
this.packet.alloc();
|
|
73
|
+
const codecId = misc_1.CODEC_TO_CODEC_ID[this.codec];
|
|
74
|
+
(0, misc_2.assert)(codecId !== undefined);
|
|
75
|
+
let codec = null;
|
|
76
|
+
if (this.codec === 'vp9' && this.config.alpha === 'keep') {
|
|
77
|
+
codec = NodeAv.Codec.findEncoderByName(misc_1.LIBVPX_VP9) ?? NodeAv.Codec.findEncoder(codecId);
|
|
78
|
+
}
|
|
79
|
+
else if (this.config.hardwareAcceleration === 'prefer-software') {
|
|
80
|
+
codec = NodeAv.Codec.findEncoder(codecId);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
codec = (0, misc_1.getHardwareEncoderCodec)(codecId) ?? NodeAv.Codec.findEncoder(codecId);
|
|
84
|
+
}
|
|
85
|
+
if (!codec) {
|
|
86
|
+
throw new Error(`Unable to obtain libav codec for '${this.codec}'.`);
|
|
87
|
+
}
|
|
88
|
+
this.avCodec = codec;
|
|
89
|
+
await this.createCodecContext();
|
|
90
|
+
}
|
|
91
|
+
async createCodecContext() {
|
|
92
|
+
(0, misc_2.assert)(this.codecContext === null);
|
|
93
|
+
const codecContext = new NodeAv.CodecContext();
|
|
94
|
+
codecContext.allocContext3(this.avCodec);
|
|
95
|
+
let pixelFormat = NodeAv.AV_PIX_FMT_YUV420P;
|
|
96
|
+
if (this.avCodec.pixelFormats) {
|
|
97
|
+
if (!this.avCodec.pixelFormats.includes(NodeAv.AV_PIX_FMT_YUV420P)) {
|
|
98
|
+
pixelFormat = this.avCodec.pixelFormats[0];
|
|
99
|
+
}
|
|
100
|
+
if (this.config.alpha === 'keep' && this.avCodec.pixelFormats.includes(NodeAv.AV_PIX_FMT_YUVA420P)) {
|
|
101
|
+
pixelFormat = NodeAv.AV_PIX_FMT_YUVA420P;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const pixelAspectRatio = (0, misc_2.simplifyRational)({
|
|
105
|
+
num: (this.config.displayWidth ?? this.config.width) * this.config.height,
|
|
106
|
+
den: (this.config.displayHeight ?? this.config.height) * this.config.width,
|
|
107
|
+
});
|
|
108
|
+
codecContext.width = this.config.width;
|
|
109
|
+
codecContext.height = this.config.height;
|
|
110
|
+
codecContext.pixelFormat = pixelFormat;
|
|
111
|
+
codecContext.timeBase = new NodeAv.Rational(1, 1e6);
|
|
112
|
+
codecContext.gopSize = 60;
|
|
113
|
+
codecContext.framerate = new NodeAv.Rational(Math.round(this.config.framerate ?? 0) || 30, 1);
|
|
114
|
+
codecContext.bitRate = BigInt(this.config.bitrate ?? mediabunny_1.QUALITY_MEDIUM._toVideoBitrate(this.codec, this.config.width, this.config.height));
|
|
115
|
+
codecContext.sampleAspectRatio = new NodeAv.Rational(pixelAspectRatio.num, pixelAspectRatio.den);
|
|
116
|
+
if (this.config.bitrateMode === 'constant') {
|
|
117
|
+
codecContext.rcMinRate = codecContext.bitRate;
|
|
118
|
+
codecContext.rcMaxRate = codecContext.bitRate;
|
|
119
|
+
}
|
|
120
|
+
const isRealtime = this.config.latencyMode === 'realtime';
|
|
121
|
+
if (this.avCodec.name === 'libx264') {
|
|
122
|
+
if (isRealtime) {
|
|
123
|
+
codecContext.setOption('tune', 'zerolatency');
|
|
124
|
+
codecContext.setOption('preset', 'ultrafast');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else if (this.avCodec.name === 'libx265') {
|
|
128
|
+
codecContext.setOption('x265-params', 'log-level=error');
|
|
129
|
+
if (isRealtime) {
|
|
130
|
+
codecContext.setOption('tune', 'zerolatency');
|
|
131
|
+
codecContext.setOption('preset', 'ultrafast');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else if (this.avCodec.name === 'libvpx') {
|
|
135
|
+
if (isRealtime) {
|
|
136
|
+
codecContext.setOption('deadline', 'realtime');
|
|
137
|
+
codecContext.setOption('cpu-used', '8');
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
codecContext.setOption('cpu-used', '8');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else if (this.avCodec.name === 'libvpx-vp9') {
|
|
144
|
+
codecContext.setOption('deadline', 'realtime');
|
|
145
|
+
if (isRealtime) {
|
|
146
|
+
codecContext.setOption('cpu-used', '8');
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
codecContext.setOption('cpu-used', '5');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else if (this.avCodec.name === 'libsvtav1') {
|
|
153
|
+
// SVTAV1 can be silenced by setting an environment variable:
|
|
154
|
+
// https://superuser.com/questions/1775236/how-to-remove-svt-av1-information-from-ffmpeg-output
|
|
155
|
+
process.env['SVT_LOG'] = '1';
|
|
156
|
+
if (isRealtime) {
|
|
157
|
+
codecContext.setOption('preset', '12');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const ret = await codecContext.open2();
|
|
161
|
+
NodeAv.FFmpegError.throwIfError(ret, 'Open codec context');
|
|
162
|
+
this.codecContext = codecContext;
|
|
163
|
+
}
|
|
164
|
+
async encode(videoSample, options) {
|
|
165
|
+
if (this.codecContext === null) {
|
|
166
|
+
await this.createCodecContext();
|
|
167
|
+
(0, misc_2.assert)(this.codecContext);
|
|
168
|
+
}
|
|
169
|
+
if (videoSample._data instanceof video_sample_1.AvFrameVideoSampleResource) {
|
|
170
|
+
this.frame.ref(videoSample._data.frame);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
if (videoSample.format === null) {
|
|
174
|
+
throw new Error('Cannot encode foreign VideoSample with unknown (null) format.');
|
|
175
|
+
}
|
|
176
|
+
this.lastBuffer = await (0, video_sample_1.copyVideoSampleToAvFrame)(videoSample, this.frame, this.lastBuffer);
|
|
177
|
+
}
|
|
178
|
+
let frameToEncode = this.frame;
|
|
179
|
+
const requiresScaler = this.codecContext.pixelFormat !== this.frame.format
|
|
180
|
+
|| this.codecContext.width !== this.frame.width
|
|
181
|
+
|| this.codecContext.height !== this.frame.height;
|
|
182
|
+
if (requiresScaler) {
|
|
183
|
+
if (!this.scaler) {
|
|
184
|
+
this.scaler = new NodeAv.SoftwareScaleContext();
|
|
185
|
+
}
|
|
186
|
+
const key = `${this.frame.width}x${this.frame.height}:${this.frame.format}`;
|
|
187
|
+
const needsConfigure = key !== this.lastScalerKey;
|
|
188
|
+
if (needsConfigure) {
|
|
189
|
+
this.scaler.getContext(this.frame.width, this.frame.height, this.frame.format, this.codecContext.width, this.codecContext.height, this.codecContext.pixelFormat, NodeAv.SWS_FAST_BILINEAR);
|
|
190
|
+
this.lastScalerKey = key;
|
|
191
|
+
const ret = this.scaler.initContext();
|
|
192
|
+
NodeAv.FFmpegError.throwIfError(ret, 'initContext');
|
|
193
|
+
}
|
|
194
|
+
if (!this.dstFrame) {
|
|
195
|
+
this.dstFrame = new NodeAv.Frame();
|
|
196
|
+
this.dstFrame.alloc();
|
|
197
|
+
this.dstFrame.width = this.codecContext.width;
|
|
198
|
+
this.dstFrame.height = this.codecContext.height;
|
|
199
|
+
this.dstFrame.format = this.codecContext.pixelFormat;
|
|
200
|
+
this.dstFrame.allocBuffer();
|
|
201
|
+
}
|
|
202
|
+
await this.scaler.scaleFrame(this.dstFrame, this.frame);
|
|
203
|
+
this.dstFrame.copyProps(this.frame);
|
|
204
|
+
frameToEncode = this.dstFrame;
|
|
205
|
+
}
|
|
206
|
+
frameToEncode.pts = BigInt(videoSample.microsecondTimestamp);
|
|
207
|
+
frameToEncode.duration = BigInt(videoSample.microsecondDuration);
|
|
208
|
+
frameToEncode.timeBase = new NodeAv.Rational(1, 1e6);
|
|
209
|
+
// Let's just set both for good measure
|
|
210
|
+
frameToEncode.pictType = options?.keyFrame
|
|
211
|
+
? NodeAv.AV_PICTURE_TYPE_I
|
|
212
|
+
: NodeAv.AV_PICTURE_TYPE_NONE;
|
|
213
|
+
frameToEncode.keyFrame = options?.keyFrame
|
|
214
|
+
? 1
|
|
215
|
+
: 0;
|
|
216
|
+
const preciseTimingIndex = (0, misc_2.binarySearchLessOrEqual)(this.preciseTimings, videoSample.microsecondTimestamp, x => x.microsecondTimestamp);
|
|
217
|
+
const existingEntry = preciseTimingIndex !== -1
|
|
218
|
+
? this.preciseTimings[preciseTimingIndex]
|
|
219
|
+
: null;
|
|
220
|
+
if (existingEntry && existingEntry.microsecondTimestamp === videoSample.microsecondTimestamp) {
|
|
221
|
+
if (existingEntry.timestamp !== videoSample.timestamp) {
|
|
222
|
+
// Mapping isn't unique, can't use the timestamp
|
|
223
|
+
existingEntry.timestampIsValid = false;
|
|
224
|
+
}
|
|
225
|
+
if (existingEntry.duration !== videoSample.duration) {
|
|
226
|
+
// Mapping isn't unique, can't use the duration
|
|
227
|
+
existingEntry.durationIsValid = false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
this.preciseTimings.splice(preciseTimingIndex + 1, 0, {
|
|
232
|
+
microsecondTimestamp: videoSample.microsecondTimestamp,
|
|
233
|
+
timestamp: videoSample.timestamp,
|
|
234
|
+
duration: videoSample.duration,
|
|
235
|
+
timestampIsValid: true,
|
|
236
|
+
durationIsValid: true,
|
|
237
|
+
});
|
|
238
|
+
// Make sure it doesn't grow indefinitely
|
|
239
|
+
if (this.preciseTimings.length > 128) {
|
|
240
|
+
this.preciseTimings.shift();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const ret = await this.codecContext.sendFrame(frameToEncode);
|
|
244
|
+
NodeAv.FFmpegError.throwIfError(ret, 'Send frame');
|
|
245
|
+
// Keep receiving packets until no more are available for this frame
|
|
246
|
+
while (true) {
|
|
247
|
+
const receiveRet = await this.codecContext.receivePacket(this.packet);
|
|
248
|
+
if (receiveRet === NodeAv.AVERROR_EAGAIN || receiveRet === NodeAv.AVERROR_EOF) {
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
this.receivePacket(receiveRet);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
receivePacket(ret) {
|
|
255
|
+
(0, misc_2.assert)(this.codecContext);
|
|
256
|
+
NodeAv.FFmpegError.throwIfError(ret, 'Receive packet');
|
|
257
|
+
if (!this.packet.data) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
let packetData = (0, misc_2.toUint8Array)(this.packet.data);
|
|
261
|
+
let timestamp = Number(this.packet.pts) / 1e6;
|
|
262
|
+
let duration = Number(this.packet.duration) / 1e6;
|
|
263
|
+
const preciseTimingIndex = (0, misc_2.binarySearchLessOrEqual)(this.preciseTimings, Number(this.packet.pts), x => x.microsecondTimestamp);
|
|
264
|
+
const entry = preciseTimingIndex !== -1
|
|
265
|
+
? this.preciseTimings[preciseTimingIndex]
|
|
266
|
+
: null;
|
|
267
|
+
// If there's a relevant timing entry, refine the packet's timing data to get better accuracy than
|
|
268
|
+
// microseconds
|
|
269
|
+
if (entry && entry.microsecondTimestamp === Number(this.packet.pts)) {
|
|
270
|
+
if (entry.timestampIsValid) {
|
|
271
|
+
timestamp = entry.timestamp;
|
|
272
|
+
}
|
|
273
|
+
if (entry.durationIsValid) {
|
|
274
|
+
duration = entry.duration;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const metadata = {};
|
|
278
|
+
let decoderConfigCodecString = null;
|
|
279
|
+
let decoderConfigDescription = null;
|
|
280
|
+
if (this.codec === 'avc' || this.codec === 'hevc') {
|
|
281
|
+
let expectsAnnexB = false;
|
|
282
|
+
if (this.codec === 'avc') {
|
|
283
|
+
expectsAnnexB = this.config.avc?.format === 'annexb';
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
// eslint-disable-next-line @stylistic/max-len
|
|
287
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
|
288
|
+
expectsAnnexB = this.config.hevc?.format === 'annexb';
|
|
289
|
+
}
|
|
290
|
+
if (!this.packetEmitted) {
|
|
291
|
+
let serializedRecord;
|
|
292
|
+
if (this.codec === 'avc') {
|
|
293
|
+
const record = (0, codec_data_1.extractAvcDecoderConfigurationRecord)(this.packet.data);
|
|
294
|
+
if (!record) {
|
|
295
|
+
throw new Error('Invalid AVC data, could not extract decoder configuration record.');
|
|
296
|
+
}
|
|
297
|
+
serializedRecord = (0, codec_data_1.serializeAvcDecoderConfigurationRecord)(record);
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
const record = (0, codec_data_1.extractHevcDecoderConfigurationRecord)(this.packet.data);
|
|
301
|
+
if (!record) {
|
|
302
|
+
throw new Error('Invalid HEVC data, could not extract decoder configuration record.');
|
|
303
|
+
}
|
|
304
|
+
serializedRecord = (0, codec_data_1.serializeHevcDecoderConfigurationRecord)(record);
|
|
305
|
+
}
|
|
306
|
+
decoderConfigCodecString = (0, codec_1.extractVideoCodecString)({
|
|
307
|
+
width: this.config.width,
|
|
308
|
+
height: this.config.height,
|
|
309
|
+
codec: this.codec,
|
|
310
|
+
codecDescription: serializedRecord,
|
|
311
|
+
colorSpace: null,
|
|
312
|
+
avcType: 1,
|
|
313
|
+
avcCodecInfo: null,
|
|
314
|
+
hevcCodecInfo: null,
|
|
315
|
+
vp9CodecInfo: null,
|
|
316
|
+
av1CodecInfo: null,
|
|
317
|
+
});
|
|
318
|
+
if (!expectsAnnexB) {
|
|
319
|
+
decoderConfigDescription = serializedRecord;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (!expectsAnnexB) {
|
|
323
|
+
const NAL_UNIT_LENGTH_SIZE = 4;
|
|
324
|
+
const nalUnits = [];
|
|
325
|
+
for (const loc of (0, codec_data_1.iterateNalUnitsInAnnexB)(packetData)) {
|
|
326
|
+
if (this.codec === 'avc') {
|
|
327
|
+
const naluType = (0, codec_data_1.extractNalUnitTypeForAvc)(packetData[loc.offset]);
|
|
328
|
+
// Certain NALUs get stripped
|
|
329
|
+
if (naluType !== codec_data_1.AvcNalUnitType.SPS
|
|
330
|
+
&& naluType !== codec_data_1.AvcNalUnitType.PPS
|
|
331
|
+
&& naluType !== codec_data_1.AvcNalUnitType.SPS_EXT) {
|
|
332
|
+
nalUnits.push(loc);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
const naluType = (0, codec_data_1.extractNalUnitTypeForHevc)(packetData[loc.offset]);
|
|
337
|
+
// Certain NALUs get stripped
|
|
338
|
+
if (naluType !== codec_data_1.HevcNalUnitType.SPS_NUT
|
|
339
|
+
&& naluType !== codec_data_1.HevcNalUnitType.PPS_NUT
|
|
340
|
+
&& naluType !== codec_data_1.HevcNalUnitType.VPS_NUT) {
|
|
341
|
+
nalUnits.push(loc);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
let totalSize = 0;
|
|
346
|
+
for (const nalUnit of nalUnits) {
|
|
347
|
+
totalSize += NAL_UNIT_LENGTH_SIZE + nalUnit.length;
|
|
348
|
+
}
|
|
349
|
+
const lengthPrefixedData = new Uint8Array(totalSize);
|
|
350
|
+
const dataView = new DataView(lengthPrefixedData.buffer);
|
|
351
|
+
let offset = 0;
|
|
352
|
+
// Write each NAL unit with its length prefix
|
|
353
|
+
for (const nalUnit of nalUnits) {
|
|
354
|
+
const length = nalUnit.length;
|
|
355
|
+
dataView.setUint32(offset, length, false);
|
|
356
|
+
offset += 4;
|
|
357
|
+
lengthPrefixedData.set(packetData.subarray(nalUnit.offset, nalUnit.offset + nalUnit.length), offset);
|
|
358
|
+
offset += nalUnit.length;
|
|
359
|
+
}
|
|
360
|
+
packetData = lengthPrefixedData;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
else if (this.codec === 'vp8') {
|
|
364
|
+
if (!this.packetEmitted) {
|
|
365
|
+
decoderConfigCodecString = (0, codec_1.extractVideoCodecString)({
|
|
366
|
+
width: this.config.width,
|
|
367
|
+
height: this.config.height,
|
|
368
|
+
codec: 'vp8',
|
|
369
|
+
codecDescription: null,
|
|
370
|
+
colorSpace: null,
|
|
371
|
+
avcType: null,
|
|
372
|
+
avcCodecInfo: null,
|
|
373
|
+
hevcCodecInfo: null,
|
|
374
|
+
vp9CodecInfo: null,
|
|
375
|
+
av1CodecInfo: null,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
else if (this.codec === 'vp9') {
|
|
380
|
+
if (!this.packetEmitted) {
|
|
381
|
+
const vp9CodecInfo = (0, codec_data_1.extractVp9CodecInfoFromPacket)(packetData);
|
|
382
|
+
decoderConfigCodecString = (0, codec_1.extractVideoCodecString)({
|
|
383
|
+
width: this.config.width,
|
|
384
|
+
height: this.config.height,
|
|
385
|
+
codec: 'vp9',
|
|
386
|
+
codecDescription: null,
|
|
387
|
+
colorSpace: null,
|
|
388
|
+
avcType: null,
|
|
389
|
+
avcCodecInfo: null,
|
|
390
|
+
hevcCodecInfo: null,
|
|
391
|
+
vp9CodecInfo,
|
|
392
|
+
av1CodecInfo: null,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
else if (this.codec === 'av1') {
|
|
397
|
+
if (!this.packetEmitted) {
|
|
398
|
+
const av1CodecInfo = (0, codec_data_1.extractAv1CodecInfoFromPacket)(packetData);
|
|
399
|
+
decoderConfigCodecString = (0, codec_1.extractVideoCodecString)({
|
|
400
|
+
width: this.config.width,
|
|
401
|
+
height: this.config.height,
|
|
402
|
+
codec: 'av1',
|
|
403
|
+
codecDescription: null,
|
|
404
|
+
colorSpace: null,
|
|
405
|
+
avcType: null,
|
|
406
|
+
avcCodecInfo: null,
|
|
407
|
+
hevcCodecInfo: null,
|
|
408
|
+
vp9CodecInfo: null,
|
|
409
|
+
av1CodecInfo,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
throw new Error('Unreachable.');
|
|
415
|
+
}
|
|
416
|
+
const sideData = {};
|
|
417
|
+
const matroskaBlockAdditional = this.packet.getSideData(NodeAv.AV_PKT_DATA_MATROSKA_BLOCKADDITIONAL);
|
|
418
|
+
if (matroskaBlockAdditional) {
|
|
419
|
+
sideData.alpha = (0, misc_2.toUint8Array)(matroskaBlockAdditional).subarray(8); // Skip the BlockAddId
|
|
420
|
+
}
|
|
421
|
+
const packet = new mediabunny_1.EncodedPacket(packetData, this.packet.isKeyframe ? 'key' : 'delta', timestamp, duration, undefined, undefined, sideData);
|
|
422
|
+
if (decoderConfigCodecString !== null) {
|
|
423
|
+
// Create the decoder config
|
|
424
|
+
metadata.decoderConfig = {
|
|
425
|
+
codec: decoderConfigCodecString,
|
|
426
|
+
codedWidth: this.codecContext.width,
|
|
427
|
+
codedHeight: this.codecContext.height,
|
|
428
|
+
displayAspectWidth: this.config.displayWidth ?? this.codecContext.width,
|
|
429
|
+
displayAspectHeight: this.config.displayHeight ?? this.codecContext.height,
|
|
430
|
+
description: decoderConfigDescription ?? undefined,
|
|
431
|
+
colorSpace: {
|
|
432
|
+
primaries: (0, misc_1.unmapColorPrimaries)(this.codecContext.colorPrimaries),
|
|
433
|
+
matrix: (0, misc_1.unmapMatrixCoefficients)(this.codecContext.colorSpace),
|
|
434
|
+
transfer: (0, misc_1.unmapTransferCharacteristics)(this.codecContext.colorTrc),
|
|
435
|
+
fullRange: this.codecContext.colorRange === NodeAv.AVCOL_RANGE_JPEG
|
|
436
|
+
? true
|
|
437
|
+
: this.codecContext.colorRange === NodeAv.AVCOL_RANGE_MPEG
|
|
438
|
+
? false
|
|
439
|
+
: undefined,
|
|
440
|
+
},
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
this.packetEmitted = true;
|
|
444
|
+
this.onPacket(packet, metadata);
|
|
445
|
+
}
|
|
446
|
+
async flush() {
|
|
447
|
+
if (this.codecContext) {
|
|
448
|
+
// Send null frame to signal flush
|
|
449
|
+
const ret = await this.codecContext.sendFrame(null);
|
|
450
|
+
NodeAv.FFmpegError.throwIfError(ret, 'Send frame');
|
|
451
|
+
// Keep receiving packets until no more are available
|
|
452
|
+
while (true) {
|
|
453
|
+
const receiveRet = await this.codecContext.receivePacket(this.packet);
|
|
454
|
+
if (receiveRet === NodeAv.AVERROR_EAGAIN || receiveRet === NodeAv.AVERROR_EOF) {
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
this.receivePacket(receiveRet);
|
|
458
|
+
}
|
|
459
|
+
this.codecContext.freeContext();
|
|
460
|
+
this.codecContext = null;
|
|
461
|
+
// The codec is done now and can't be reused. Any subsequent encode call will first need to recreate a
|
|
462
|
+
// codec context.
|
|
463
|
+
}
|
|
464
|
+
this.packetEmitted = false;
|
|
465
|
+
}
|
|
466
|
+
close() {
|
|
467
|
+
this.codecContext?.freeContext();
|
|
468
|
+
this.frame.free();
|
|
469
|
+
this.packet.free();
|
|
470
|
+
this.scaler?.freeContext();
|
|
471
|
+
this.dstFrame?.free();
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
exports.NodeAvVideoEncoder = NodeAvVideoEncoder;
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
import { type MaybePromise, VideoSamplePixelFormat, VideoSampleResource, VideoSampleColorSpace, SetRequired, VideoSampleInit, VideoSample, VideoDataPlane, VideoSampleTransformationDescription } from 'mediabunny';
|
|
9
|
+
import * as NodeAv from 'node-av';
|
|
10
|
+
/**
|
|
11
|
+
* A custom `VideoSampleResource` backed by NodeAV's
|
|
12
|
+
* [`Frame`](https://seydx.github.io/node-av/api/lib/classes/Frame.html), which in turn is backed by FFmpeg's
|
|
13
|
+
* [`AVFrame`](https://ffmpeg.org/doxygen/2.7/structAVFrame.html). You can use this resource to create `VideoSample`
|
|
14
|
+
* instances that are directly backed by FFmpeg's `AVFrame` without data having to be copied. Since `AVFrame`s can
|
|
15
|
+
* themselves be backed by data on the GPU, this enables zero-copy hardware-accelerated decode and encode paths.
|
|
16
|
+
*
|
|
17
|
+
* When using Electron, you can directly create `Frame` instances without the data having to leave the GPU. For more,
|
|
18
|
+
* see [NodeAV's docs](https://seydx.github.io/node-av/api/lib/classes/Frame.html).
|
|
19
|
+
*
|
|
20
|
+
* When passed, the `Frame` is now owned by resource, meaning it takes care of closing the frame later. If you want to
|
|
21
|
+
* keep a copy for your own use, clone the frame first.
|
|
22
|
+
*
|
|
23
|
+
* @group \@mediabunny/server
|
|
24
|
+
* @public
|
|
25
|
+
*/
|
|
26
|
+
export declare class AvFrameVideoSampleResource extends VideoSampleResource {
|
|
27
|
+
/**
|
|
28
|
+
* The NodeAV [`Frame`](https://seydx.github.io/node-av/api/lib/classes/Frame.html) instance backing this resource.
|
|
29
|
+
* Access throws if the resource has already been closed.
|
|
30
|
+
*/
|
|
31
|
+
get frame(): NodeAv.Frame;
|
|
32
|
+
constructor(frame: NodeAv.Frame);
|
|
33
|
+
getFormat(): VideoSamplePixelFormat | null;
|
|
34
|
+
getCodedWidth(): number;
|
|
35
|
+
getCodedHeight(): number;
|
|
36
|
+
getSquarePixelWidth(): number;
|
|
37
|
+
getSquarePixelHeight(): number;
|
|
38
|
+
getColorSpace(): VideoSampleColorSpace;
|
|
39
|
+
close(): void;
|
|
40
|
+
getDataPlanes(): MaybePromise<VideoDataPlane[]>;
|
|
41
|
+
toRgbSample(init: SetRequired<VideoSampleInit, 'timestamp'>, colorSpace: PredefinedColorSpace): Promise<VideoSample>;
|
|
42
|
+
}
|
|
43
|
+
export declare const copyVideoSampleToAvFrame: (sample: VideoSample, frame: NodeAv.Frame, lastBuffer: Buffer | null) => Promise<Buffer<ArrayBufferLike>>;
|
|
44
|
+
export declare const transformVideoSample: (sample: VideoSample, description: VideoSampleTransformationDescription) => Promise<VideoSample | null>;
|
|
45
|
+
//# sourceMappingURL=video-sample.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"video-sample.d.ts","sourceRoot":"","sources":["../../../src/video-sample.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EACN,KAAK,YAAY,EACjB,sBAAsB,EACtB,mBAAmB,EACnB,qBAAqB,EACrB,WAAW,EACX,eAAe,EACf,WAAW,EACX,cAAc,EACd,oCAAoC,EACpC,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,MAAM,MAAM,SAAS,CAAC;AAqBlC;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,0BAA2B,SAAQ,mBAAmB;IAIlE;;;OAGG;IACH,IAAI,KAAK,iBAMR;gBAEW,KAAK,EAAE,MAAM,CAAC,KAAK;IAU/B,SAAS,IAAI,sBAAsB,GAAG,IAAI;IAI1C,aAAa,IAAI,MAAM;IAIvB,cAAc,IAAI,MAAM;IAIxB,mBAAmB,IAAI,MAAM;IAQ7B,oBAAoB,IAAI,MAAM;IAQ9B,aAAa,IAAI,qBAAqB;IActC,KAAK,IAAI,IAAI;IAKb,aAAa,IAAI,YAAY,CAAC,cAAc,EAAE,CAAC;IASzC,WAAW,CAChB,IAAI,EAAE,WAAW,CAAC,eAAe,EAAE,WAAW,CAAC,EAG/C,UAAU,EAAE,oBAAoB,GAC9B,OAAO,CAAC,WAAW,CAAC;CAiCvB;AAED,eAAO,MAAM,wBAAwB,GAAU,QAAQ,WAAW,EAAE,OAAO,MAAM,CAAC,KAAK,EAAE,YAAY,MAAM,GAAG,IAAI,qCA+BjH,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAChC,QAAQ,WAAW,EACnB,aAAa,oCAAoC,KAC/C,OAAO,CAAC,WAAW,GAAG,IAAI,CAkG5B,CAAC"}
|