@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.
Files changed (41) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +276 -0
  3. package/dist/bundles/mediabunny-server.cjs +3361 -0
  4. package/dist/bundles/mediabunny-server.min.cjs +10 -0
  5. package/dist/bundles/mediabunny-server.min.mjs +9 -0
  6. package/dist/bundles/mediabunny-server.mjs +3334 -0
  7. package/dist/mediabunny-server.d.ts +105 -0
  8. package/dist/modules/src/audio-decoder.d.ts +21 -0
  9. package/dist/modules/src/audio-decoder.d.ts.map +1 -0
  10. package/dist/modules/src/audio-decoder.js +132 -0
  11. package/dist/modules/src/audio-encoder.d.ts +35 -0
  12. package/dist/modules/src/audio-encoder.d.ts.map +1 -0
  13. package/dist/modules/src/audio-encoder.js +329 -0
  14. package/dist/modules/src/audio-sample.d.ts +38 -0
  15. package/dist/modules/src/audio-sample.d.ts.map +1 -0
  16. package/dist/modules/src/audio-sample.js +119 -0
  17. package/dist/modules/src/index.d.ts +39 -0
  18. package/dist/modules/src/index.d.ts.map +1 -0
  19. package/dist/modules/src/index.js +132 -0
  20. package/dist/modules/src/misc.d.ts +26 -0
  21. package/dist/modules/src/misc.d.ts.map +1 -0
  22. package/dist/modules/src/misc.js +255 -0
  23. package/dist/modules/src/video-decoder.d.ts +30 -0
  24. package/dist/modules/src/video-decoder.d.ts.map +1 -0
  25. package/dist/modules/src/video-decoder.js +214 -0
  26. package/dist/modules/src/video-encoder.d.ts +35 -0
  27. package/dist/modules/src/video-encoder.d.ts.map +1 -0
  28. package/dist/modules/src/video-encoder.js +474 -0
  29. package/dist/modules/src/video-sample.d.ts +45 -0
  30. package/dist/modules/src/video-sample.d.ts.map +1 -0
  31. package/dist/modules/src/video-sample.js +276 -0
  32. package/dist/modules/tsconfig.tsbuildinfo +1 -0
  33. package/package.json +59 -0
  34. package/src/audio-decoder.ts +120 -0
  35. package/src/audio-encoder.ts +396 -0
  36. package/src/audio-sample.ts +101 -0
  37. package/src/index.ts +104 -0
  38. package/src/misc.ts +242 -0
  39. package/src/video-decoder.ts +224 -0
  40. package/src/video-encoder.ts +568 -0
  41. 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"}