node-av 6.2.0-beta.3 → 6.2.0-beta.30
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 +13 -8
- package/dist/api/audio-frame-buffer.d.ts +438 -7
- package/dist/api/audio-frame-buffer.js +792 -52
- package/dist/api/audio-frame-buffer.js.map +1 -1
- package/dist/api/bitstream-filter.d.ts +82 -4
- package/dist/api/bitstream-filter.js +197 -100
- package/dist/api/bitstream-filter.js.map +1 -1
- package/dist/api/decoder.d.ts +5 -0
- package/dist/api/decoder.js +13 -6
- package/dist/api/decoder.js.map +1 -1
- package/dist/api/demuxer.d.ts +29 -4
- package/dist/api/demuxer.js +209 -60
- package/dist/api/demuxer.js.map +1 -1
- package/dist/api/encoder-pool.d.ts +43 -1
- package/dist/api/encoder-pool.js +121 -9
- package/dist/api/encoder-pool.js.map +1 -1
- package/dist/api/encoder.d.ts +58 -2
- package/dist/api/encoder.js +173 -57
- package/dist/api/encoder.js.map +1 -1
- package/dist/api/filter.js +15 -6
- package/dist/api/filter.js.map +1 -1
- package/dist/api/fmp4-stream.d.ts +327 -8
- package/dist/api/fmp4-stream.js +663 -123
- package/dist/api/fmp4-stream.js.map +1 -1
- package/dist/api/hardware.d.ts +2 -0
- package/dist/api/hardware.js +25 -4
- package/dist/api/hardware.js.map +1 -1
- package/dist/api/index.d.ts +2 -2
- package/dist/api/index.js.map +1 -1
- package/dist/api/io-stream.d.ts +3 -2
- package/dist/api/io-stream.js +76 -5
- package/dist/api/io-stream.js.map +1 -1
- package/dist/api/muxer.d.ts +289 -7
- package/dist/api/muxer.js +696 -109
- package/dist/api/muxer.js.map +1 -1
- package/dist/api/pipeline.d.ts +24 -0
- package/dist/api/pipeline.js +76 -5
- package/dist/api/pipeline.js.map +1 -1
- package/dist/api/rtp-stream.d.ts +77 -8
- package/dist/api/rtp-stream.js +316 -88
- package/dist/api/rtp-stream.js.map +1 -1
- package/dist/api/scaler.d.ts +124 -21
- package/dist/api/scaler.js +347 -114
- package/dist/api/scaler.js.map +1 -1
- package/dist/api/utilities/async-queue.d.ts +3 -0
- package/dist/api/utilities/async-queue.js +8 -0
- package/dist/api/utilities/async-queue.js.map +1 -1
- package/dist/api/utilities/packet-age-watchdog.d.ts +198 -0
- package/dist/api/utilities/packet-age-watchdog.js +594 -0
- package/dist/api/utilities/packet-age-watchdog.js.map +1 -0
- package/dist/api/webrtc-stream.d.ts +2 -0
- package/dist/api/webrtc-stream.js +46 -12
- package/dist/api/webrtc-stream.js.map +1 -1
- package/dist/api/whisper.d.ts +2 -0
- package/dist/api/whisper.js +63 -23
- package/dist/api/whisper.js.map +1 -1
- package/dist/constants/constants.d.ts +1 -0
- package/dist/constants/constants.js +1 -0
- package/dist/constants/constants.js.map +1 -1
- package/dist/constants/filter-options.d.ts +2 -0
- package/dist/ffmpeg/install.js +104 -26
- package/dist/ffmpeg/install.js.map +1 -1
- package/dist/ffmpeg/utils.js +7 -4
- package/dist/ffmpeg/utils.js.map +1 -1
- package/dist/lib/binding.d.ts +10 -1
- package/dist/lib/binding.js +25 -16
- package/dist/lib/binding.js.map +1 -1
- package/dist/lib/bitstream-filter-context.d.ts +2 -0
- package/dist/lib/bitstream-filter-context.js +2 -0
- package/dist/lib/bitstream-filter-context.js.map +1 -1
- package/dist/lib/codec-context.d.ts +0 -2
- package/dist/lib/codec-context.js +4 -22
- package/dist/lib/codec-context.js.map +1 -1
- package/dist/lib/filter-context.d.ts +0 -1
- package/dist/lib/filter-context.js +2 -11
- package/dist/lib/filter-context.js.map +1 -1
- package/dist/lib/format-context.d.ts +70 -7
- package/dist/lib/format-context.js +77 -21
- package/dist/lib/format-context.js.map +1 -1
- package/dist/lib/frame.d.ts +17 -13
- package/dist/lib/frame.js +24 -23
- package/dist/lib/frame.js.map +1 -1
- package/dist/lib/hardware-frames-context.d.ts +0 -1
- package/dist/lib/hardware-frames-context.js +2 -8
- package/dist/lib/hardware-frames-context.js.map +1 -1
- package/dist/lib/io-context.d.ts +4 -2
- package/dist/lib/io-context.js +4 -2
- package/dist/lib/io-context.js.map +1 -1
- package/dist/lib/native-types.d.ts +7 -5
- package/dist/lib/option.d.ts +26 -0
- package/dist/lib/option.js +42 -18
- package/dist/lib/option.js.map +1 -1
- package/dist/lib/packet.d.ts +5 -0
- package/dist/lib/packet.js +5 -0
- package/dist/lib/packet.js.map +1 -1
- package/dist/lib/stream.d.ts +8 -4
- package/dist/lib/stream.js +22 -24
- package/dist/lib/stream.js.map +1 -1
- package/install/check.js +40 -79
- package/package.json +45 -34
- package/build_mac_local.sh +0 -69
package/dist/api/demuxer.js
CHANGED
|
@@ -55,7 +55,7 @@ import { closeSync, openSync, readSync } from 'fs';
|
|
|
55
55
|
import { open } from 'fs/promises';
|
|
56
56
|
import { Readable } from 'node:stream';
|
|
57
57
|
import { resolve } from 'path';
|
|
58
|
-
import { AV_NOPTS_VALUE, AV_PIX_FMT_NONE, AV_ROUND_NEAR_INF, AV_ROUND_PASS_MINMAX, AV_TIME_BASE, AV_TIME_BASE_Q, AVFLAG_NONE, AVFMT_FLAG_CUSTOM_IO, AVFMT_FLAG_NONBLOCK, AVFMT_TS_DISCONT, AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_VIDEO, AVSEEK_CUR, AVSEEK_END, AVSEEK_SET, } from '../constants/constants.js';
|
|
58
|
+
import { AV_NOPTS_VALUE, AV_PIX_FMT_NONE, AV_ROUND_NEAR_INF, AV_ROUND_PASS_MINMAX, AV_TIME_BASE, AV_TIME_BASE_Q, AVERROR_EOF, AVERROR_EXIT, AVFLAG_NONE, AVFMT_FLAG_CUSTOM_IO, AVFMT_FLAG_NONBLOCK, AVFMT_TS_DISCONT, AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_VIDEO, AVSEEK_CUR, AVSEEK_END, AVSEEK_SET, } from '../constants/constants.js';
|
|
59
59
|
import { Dictionary } from '../lib/dictionary.js';
|
|
60
60
|
import { AVERROR_EAGAIN, FFmpegError } from '../lib/error.js';
|
|
61
61
|
import { FormatContext } from '../lib/format-context.js';
|
|
@@ -67,6 +67,9 @@ import { avGetPixFmtName, avGetSampleFmtName, avRescaleQ, avRescaleQRnd, dtsPred
|
|
|
67
67
|
import { DELTA_THRESHOLD, DTS_ERROR_THRESHOLD, IO_BUFFER_SIZE, MAX_INPUT_QUEUE_SIZE } from './constants.js';
|
|
68
68
|
import { IOStream } from './io-stream.js';
|
|
69
69
|
import { StreamingUtils } from './utilities/streaming.js';
|
|
70
|
+
// Scratch buffer for Atomics.wait() - gives packetsSync a blocking sleep for
|
|
71
|
+
// EAGAIN retries (mirrors FFmpeg CLI's av_usleep()) without spinning the CPU.
|
|
72
|
+
const SYNC_SLEEP_SIGNAL = new Int32Array(new SharedArrayBuffer(4));
|
|
70
73
|
/**
|
|
71
74
|
* High-level demuxer for reading and demuxing media files.
|
|
72
75
|
*
|
|
@@ -110,6 +113,7 @@ export class Demuxer {
|
|
|
110
113
|
formatContext;
|
|
111
114
|
_streams = [];
|
|
112
115
|
ioContext;
|
|
116
|
+
externalIO = false; // ioContext was supplied by the caller - may own a protocol handle from open2()
|
|
113
117
|
isClosed = false;
|
|
114
118
|
options;
|
|
115
119
|
// Timestamp processing state (per-stream)
|
|
@@ -122,9 +126,11 @@ export class Demuxer {
|
|
|
122
126
|
demuxThread = null;
|
|
123
127
|
packetQueues = new Map(); // streamIndex or 'all' -> queue
|
|
124
128
|
packetQueueConsumers = new Map(); // active consumer count per queue key
|
|
125
|
-
queueResolvers = new Map(); // Promise resolvers for waiting consumers
|
|
129
|
+
queueResolvers = new Map(); // Promise resolvers for waiting consumers (multiple per key)
|
|
130
|
+
drainResolver = null; // Promise resolver for the demux thread parked on a full queue
|
|
126
131
|
demuxThreadActive = false;
|
|
127
132
|
demuxEof = false;
|
|
133
|
+
lastError = null; // read error that ended the demux loop (null = clean EOF)
|
|
128
134
|
signal;
|
|
129
135
|
signalCleanup;
|
|
130
136
|
/**
|
|
@@ -134,11 +140,14 @@ export class Demuxer {
|
|
|
134
140
|
*
|
|
135
141
|
* @param ioContext - Optional IO context for custom I/O (e.g., from Buffer)
|
|
136
142
|
*
|
|
143
|
+
* @param externalIO - Whether ioContext was supplied by the caller instead of built here
|
|
144
|
+
*
|
|
137
145
|
* @internal
|
|
138
146
|
*/
|
|
139
|
-
constructor(formatContext, options, ioContext) {
|
|
147
|
+
constructor(formatContext, options, ioContext, externalIO = false) {
|
|
140
148
|
this.formatContext = formatContext;
|
|
141
149
|
this.ioContext = ioContext;
|
|
150
|
+
this.externalIO = externalIO;
|
|
142
151
|
this._streams = formatContext.streams ?? [];
|
|
143
152
|
this.options = options;
|
|
144
153
|
}
|
|
@@ -339,8 +348,17 @@ export class Demuxer {
|
|
|
339
348
|
// Original implementation for non-raw data
|
|
340
349
|
const formatContext = new FormatContext();
|
|
341
350
|
let ioContext;
|
|
351
|
+
let externalIO = false; // caller-supplied context - released via dispose, not freeContext
|
|
342
352
|
let optionsDict = null;
|
|
343
353
|
let inputFormat = null;
|
|
354
|
+
// Abort during the open phase: avformat_open_input()/find_stream_info()
|
|
355
|
+
// block for seconds on an unresponsive network source, so the signal must
|
|
356
|
+
// fire the native interrupt callback to unblock them.
|
|
357
|
+
const onOpenAbort = () => formatContext.interrupt();
|
|
358
|
+
if (options.signal) {
|
|
359
|
+
options.signal.throwIfAborted();
|
|
360
|
+
options.signal.addEventListener('abort', onOpenAbort, { once: true });
|
|
361
|
+
}
|
|
344
362
|
try {
|
|
345
363
|
// Create options dictionary if options are provided
|
|
346
364
|
if (options.options) {
|
|
@@ -381,6 +399,7 @@ export class Demuxer {
|
|
|
381
399
|
}
|
|
382
400
|
formatContext.allocContext();
|
|
383
401
|
ioContext = input;
|
|
402
|
+
externalIO = true;
|
|
384
403
|
formatContext.pb = ioContext;
|
|
385
404
|
formatContext.setFlags(AVFMT_FLAG_CUSTOM_IO);
|
|
386
405
|
const ret = await formatContext.openInput('', inputFormat, optionsDict);
|
|
@@ -454,20 +473,43 @@ export class Demuxer {
|
|
|
454
473
|
copyTs: options.copyTs ?? false,
|
|
455
474
|
options: options.options ?? {},
|
|
456
475
|
};
|
|
457
|
-
const demuxer = new Demuxer(formatContext, fullOptions, ioContext);
|
|
476
|
+
const demuxer = new Demuxer(formatContext, fullOptions, ioContext, externalIO);
|
|
458
477
|
if (options.signal) {
|
|
459
|
-
options.signal
|
|
460
|
-
|
|
478
|
+
const signal = options.signal;
|
|
479
|
+
signal.removeEventListener('abort', onOpenAbort);
|
|
480
|
+
demuxer.signal = signal;
|
|
481
|
+
// An abort that raced the successful open must not hand out a usable
|
|
482
|
+
// demuxer - tear it down and surface the abort instead.
|
|
483
|
+
if (signal.aborted) {
|
|
484
|
+
await demuxer.close();
|
|
485
|
+
signal.throwIfAborted();
|
|
486
|
+
}
|
|
487
|
+
// Keep abort -> interrupt wired for the demuxer's lifetime so a
|
|
488
|
+
// blocked av_read_frame() unwinds too; close() removes the listener.
|
|
489
|
+
const onAbort = () => {
|
|
490
|
+
demuxer.demuxThreadActive = false;
|
|
491
|
+
demuxer.interrupt();
|
|
492
|
+
};
|
|
493
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
494
|
+
demuxer.signalCleanup = () => signal.removeEventListener('abort', onAbort);
|
|
461
495
|
}
|
|
462
496
|
return demuxer;
|
|
463
497
|
}
|
|
464
498
|
catch (error) {
|
|
499
|
+
options.signal?.removeEventListener('abort', onOpenAbort);
|
|
465
500
|
// Clean up only on error
|
|
466
501
|
if (ioContext) {
|
|
467
502
|
// Clear the pb reference first
|
|
468
503
|
formatContext.pb = null;
|
|
469
|
-
//
|
|
470
|
-
|
|
504
|
+
// Release the IOContext (for both custom I/O and buffer-based I/O).
|
|
505
|
+
// A caller-supplied context may own a protocol handle from open2(),
|
|
506
|
+
// which only dispose closes - see close().
|
|
507
|
+
if (externalIO) {
|
|
508
|
+
await ioContext[Symbol.asyncDispose]();
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
ioContext.freeContext();
|
|
512
|
+
}
|
|
471
513
|
}
|
|
472
514
|
// Clean up FormatContext
|
|
473
515
|
await formatContext.closeInput();
|
|
@@ -513,6 +555,7 @@ export class Demuxer {
|
|
|
513
555
|
// Original implementation for non-raw data
|
|
514
556
|
const formatContext = new FormatContext();
|
|
515
557
|
let ioContext;
|
|
558
|
+
let externalIO = false; // caller-supplied context - released via dispose, not freeContext
|
|
516
559
|
let optionsDict = null;
|
|
517
560
|
let inputFormat = null;
|
|
518
561
|
try {
|
|
@@ -555,6 +598,7 @@ export class Demuxer {
|
|
|
555
598
|
}
|
|
556
599
|
formatContext.allocContext();
|
|
557
600
|
ioContext = input;
|
|
601
|
+
externalIO = true;
|
|
558
602
|
formatContext.pb = ioContext;
|
|
559
603
|
formatContext.setFlags(AVFMT_FLAG_CUSTOM_IO);
|
|
560
604
|
const ret = formatContext.openInputSync('', inputFormat, optionsDict);
|
|
@@ -616,7 +660,7 @@ export class Demuxer {
|
|
|
616
660
|
copyTs: options.copyTs ?? false,
|
|
617
661
|
options: options.options ?? {},
|
|
618
662
|
};
|
|
619
|
-
const demuxer = new Demuxer(formatContext, fullOptions, ioContext);
|
|
663
|
+
const demuxer = new Demuxer(formatContext, fullOptions, ioContext, externalIO);
|
|
620
664
|
if (options.signal) {
|
|
621
665
|
options.signal.throwIfAborted();
|
|
622
666
|
demuxer.signal = options.signal;
|
|
@@ -628,8 +672,13 @@ export class Demuxer {
|
|
|
628
672
|
if (ioContext) {
|
|
629
673
|
// Clear the pb reference first
|
|
630
674
|
formatContext.pb = null;
|
|
631
|
-
//
|
|
632
|
-
|
|
675
|
+
// Release the IOContext (see open() for why dispose)
|
|
676
|
+
if (externalIO) {
|
|
677
|
+
ioContext[Symbol.dispose]();
|
|
678
|
+
}
|
|
679
|
+
else {
|
|
680
|
+
ioContext.freeContext();
|
|
681
|
+
}
|
|
633
682
|
}
|
|
634
683
|
// Clean up FormatContext
|
|
635
684
|
formatContext.closeInputSync();
|
|
@@ -1197,6 +1246,9 @@ export class Demuxer {
|
|
|
1197
1246
|
*
|
|
1198
1247
|
* @yields {Packet} Demuxed packets (must be freed by caller)
|
|
1199
1248
|
*
|
|
1249
|
+
* @throws {FFmpegError} If reading fails with an error other than EOF (e.g. I/O failure
|
|
1250
|
+
* or a dropped network source) - only a real end-of-file ends the stream cleanly
|
|
1251
|
+
*
|
|
1200
1252
|
* @throws {Error} If packet cloning fails
|
|
1201
1253
|
*
|
|
1202
1254
|
* @example
|
|
@@ -1253,42 +1305,64 @@ export class Demuxer {
|
|
|
1253
1305
|
this.packetQueueConsumers.set(queueKey, (this.packetQueueConsumers.get(queueKey) ?? 0) + 1);
|
|
1254
1306
|
// Always start demux thread (handles single and multiple generators)
|
|
1255
1307
|
this.startDemuxThread();
|
|
1256
|
-
let aborted = false;
|
|
1257
1308
|
try {
|
|
1258
1309
|
let hasSeenKeyframe = !this.options.startWithKeyframe;
|
|
1259
1310
|
// Read from queue (demux thread is handling av_read_frame)
|
|
1260
1311
|
const queue = this.packetQueues.get(queueKey);
|
|
1261
1312
|
while (!this.isClosed) {
|
|
1262
1313
|
if (this.signal?.aborted) {
|
|
1263
|
-
aborted = true;
|
|
1264
1314
|
break;
|
|
1265
1315
|
}
|
|
1266
1316
|
// Try to get packet from queue
|
|
1267
1317
|
let packet = queue.shift();
|
|
1318
|
+
if (packet) {
|
|
1319
|
+
this.wakeDemuxThread();
|
|
1320
|
+
}
|
|
1268
1321
|
// If queue is empty, wait for next packet
|
|
1269
1322
|
if (!packet) {
|
|
1270
|
-
//
|
|
1323
|
+
// Queue drained - check for end of stream. Buffered packets always
|
|
1324
|
+
// drain before EOF/error is honored, so successfully read data is
|
|
1325
|
+
// never dropped.
|
|
1271
1326
|
if (this.demuxEof) {
|
|
1327
|
+
// The read loop ended with a real error (not EOF): propagate it
|
|
1328
|
+
// instead of ending the stream silently - a dead live source must
|
|
1329
|
+
// be distinguishable from a finished file.
|
|
1330
|
+
if (this.lastError) {
|
|
1331
|
+
throw this.lastError;
|
|
1332
|
+
}
|
|
1272
1333
|
break; // End of stream
|
|
1273
1334
|
}
|
|
1274
|
-
// Create promise and register resolver
|
|
1335
|
+
// Create promise and register resolver. Multiple consumers may wait
|
|
1336
|
+
// on the same queue key concurrently, so resolvers are collected per
|
|
1337
|
+
// key - a single slot would be overwritten by the second consumer,
|
|
1338
|
+
// parking the first one forever (whole-demuxer deadlock).
|
|
1275
1339
|
const { promise, resolve } = Promise.withResolvers();
|
|
1276
|
-
this.queueResolvers.
|
|
1340
|
+
const resolvers = this.queueResolvers.get(queueKey);
|
|
1341
|
+
if (resolvers) {
|
|
1342
|
+
resolvers.push(resolve);
|
|
1343
|
+
}
|
|
1344
|
+
else {
|
|
1345
|
+
this.queueResolvers.set(queueKey, [resolve]);
|
|
1346
|
+
}
|
|
1277
1347
|
// Wait for demux thread to add packet
|
|
1278
1348
|
await promise;
|
|
1279
1349
|
// Check for abort after wakeup
|
|
1280
1350
|
if (this.signal?.aborted) {
|
|
1281
|
-
aborted = true;
|
|
1282
|
-
break;
|
|
1283
|
-
}
|
|
1284
|
-
// Check again after wakeup
|
|
1285
|
-
if (this.demuxEof) {
|
|
1286
1351
|
break;
|
|
1287
1352
|
}
|
|
1288
1353
|
packet = queue.shift();
|
|
1289
1354
|
if (!packet) {
|
|
1355
|
+
// Woken without a packet for us - either EOF/error or another
|
|
1356
|
+
// consumer grabbed it. Loop back and re-evaluate.
|
|
1357
|
+
if (this.demuxEof) {
|
|
1358
|
+
if (this.lastError) {
|
|
1359
|
+
throw this.lastError;
|
|
1360
|
+
}
|
|
1361
|
+
break;
|
|
1362
|
+
}
|
|
1290
1363
|
continue;
|
|
1291
1364
|
}
|
|
1365
|
+
this.wakeDemuxThread();
|
|
1292
1366
|
}
|
|
1293
1367
|
// Apply keyframe filtering if needed
|
|
1294
1368
|
if (!hasSeenKeyframe) {
|
|
@@ -1323,6 +1397,8 @@ export class Demuxer {
|
|
|
1323
1397
|
p.free();
|
|
1324
1398
|
}
|
|
1325
1399
|
}
|
|
1400
|
+
// The demux thread may be waiting on this queue, so wake it to check the remaining ones.
|
|
1401
|
+
this.wakeDemuxThread();
|
|
1326
1402
|
}
|
|
1327
1403
|
else {
|
|
1328
1404
|
this.packetQueueConsumers.set(queueKey, remaining);
|
|
@@ -1331,13 +1407,13 @@ export class Demuxer {
|
|
|
1331
1407
|
if (this.activeGenerators === 0) {
|
|
1332
1408
|
await this.stopDemuxThread();
|
|
1333
1409
|
}
|
|
1334
|
-
if (!aborted) {
|
|
1335
|
-
yield null; // Signal EOF
|
|
1336
|
-
}
|
|
1337
1410
|
}
|
|
1338
|
-
//
|
|
1339
|
-
//
|
|
1411
|
+
// Only reached when the loop ended naturally (EOF, abort, close). A consumer
|
|
1412
|
+
// break resumes inside the finally above and completes the generator there -
|
|
1413
|
+
// yielding from the finally would suspend forever inside iterator.return(),
|
|
1414
|
+
// violating the iterator protocol.
|
|
1340
1415
|
this.signal?.throwIfAborted();
|
|
1416
|
+
yield null; // Signal EOF
|
|
1341
1417
|
}
|
|
1342
1418
|
/**
|
|
1343
1419
|
* Read packets from media as generator synchronously.
|
|
@@ -1353,6 +1429,9 @@ export class Demuxer {
|
|
|
1353
1429
|
*
|
|
1354
1430
|
* @yields {Packet} Demuxed packets (must be freed by caller)
|
|
1355
1431
|
*
|
|
1432
|
+
* @throws {FFmpegError} If reading fails with an error other than EOF (e.g. I/O failure
|
|
1433
|
+
* or a dropped network source) - only a real end-of-file ends the stream cleanly
|
|
1434
|
+
*
|
|
1356
1435
|
* @throws {Error} If packet cloning fails
|
|
1357
1436
|
*
|
|
1358
1437
|
* @example
|
|
@@ -1385,6 +1464,16 @@ export class Demuxer {
|
|
|
1385
1464
|
while (!this.isClosed) {
|
|
1386
1465
|
const ret = this.formatContext.readFrameSync(packet);
|
|
1387
1466
|
if (ret < 0) {
|
|
1467
|
+
// No data available yet (live device capture) - blocking retry
|
|
1468
|
+
if (ret === AVERROR_EAGAIN) {
|
|
1469
|
+
Atomics.wait(SYNC_SLEEP_SIGNAL, 0, 0, 10);
|
|
1470
|
+
continue;
|
|
1471
|
+
}
|
|
1472
|
+
// Real read errors propagate; end-of-file and a deliberate
|
|
1473
|
+
// interrupt() (AVERROR_EXIT) end cleanly
|
|
1474
|
+
if (!FFmpegError.is(ret, AVERROR_EOF) && !FFmpegError.is(ret, AVERROR_EXIT)) {
|
|
1475
|
+
throw new FFmpegError(ret);
|
|
1476
|
+
}
|
|
1388
1477
|
break;
|
|
1389
1478
|
}
|
|
1390
1479
|
// Get stream for timestamp processing
|
|
@@ -1531,13 +1620,26 @@ export class Demuxer {
|
|
|
1531
1620
|
const ts = BigInt(Math.floor(timestamp * 1000000));
|
|
1532
1621
|
return this.formatContext.seekFrameSync(streamIndex, ts, flags);
|
|
1533
1622
|
}
|
|
1623
|
+
/**
|
|
1624
|
+
* Wake the demux thread if it is parked waiting for a full queue to drain.
|
|
1625
|
+
*
|
|
1626
|
+
* @internal
|
|
1627
|
+
*/
|
|
1628
|
+
wakeDemuxThread() {
|
|
1629
|
+
const resolve = this.drainResolver;
|
|
1630
|
+
if (resolve) {
|
|
1631
|
+
this.drainResolver = null;
|
|
1632
|
+
resolve();
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1534
1635
|
/**
|
|
1535
1636
|
* Interrupt a blocking read without closing the demuxer.
|
|
1536
1637
|
*
|
|
1537
1638
|
* Aborts any in-progress `av_read_frame()` (e.g. on a quiet RTSP/network source
|
|
1538
|
-
* that is waiting for data) and signals
|
|
1539
|
-
* an active pipeline can drain and finish.
|
|
1540
|
-
* streams stay valid - call {@link close}
|
|
1639
|
+
* or a stalled Readable/custom I/O source that is waiting for data) and signals
|
|
1640
|
+
* end-of-stream to packet consumers, so an active pipeline can drain and finish.
|
|
1641
|
+
* The demuxer is not freed and its streams stay valid - call {@link close}
|
|
1642
|
+
* afterwards to release resources.
|
|
1541
1643
|
*
|
|
1542
1644
|
* Mainly used by pipeline teardown: a blocking read only unblocks on close, but
|
|
1543
1645
|
* closing while the pipeline is still draining would free the context too early,
|
|
@@ -1561,8 +1663,11 @@ export class Demuxer {
|
|
|
1561
1663
|
this.formatContext.interrupt();
|
|
1562
1664
|
// Signal EOF to consumers and wake any generator parked on an empty queue.
|
|
1563
1665
|
this.demuxEof = true;
|
|
1564
|
-
|
|
1565
|
-
|
|
1666
|
+
this.wakeDemuxThread();
|
|
1667
|
+
for (const resolvers of this.queueResolvers.values()) {
|
|
1668
|
+
for (const resolve of resolvers) {
|
|
1669
|
+
resolve();
|
|
1670
|
+
}
|
|
1566
1671
|
}
|
|
1567
1672
|
this.queueResolvers.clear();
|
|
1568
1673
|
}
|
|
@@ -1610,10 +1715,21 @@ export class Demuxer {
|
|
|
1610
1715
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1611
1716
|
continue;
|
|
1612
1717
|
}
|
|
1613
|
-
//
|
|
1718
|
+
// Anything but a genuine end-of-file is a real read error (I/O
|
|
1719
|
+
// failure, dropped network source, ...). Store it so consumers throw
|
|
1720
|
+
// instead of seeing a clean EOF - matching FFmpeg CLI, where a read
|
|
1721
|
+
// error ends processing with a failure, not a normal finish.
|
|
1722
|
+
// AVERROR_EXIT is exempt: it is how interrupt() aborts a blocking
|
|
1723
|
+
// read on purpose (pipeline stop/teardown), not a source failure.
|
|
1724
|
+
if (!FFmpegError.is(ret, AVERROR_EOF) && !FFmpegError.is(ret, AVERROR_EXIT)) {
|
|
1725
|
+
this.lastError = new FFmpegError(ret);
|
|
1726
|
+
}
|
|
1727
|
+
// End of stream (or error) - notify all waiting consumers
|
|
1614
1728
|
this.demuxEof = true;
|
|
1615
|
-
for (const
|
|
1616
|
-
resolve
|
|
1729
|
+
for (const resolvers of this.queueResolvers.values()) {
|
|
1730
|
+
for (const resolve of resolvers) {
|
|
1731
|
+
resolve();
|
|
1732
|
+
}
|
|
1617
1733
|
}
|
|
1618
1734
|
this.queueResolvers.clear();
|
|
1619
1735
|
break;
|
|
@@ -1678,7 +1794,10 @@ export class Demuxer {
|
|
|
1678
1794
|
if (!queueFull) {
|
|
1679
1795
|
break;
|
|
1680
1796
|
}
|
|
1681
|
-
|
|
1797
|
+
// Wait until a consumer frees a slot or the demuxer shuts down. Polling here would keep a CPU core busy.
|
|
1798
|
+
const { promise: drained, resolve: onDrain } = Promise.withResolvers();
|
|
1799
|
+
this.drainResolver = onDrain;
|
|
1800
|
+
await drained;
|
|
1682
1801
|
}
|
|
1683
1802
|
if (abandoned || !this.demuxThreadActive || this.isClosed) {
|
|
1684
1803
|
packet.unref();
|
|
@@ -1700,10 +1819,14 @@ export class Demuxer {
|
|
|
1700
1819
|
throw new Error('Failed to clone packet in demux thread (out of memory)');
|
|
1701
1820
|
}
|
|
1702
1821
|
target.queue.push(clone);
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1822
|
+
// Wake every consumer waiting on this key - only one gets the packet,
|
|
1823
|
+
// the others re-park, but a lost wakeup would deadlock them.
|
|
1824
|
+
const resolvers = this.queueResolvers.get(target.key);
|
|
1825
|
+
if (resolvers) {
|
|
1706
1826
|
this.queueResolvers.delete(target.key);
|
|
1827
|
+
for (const resolve of resolvers) {
|
|
1828
|
+
resolve();
|
|
1829
|
+
}
|
|
1707
1830
|
}
|
|
1708
1831
|
}
|
|
1709
1832
|
packet.unref();
|
|
@@ -1727,9 +1850,12 @@ export class Demuxer {
|
|
|
1727
1850
|
async stopDemuxThread() {
|
|
1728
1851
|
this.demuxThreadActive = false;
|
|
1729
1852
|
this.demuxEof = true;
|
|
1853
|
+
this.wakeDemuxThread();
|
|
1730
1854
|
// Wake up any waiting generators
|
|
1731
|
-
for (const
|
|
1732
|
-
resolve
|
|
1855
|
+
for (const resolvers of this.queueResolvers.values()) {
|
|
1856
|
+
for (const resolve of resolvers) {
|
|
1857
|
+
resolve();
|
|
1858
|
+
}
|
|
1733
1859
|
}
|
|
1734
1860
|
this.queueResolvers.clear();
|
|
1735
1861
|
// Wait for demux thread with timeout to avoid hanging on blocked reads
|
|
@@ -1993,6 +2119,8 @@ export class Demuxer {
|
|
|
1993
2119
|
* Close demuxer and free resources.
|
|
1994
2120
|
*
|
|
1995
2121
|
* Releases format context and I/O context.
|
|
2122
|
+
* A read waiting for a stalled Readable or custom I/O source is abandoned,
|
|
2123
|
+
* so closing never waits for the source to deliver.
|
|
1996
2124
|
* Safe to call multiple times.
|
|
1997
2125
|
* Automatically called by Symbol.asyncDispose.
|
|
1998
2126
|
*
|
|
@@ -2022,18 +2150,19 @@ export class Demuxer {
|
|
|
2022
2150
|
this.demuxThreadActive = false;
|
|
2023
2151
|
// Set EOF flag so generators know to exit
|
|
2024
2152
|
this.demuxEof = true;
|
|
2153
|
+
this.wakeDemuxThread();
|
|
2025
2154
|
// Wake up all waiting generators BEFORE closing format context
|
|
2026
2155
|
// This ensures generators can exit cleanly even if readFrame() is blocking
|
|
2027
|
-
for (const
|
|
2028
|
-
resolve
|
|
2156
|
+
for (const resolvers of this.queueResolvers.values()) {
|
|
2157
|
+
for (const resolve of resolvers) {
|
|
2158
|
+
resolve();
|
|
2159
|
+
}
|
|
2029
2160
|
}
|
|
2030
2161
|
this.queueResolvers.clear();
|
|
2031
|
-
//
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
// Close FormatContext - this may interrupt blocking readFrame()
|
|
2036
|
-
await this.formatContext.closeInput();
|
|
2162
|
+
// Close FormatContext - this may interrupt blocking readFrame(). A custom
|
|
2163
|
+
// pb is detached inside, after the reader thread stopped: nulling it from
|
|
2164
|
+
// here would pull it out from under a running av_read_frame()
|
|
2165
|
+
await this.formatContext.closeInput(!!this.ioContext);
|
|
2037
2166
|
// Wait for demux thread with timeout to avoid hanging on blocked reads
|
|
2038
2167
|
if (this.demuxThread) {
|
|
2039
2168
|
const threadPromise = this.demuxThread;
|
|
@@ -2055,9 +2184,18 @@ export class Demuxer {
|
|
|
2055
2184
|
}
|
|
2056
2185
|
this.packetQueues.clear();
|
|
2057
2186
|
this.packetQueueConsumers.clear();
|
|
2058
|
-
// NOW we can safely
|
|
2187
|
+
// NOW we can safely release the IOContext. Contexts we built ourselves are
|
|
2188
|
+
// callback-backed and go through freeContext(), which IOStream hooks to
|
|
2189
|
+
// detach its stream listeners. A caller-supplied context may instead own a
|
|
2190
|
+
// protocol handle from open2(); dispose picks avio_closep() for those, as
|
|
2191
|
+
// freeing them alone leaks the handle and keeps the file locked on Windows.
|
|
2059
2192
|
if (this.ioContext) {
|
|
2060
|
-
this.
|
|
2193
|
+
if (this.externalIO) {
|
|
2194
|
+
await this.ioContext[Symbol.asyncDispose]();
|
|
2195
|
+
}
|
|
2196
|
+
else {
|
|
2197
|
+
this.ioContext.freeContext();
|
|
2198
|
+
}
|
|
2061
2199
|
this.ioContext = undefined;
|
|
2062
2200
|
}
|
|
2063
2201
|
}
|
|
@@ -2066,6 +2204,7 @@ export class Demuxer {
|
|
|
2066
2204
|
* Synchronous version of close.
|
|
2067
2205
|
*
|
|
2068
2206
|
* Releases format context and I/O context.
|
|
2207
|
+
* A read waiting for a stalled Readable or custom I/O source is abandoned.
|
|
2069
2208
|
* Safe to call multiple times.
|
|
2070
2209
|
* Automatically called by Symbol.dispose.
|
|
2071
2210
|
*
|
|
@@ -2091,13 +2230,19 @@ export class Demuxer {
|
|
|
2091
2230
|
// Clean up abort signal listener
|
|
2092
2231
|
this.signalCleanup?.();
|
|
2093
2232
|
this.signalCleanup = undefined;
|
|
2094
|
-
//
|
|
2095
|
-
|
|
2096
|
-
this.formatContext.pb = null;
|
|
2097
|
-
}
|
|
2098
|
-
// Close FormatContext
|
|
2099
|
-
this.formatContext.closeInputSync();
|
|
2233
|
+
// Stop the demux thread and wake generators parked on an empty queue: they
|
|
2234
|
+
// resume once this returns and end, as in close()
|
|
2100
2235
|
this.demuxThreadActive = false;
|
|
2236
|
+
this.demuxEof = true;
|
|
2237
|
+
this.wakeDemuxThread();
|
|
2238
|
+
for (const resolvers of this.queueResolvers.values()) {
|
|
2239
|
+
for (const resolve of resolvers) {
|
|
2240
|
+
resolve();
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
this.queueResolvers.clear();
|
|
2244
|
+
// Close FormatContext; a custom pb is detached inside so it is not closed with the input
|
|
2245
|
+
this.formatContext.closeInputSync(!!this.ioContext);
|
|
2101
2246
|
for (const queue of this.packetQueues.values()) {
|
|
2102
2247
|
for (const packet of queue) {
|
|
2103
2248
|
packet.free();
|
|
@@ -2105,11 +2250,15 @@ export class Demuxer {
|
|
|
2105
2250
|
queue.length = 0;
|
|
2106
2251
|
}
|
|
2107
2252
|
this.packetQueues.clear();
|
|
2108
|
-
this.
|
|
2109
|
-
|
|
2110
|
-
// NOW we can safely free the IOContext
|
|
2253
|
+
this.packetQueueConsumers.clear();
|
|
2254
|
+
// NOW we can safely release the IOContext (see close() for why dispose)
|
|
2111
2255
|
if (this.ioContext) {
|
|
2112
|
-
this.
|
|
2256
|
+
if (this.externalIO) {
|
|
2257
|
+
this.ioContext[Symbol.dispose]();
|
|
2258
|
+
}
|
|
2259
|
+
else {
|
|
2260
|
+
this.ioContext.freeContext();
|
|
2261
|
+
}
|
|
2113
2262
|
this.ioContext = undefined;
|
|
2114
2263
|
}
|
|
2115
2264
|
}
|