node-av 6.2.0-beta.9 → 7.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.md +7 -19
  2. package/dist/api/audio-frame-buffer.d.ts +378 -19
  3. package/dist/api/audio-frame-buffer.js +715 -101
  4. package/dist/api/audio-frame-buffer.js.map +1 -1
  5. package/dist/api/data/test_av1.ivf +0 -0
  6. package/dist/api/data/test_hevc.h265 +0 -0
  7. package/dist/api/data/test_vp9.ivf +0 -0
  8. package/dist/api/decoder.d.ts +117 -2
  9. package/dist/api/decoder.js +290 -23
  10. package/dist/api/decoder.js.map +1 -1
  11. package/dist/api/demuxer.d.ts +19 -3
  12. package/dist/api/demuxer.js +95 -29
  13. package/dist/api/demuxer.js.map +1 -1
  14. package/dist/api/encoder-pool.js +9 -1
  15. package/dist/api/encoder-pool.js.map +1 -1
  16. package/dist/api/encoder.d.ts +100 -6
  17. package/dist/api/encoder.js +249 -108
  18. package/dist/api/encoder.js.map +1 -1
  19. package/dist/api/filter-presets.d.ts +25 -1
  20. package/dist/api/filter-presets.js +67 -5
  21. package/dist/api/filter-presets.js.map +1 -1
  22. package/dist/api/filter.d.ts +62 -3
  23. package/dist/api/filter.js +118 -36
  24. package/dist/api/filter.js.map +1 -1
  25. package/dist/api/fmp4-stream.d.ts +179 -15
  26. package/dist/api/fmp4-stream.js +341 -120
  27. package/dist/api/fmp4-stream.js.map +1 -1
  28. package/dist/api/hardware.d.ts +2 -0
  29. package/dist/api/hardware.js +33 -10
  30. package/dist/api/hardware.js.map +1 -1
  31. package/dist/api/index.d.ts +2 -2
  32. package/dist/api/index.js.map +1 -1
  33. package/dist/api/io-stream.d.ts +3 -2
  34. package/dist/api/io-stream.js +23 -7
  35. package/dist/api/io-stream.js.map +1 -1
  36. package/dist/api/muxer.d.ts +276 -7
  37. package/dist/api/muxer.js +649 -116
  38. package/dist/api/muxer.js.map +1 -1
  39. package/dist/api/pipeline.d.ts +22 -0
  40. package/dist/api/pipeline.js +53 -0
  41. package/dist/api/pipeline.js.map +1 -1
  42. package/dist/api/rtp-stream.d.ts +54 -33
  43. package/dist/api/rtp-stream.js +232 -153
  44. package/dist/api/rtp-stream.js.map +1 -1
  45. package/dist/api/scaler.d.ts +39 -6
  46. package/dist/api/scaler.js +72 -18
  47. package/dist/api/scaler.js.map +1 -1
  48. package/dist/api/utilities/live-decoding.d.ts +15 -0
  49. package/dist/api/utilities/live-decoding.js +32 -0
  50. package/dist/api/utilities/live-decoding.js.map +1 -0
  51. package/dist/api/utilities/packet-age-watchdog.d.ts +198 -0
  52. package/dist/api/utilities/packet-age-watchdog.js +594 -0
  53. package/dist/api/utilities/packet-age-watchdog.js.map +1 -0
  54. package/dist/api/utilities/scale-size.d.ts +51 -0
  55. package/dist/api/utilities/scale-size.js +55 -0
  56. package/dist/api/utilities/scale-size.js.map +1 -0
  57. package/dist/api/utilities/video-transcode.d.ts +218 -0
  58. package/dist/api/utilities/video-transcode.js +624 -0
  59. package/dist/api/utilities/video-transcode.js.map +1 -0
  60. package/dist/constants/bsf-options.d.ts +32 -8
  61. package/dist/constants/constants.d.ts +36 -114
  62. package/dist/constants/constants.js +32 -95
  63. package/dist/constants/constants.js.map +1 -1
  64. package/dist/constants/decoders.d.ts +74 -74
  65. package/dist/constants/decoders.js +79 -77
  66. package/dist/constants/decoders.js.map +1 -1
  67. package/dist/constants/encoders.d.ts +20 -21
  68. package/dist/constants/encoders.js +25 -24
  69. package/dist/constants/encoders.js.map +1 -1
  70. package/dist/constants/filter-options.d.ts +1378 -194
  71. package/dist/constants/format-options.d.ts +1342 -83
  72. package/dist/constants/formats.d.ts +2 -2
  73. package/dist/constants/options.d.ts +2261 -125
  74. package/dist/ffmpeg/utils.js +7 -4
  75. package/dist/ffmpeg/utils.js.map +1 -1
  76. package/dist/ffmpeg/version.d.ts +1 -1
  77. package/dist/ffmpeg/version.js +1 -1
  78. package/dist/ffmpeg/version.js.map +1 -1
  79. package/dist/lib/binding.d.ts +10 -1
  80. package/dist/lib/binding.js +25 -16
  81. package/dist/lib/binding.js.map +1 -1
  82. package/dist/lib/bitstream-filter-context.d.ts +2 -0
  83. package/dist/lib/bitstream-filter-context.js +2 -0
  84. package/dist/lib/bitstream-filter-context.js.map +1 -1
  85. package/dist/lib/filter-context.d.ts +3 -0
  86. package/dist/lib/filter-context.js +3 -0
  87. package/dist/lib/filter-context.js.map +1 -1
  88. package/dist/lib/filter-graph-segment.d.ts +3 -2
  89. package/dist/lib/filter-graph-segment.js +3 -2
  90. package/dist/lib/filter-graph-segment.js.map +1 -1
  91. package/dist/lib/filter-graph.d.ts +4 -4
  92. package/dist/lib/filter-graph.js +4 -4
  93. package/dist/lib/filter-inout.d.ts +4 -0
  94. package/dist/lib/filter-inout.js +4 -0
  95. package/dist/lib/filter-inout.js.map +1 -1
  96. package/dist/lib/format-context.d.ts +27 -3
  97. package/dist/lib/format-context.js +30 -6
  98. package/dist/lib/format-context.js.map +1 -1
  99. package/dist/lib/frame.d.ts +6 -2
  100. package/dist/lib/frame.js +11 -3
  101. package/dist/lib/frame.js.map +1 -1
  102. package/dist/lib/io-context.d.ts +4 -2
  103. package/dist/lib/io-context.js +4 -2
  104. package/dist/lib/io-context.js.map +1 -1
  105. package/dist/lib/native-types.d.ts +7 -3
  106. package/dist/lib/output-format.d.ts +42 -0
  107. package/dist/lib/output-format.js +46 -0
  108. package/dist/lib/output-format.js.map +1 -1
  109. package/dist/lib/stream.d.ts +5 -0
  110. package/dist/lib/stream.js +5 -0
  111. package/dist/lib/stream.js.map +1 -1
  112. package/dist/lib/utilities.d.ts +26 -0
  113. package/dist/lib/utilities.js +28 -0
  114. package/dist/lib/utilities.js.map +1 -1
  115. package/install/check.js +14 -16
  116. package/package.json +28 -44
@@ -50,21 +50,34 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
50
50
  var e = new Error(message);
51
51
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
52
  });
53
- import { AV_CHANNEL_ORDER_UNSPEC, AV_CODEC_FLAG_COPY_OPAQUE, AV_FRAME_FLAG_CORRUPT, AV_HWFRAME_TRANSFER_DIRECTION_FROM, AV_NOPTS_VALUE, AV_PIX_FMT_NONE, AV_ROUND_UP, AVERROR_DECODER_NOT_FOUND, AVERROR_EAGAIN, AVERROR_EOF, AVERROR_INVALIDDATA, AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_VIDEO, EOF, INT_MAX, SWS_BILINEAR, } from '../constants/constants.js';
53
+ import { AV_CHANNEL_ORDER_UNSPEC, AV_CODEC_FLAG_COPY_OPAQUE, AV_FRAME_FLAG_CORRUPT, AV_HWFRAME_TRANSFER_DIRECTION_FROM, AV_LOG_WARNING, AV_NOPTS_VALUE, AV_PIX_FMT_NONE, AV_ROUND_UP, AVERROR_DECODER_NOT_FOUND, AVERROR_EAGAIN, AVERROR_EOF, AVERROR_INVALIDDATA, AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_VIDEO, EOF, INT_MAX, SWS_BILINEAR, } from '../constants/constants.js';
54
54
  import { CodecContext } from '../lib/codec-context.js';
55
55
  import { Codec } from '../lib/codec.js';
56
56
  import { Dictionary } from '../lib/dictionary.js';
57
57
  import { FFmpegError } from '../lib/error.js';
58
58
  import { Frame } from '../lib/frame.js';
59
+ import { Log } from '../lib/log.js';
59
60
  import { Packet } from '../lib/packet.js';
60
61
  import { Rational } from '../lib/rational.js';
61
62
  import { SoftwareResampleContext } from '../lib/software-resample-context.js';
62
63
  import { SoftwareScaleContext } from '../lib/software-scale-context.js';
63
- import { avChannelLayoutDefault, avGcd, avInvQ, avMulQ, avRescaleDelta, avRescaleQ, avRescaleQRnd } from '../lib/utilities.js';
64
+ import { avChannelLayoutDefault, avGcd, avGuessFrameRate, avInvQ, avMulQ, avRescaleDelta, avRescaleQ, avRescaleQRnd } from '../lib/utilities.js';
64
65
  import { FRAME_THREAD_QUEUE_SIZE, PACKET_THREAD_QUEUE_SIZE } from './constants.js';
65
66
  import { AsyncQueue } from './utilities/async-queue.js';
66
67
  import { applyContextOptions } from './utilities/context-options.js';
67
68
  import { Scheduler } from './utilities/scheduler.js';
69
+ /**
70
+ * Packets a new hardware decoder has after its first keyframe, on top of its threads' delay, to produce a frame.
71
+ *
72
+ * @internal
73
+ */
74
+ const HW_DECODE_PROBE = 16;
75
+ /**
76
+ * Packets after the second silent keyframe in a row before a running hardware decoder counts as stuck.
77
+ *
78
+ * @internal
79
+ */
80
+ const HW_DECODE_DELAY = 8;
68
81
  /**
69
82
  * High-level decoder for audio and video streams.
70
83
  *
@@ -114,9 +127,14 @@ export class Decoder {
114
127
  stream;
115
128
  streamIndex;
116
129
  streamFramerate;
130
+ guessedFramerate;
117
131
  initialized = true;
118
132
  isClosed = false;
119
133
  options;
134
+ _decodeErrors = 0;
135
+ // Software fallback of a hardware decoder (hardwareFallback)
136
+ hardwareWatch;
137
+ replayedFrames = [];
120
138
  // Frame tracking for PTS/duration estimation
121
139
  lastFramePts = AV_NOPTS_VALUE;
122
140
  lastFrameDurationEst = 0n;
@@ -160,13 +178,19 @@ export class Decoder {
160
178
  this.codec = codec;
161
179
  this.stream = stream;
162
180
  this.streamIndex = stream.index;
163
- this.streamFramerate = stream.avgFrameRate ?? stream.rFrameRate;
181
+ const avgFramerate = stream.avgFrameRate;
182
+ this.streamFramerate = avgFramerate.num > 0 && avgFramerate.den > 0 ? avgFramerate : stream.rFrameRate;
183
+ // Taken now: the stream belongs to the demuxer and reads 0/1 once it closes
184
+ this.guessedFramerate = avGuessFrameRate(stream);
164
185
  this.options = options;
165
186
  this.frame = new Frame();
166
187
  this.frame.alloc();
167
188
  this.lastFrameTb = new Rational(0, 1);
168
189
  this.inputQueue = new AsyncQueue(PACKET_THREAD_QUEUE_SIZE, (p) => p.free());
169
190
  this.outputQueue = new AsyncQueue(FRAME_THREAD_QUEUE_SIZE, (f) => f.free());
191
+ if (options.hardwareFallback && this.isHardware() && codecContext.codecType === AVMEDIA_TYPE_VIDEO) {
192
+ this.hardwareWatch = { started: false, proven: false, framesSincePacket: 0, silent: 0, silentKeyframes: 0, sinceSecondKeyframe: 0, held: [] };
193
+ }
170
194
  }
171
195
  static async create(stream, optionsOrCodec, maybeOptions) {
172
196
  // Parse arguments
@@ -180,11 +204,11 @@ export class Decoder {
180
204
  ) {
181
205
  // First argument is a codec
182
206
  explicitCodec = optionsOrCodec;
183
- options = maybeOptions ?? {};
207
+ options = { ...maybeOptions };
184
208
  }
185
209
  else {
186
- // First argument is options
187
- options = optionsOrCodec;
210
+ // First argument is options (copied: create() fills in defaults)
211
+ options = { ...optionsOrCodec };
188
212
  }
189
213
  }
190
214
  let codec = null;
@@ -301,11 +325,11 @@ export class Decoder {
301
325
  ) {
302
326
  // First argument is a codec
303
327
  explicitCodec = optionsOrCodec;
304
- options = maybeOptions ?? {};
328
+ options = { ...maybeOptions };
305
329
  }
306
330
  else {
307
- // First argument is options
308
- options = optionsOrCodec;
331
+ // First argument is options (copied: create() fills in defaults)
332
+ options = { ...optionsOrCodec };
309
333
  }
310
334
  }
311
335
  let codec = null;
@@ -445,6 +469,31 @@ export class Decoder {
445
469
  get isDecoderInitialized() {
446
470
  return this.initialized;
447
471
  }
472
+ /**
473
+ * Number of decode errors so far.
474
+ *
475
+ * Counts packets the decoder rejected, failed frame receives and frames that
476
+ * could not be transferred from the GPU, rescaled or cropped. With
477
+ * `exitOnError: false` these are skipped instead of thrown, so this is the way
478
+ * to notice them (e.g. a hardware decoder that keeps failing).
479
+ *
480
+ * Like FFmpeg CLI's decode error count. Corrupt frames are not counted; they are
481
+ * returned with `decodeErrorFlags` or `AV_FRAME_FLAG_CORRUPT` set.
482
+ *
483
+ * @returns Decode error count
484
+ *
485
+ * @example
486
+ * ```typescript
487
+ * const decoder = await Decoder.create(stream, { exitOnError: false });
488
+ * // ...
489
+ * if (decoder.decodeErrors > lastErrors) {
490
+ * console.warn(`${decoder.decodeErrors - lastErrors} decode errors`);
491
+ * }
492
+ * ```
493
+ */
494
+ get decodeErrors() {
495
+ return this._decodeErrors;
496
+ }
448
497
  /**
449
498
  * Check if decoder uses hardware acceleration.
450
499
  *
@@ -546,6 +595,11 @@ export class Decoder {
546
595
  if (packet.size === 0) {
547
596
  return;
548
597
  }
598
+ // A hardware decoder that stayed silent is replaced before this packet goes in
599
+ const stuck = this.hardwareWatch ? this.watchHardware(packet) : null;
600
+ if (stuck) {
601
+ await this.fallBackToSoftware(stuck);
602
+ }
549
603
  // Send packet to decoder
550
604
  const sendRet = await this.codecContext.sendPacket(packet);
551
605
  // EAGAIN during send_packet is a decoder bug (FFmpeg treats this as AVERROR_BUG)
@@ -555,6 +609,7 @@ export class Decoder {
555
609
  }
556
610
  // Handle send errors
557
611
  if (sendRet < 0 && sendRet !== AVERROR_EOF) {
612
+ this._decodeErrors++;
558
613
  if (this.options.exitOnError) {
559
614
  FFmpegError.throwIfError(sendRet, 'Failed to send packet to decoder');
560
615
  }
@@ -630,6 +685,10 @@ export class Decoder {
630
685
  if (packet.size === 0) {
631
686
  return;
632
687
  }
688
+ const stuck = this.hardwareWatch ? this.watchHardware(packet) : null;
689
+ if (stuck) {
690
+ this.fallBackToSoftwareSync(stuck);
691
+ }
633
692
  // Send packet to decoder
634
693
  const sendRet = this.codecContext.sendPacketSync(packet);
635
694
  // EAGAIN during send_packet is a decoder bug (FFmpeg treats this as AVERROR_BUG)
@@ -639,6 +698,7 @@ export class Decoder {
639
698
  }
640
699
  // Handle send errors
641
700
  if (sendRet < 0 && sendRet !== AVERROR_EOF) {
701
+ this._decodeErrors++;
642
702
  if (this.options.exitOnError) {
643
703
  FFmpegError.throwIfError(sendRet, 'Failed to send packet to decoder');
644
704
  }
@@ -1170,19 +1230,24 @@ export class Decoder {
1170
1230
  if (this.isClosed) {
1171
1231
  return EOF;
1172
1232
  }
1233
+ // Frames the software decoder produced from the replayed packets come first
1234
+ const replayed = this.replayedFrames.shift();
1235
+ if (replayed) {
1236
+ return replayed;
1237
+ }
1173
1238
  // Clear previous frame data
1174
1239
  this.frame.unref();
1175
1240
  const ret = await this.codecContext.receiveFrame(this.frame);
1176
1241
  if (ret === 0) {
1242
+ if (this.hardwareWatch) {
1243
+ this.hardwareWatch.framesSincePacket++;
1244
+ }
1177
1245
  // Set frame time_base to decoder's packet timebase
1178
1246
  this.frame.timeBase = this.codecContext.pktTimebase;
1179
- // Check for corrupt frame
1180
- if (this.frame.decodeErrorFlags || this.frame.hasFlags(AV_FRAME_FLAG_CORRUPT)) {
1181
- if (this.options.exitOnError) {
1182
- throw new FFmpegError(AVERROR_INVALIDDATA);
1183
- }
1184
- // exitOnError=false: skip corrupt frame
1185
- return null;
1247
+ // Corrupt frame: with exitOnError off it is passed on with its flags set,
1248
+ // as FFmpeg CLI does (it only warns), so callers can decide to drop it
1249
+ if (this.options.exitOnError && (this.frame.decodeErrorFlags || this.frame.hasFlags(AV_FRAME_FLAG_CORRUPT))) {
1250
+ throw new FFmpegError(AVERROR_INVALIDDATA);
1186
1251
  }
1187
1252
  // Handles PTS assignment, duration estimation, and frame tracking
1188
1253
  if (this.codecContext.codecType === AVMEDIA_TYPE_VIDEO) {
@@ -1224,6 +1289,7 @@ export class Decoder {
1224
1289
  }
1225
1290
  else {
1226
1291
  // Error during receive
1292
+ this._decodeErrors++;
1227
1293
  if (this.options.exitOnError) {
1228
1294
  FFmpegError.throwIfError(ret, 'Failed to receive frame');
1229
1295
  }
@@ -1285,19 +1351,22 @@ export class Decoder {
1285
1351
  if (this.isClosed) {
1286
1352
  return EOF;
1287
1353
  }
1354
+ const replayed = this.replayedFrames.shift();
1355
+ if (replayed) {
1356
+ return replayed;
1357
+ }
1288
1358
  // Clear previous frame data
1289
1359
  this.frame.unref();
1290
1360
  const ret = this.codecContext.receiveFrameSync(this.frame);
1291
1361
  if (ret === 0) {
1362
+ if (this.hardwareWatch) {
1363
+ this.hardwareWatch.framesSincePacket++;
1364
+ }
1292
1365
  // Set frame time_base to decoder's packet timebase
1293
1366
  this.frame.timeBase = this.codecContext.pktTimebase;
1294
1367
  // Check for corrupt frame
1295
- if (this.frame.decodeErrorFlags || this.frame.hasFlags(AV_FRAME_FLAG_CORRUPT)) {
1296
- if (this.options.exitOnError) {
1297
- throw new FFmpegError(AVERROR_INVALIDDATA);
1298
- }
1299
- // exitOnError=false: skip corrupt frame
1300
- return null;
1368
+ if (this.options.exitOnError && (this.frame.decodeErrorFlags || this.frame.hasFlags(AV_FRAME_FLAG_CORRUPT))) {
1369
+ throw new FFmpegError(AVERROR_INVALIDDATA);
1301
1370
  }
1302
1371
  // Process video frame
1303
1372
  // Handles PTS assignment, duration estimation, and frame tracking
@@ -1341,6 +1410,7 @@ export class Decoder {
1341
1410
  }
1342
1411
  else {
1343
1412
  // Error during receive
1413
+ this._decodeErrors++;
1344
1414
  if (this.options.exitOnError) {
1345
1415
  FFmpegError.throwIfError(ret, 'Failed to receive frame');
1346
1416
  }
@@ -1394,7 +1464,6 @@ export class Decoder {
1394
1464
  this.outputQueue?.close();
1395
1465
  this.inputQueue?.clear();
1396
1466
  this.outputQueue?.clear();
1397
- this.frame.free();
1398
1467
  this.audioResampler?.[Symbol.dispose]();
1399
1468
  this.audioResampler = undefined;
1400
1469
  this.resampledFrame?.free();
@@ -1403,7 +1472,16 @@ export class Decoder {
1403
1472
  this.videoScaler = undefined;
1404
1473
  this.scaledFrame?.free();
1405
1474
  this.scaledFrame = undefined;
1475
+ for (const held of this.hardwareWatch?.held ?? []) {
1476
+ held.free();
1477
+ }
1478
+ this.hardwareWatch = undefined;
1479
+ for (const frame of this.replayedFrames) {
1480
+ frame.free();
1481
+ }
1482
+ this.replayedFrames = [];
1406
1483
  this.codecContext.freeContext();
1484
+ this.frame.free();
1407
1485
  this.initialized = false;
1408
1486
  }
1409
1487
  /**
@@ -1424,6 +1502,20 @@ export class Decoder {
1424
1502
  getStream() {
1425
1503
  return this.stream;
1426
1504
  }
1505
+ /**
1506
+ * Get the stream's guessed frame rate.
1507
+ *
1508
+ * av_guess_frame_rate() of the decoded stream, taken when the decoder was
1509
+ * created, so it outlives the demuxer. FFmpeg CLI feeds this rate into the
1510
+ * filter graph, from where it reaches the encoder.
1511
+ *
1512
+ * @returns Guessed frame rate, or 0/1 if unknown
1513
+ *
1514
+ * @internal
1515
+ */
1516
+ getGuessedFrameRate() {
1517
+ return this.guessedFramerate;
1518
+ }
1427
1519
  /**
1428
1520
  * Get decoder codec.
1429
1521
  *
@@ -1455,6 +1547,177 @@ export class Decoder {
1455
1547
  getCodecContext() {
1456
1548
  return !this.isClosed && this.initialized ? this.codecContext : null;
1457
1549
  }
1550
+ /**
1551
+ * Account for the packet that went in before this one and hold this one.
1552
+ *
1553
+ * A new hardware decoder has to produce a frame within {@link HW_DECODE_PROBE}
1554
+ * packets (plus its threads' delay) after its first keyframe. A running one
1555
+ * counts as stuck once two keyframes in a row stayed silent, because a
1556
+ * single corrupt keyframe leaves even a healthy decoder silent until the next
1557
+ * one. Packets since the last silent keyframe are held, so the software
1558
+ * decoder can start over from it.
1559
+ *
1560
+ * @param packet - The packet about to go in
1561
+ *
1562
+ * @returns Why the decoder counts as stuck, or null while it is not
1563
+ *
1564
+ * @internal
1565
+ */
1566
+ watchHardware(packet) {
1567
+ const watch = this.hardwareWatch;
1568
+ if (watch.lastKeyframe !== undefined) {
1569
+ if (watch.framesSincePacket > 0) {
1570
+ watch.proven = true;
1571
+ watch.silent = 0;
1572
+ watch.silentKeyframes = 0;
1573
+ watch.sinceSecondKeyframe = 0;
1574
+ for (const held of watch.held) {
1575
+ held.free();
1576
+ }
1577
+ watch.held = [];
1578
+ }
1579
+ else if (watch.started) {
1580
+ watch.silent++;
1581
+ if (watch.lastKeyframe && ++watch.silentKeyframes === 2) {
1582
+ watch.sinceSecondKeyframe = 0;
1583
+ }
1584
+ else if (watch.silentKeyframes >= 2) {
1585
+ watch.sinceSecondKeyframe++;
1586
+ }
1587
+ }
1588
+ }
1589
+ watch.framesSincePacket = 0;
1590
+ watch.lastKeyframe = packet.isKeyframe;
1591
+ watch.started ||= packet.isKeyframe;
1592
+ // Frame threads hold back one frame each before the first comes out
1593
+ const probe = HW_DECODE_PROBE + Math.max(0, this.codecContext.threadCount);
1594
+ const stuck = watch.proven ? watch.silentKeyframes >= 2 && watch.sinceSecondKeyframe >= HW_DECODE_DELAY : watch.silent >= probe;
1595
+ if (stuck) {
1596
+ return `no frame from the last ${watch.silent} packets`;
1597
+ }
1598
+ // Only a run that starts at a keyframe can be decoded again
1599
+ if (packet.isKeyframe) {
1600
+ for (const held of watch.held) {
1601
+ held.free();
1602
+ }
1603
+ watch.held = [];
1604
+ }
1605
+ if (packet.isKeyframe || watch.held.length > 0) {
1606
+ const held = packet.clone();
1607
+ if (held) {
1608
+ watch.held.push(held);
1609
+ }
1610
+ }
1611
+ return null;
1612
+ }
1613
+ /**
1614
+ * Reopen the decoder in software and decode the held packets again.
1615
+ *
1616
+ * The frames they produce are returned by the next receive calls, before
1617
+ * anything the new decoder makes of later packets.
1618
+ *
1619
+ * @param reason - Why the hardware decoder counts as stuck
1620
+ *
1621
+ * @throws {FFmpegError} If the software decoder fails to open or decode
1622
+ *
1623
+ * @internal
1624
+ */
1625
+ async fallBackToSoftware(reason) {
1626
+ const held = this.takeHardwareWatch(reason);
1627
+ const frames = [];
1628
+ try {
1629
+ this.adoptCodecContext(await Decoder.create(this.stream, { ...this.options, hardware: undefined, hardwareFallback: false }));
1630
+ for (const packet of held) {
1631
+ await this.decode(packet);
1632
+ let frame;
1633
+ while ((frame = await this.receive()) && frame !== EOF) {
1634
+ frames.push(frame);
1635
+ }
1636
+ }
1637
+ }
1638
+ catch (error) {
1639
+ for (const frame of frames) {
1640
+ frame.free();
1641
+ }
1642
+ throw error;
1643
+ }
1644
+ finally {
1645
+ for (const packet of held) {
1646
+ packet.free();
1647
+ }
1648
+ }
1649
+ this.replayedFrames.push(...frames);
1650
+ }
1651
+ /**
1652
+ * Reopen the decoder in software and decode the held packets again synchronously.
1653
+ * Synchronous version of fallBackToSoftware.
1654
+ *
1655
+ * @param reason - Why the hardware decoder counts as stuck
1656
+ *
1657
+ * @throws {FFmpegError} If the software decoder fails to open or decode
1658
+ *
1659
+ * @internal
1660
+ *
1661
+ * @see {@link fallBackToSoftware} For async version
1662
+ */
1663
+ fallBackToSoftwareSync(reason) {
1664
+ const held = this.takeHardwareWatch(reason);
1665
+ const frames = [];
1666
+ try {
1667
+ this.adoptCodecContext(Decoder.createSync(this.stream, { ...this.options, hardware: undefined, hardwareFallback: false }));
1668
+ for (const packet of held) {
1669
+ this.decodeSync(packet);
1670
+ let frame;
1671
+ while ((frame = this.receiveSync()) && frame !== EOF) {
1672
+ frames.push(frame);
1673
+ }
1674
+ }
1675
+ }
1676
+ catch (error) {
1677
+ for (const frame of frames) {
1678
+ frame.free();
1679
+ }
1680
+ throw error;
1681
+ }
1682
+ finally {
1683
+ for (const packet of held) {
1684
+ packet.free();
1685
+ }
1686
+ }
1687
+ this.replayedFrames.push(...frames);
1688
+ }
1689
+ /**
1690
+ * End the watch on the hardware decoder, which fell back once and for all.
1691
+ *
1692
+ * @param reason - Why the hardware decoder counts as stuck
1693
+ *
1694
+ * @returns The held packets, owned by the caller
1695
+ *
1696
+ * @internal
1697
+ */
1698
+ takeHardwareWatch(reason) {
1699
+ const held = this.hardwareWatch?.held ?? [];
1700
+ this.hardwareWatch = undefined;
1701
+ Log.log(AV_LOG_WARNING, `Hardware decoder ${this.codec.name} failed (${reason}, ${this._decodeErrors} decode errors), decoding in software\n`);
1702
+ return held;
1703
+ }
1704
+ /**
1705
+ * Take over the codec context of a freshly opened decoder and close it with the old one.
1706
+ *
1707
+ * @param software - Decoder opened in software for the same stream
1708
+ *
1709
+ * @internal
1710
+ */
1711
+ adoptCodecContext(software) {
1712
+ [this.codecContext, software.codecContext] = [software.codecContext, this.codecContext];
1713
+ [this.codec, software.codec] = [software.codec, this.codec];
1714
+ this.options.hardware = undefined;
1715
+ // Set up for the device's frames, the software frames may differ
1716
+ this.hwTransferFormat = undefined;
1717
+ this.videoScaler?.[Symbol.dispose]();
1718
+ this.videoScaler = undefined;
1719
+ software.close();
1720
+ }
1458
1721
  /**
1459
1722
  * Worker loop for push-based processing.
1460
1723
  *
@@ -1689,6 +1952,7 @@ export class Decoder {
1689
1952
  if (this.options.applyCropping) {
1690
1953
  const ret = frame.applyCropping(1); // AV_FRAME_CROP_UNALIGNED = 1
1691
1954
  if (ret < 0) {
1955
+ this._decodeErrors++;
1692
1956
  if (this.options.exitOnError) {
1693
1957
  FFmpegError.throwIfError(ret, 'Error applying decoder cropping');
1694
1958
  }
@@ -1997,6 +2261,7 @@ export class Decoder {
1997
2261
  const ret = frame.hwframeTransferDataSync(swFrame, 0);
1998
2262
  if (ret < 0) {
1999
2263
  swFrame.free();
2264
+ this._decodeErrors++;
2000
2265
  if (this.options.exitOnError) {
2001
2266
  FFmpegError.throwIfError(ret, 'Failed to transfer hardware frame data');
2002
2267
  }
@@ -2007,6 +2272,7 @@ export class Decoder {
2007
2272
  const refRet = frame.ref(swFrame);
2008
2273
  swFrame.free();
2009
2274
  if (refRet < 0) {
2275
+ this._decodeErrors++;
2010
2276
  if (this.options.exitOnError) {
2011
2277
  FFmpegError.throwIfError(refRet, 'Failed to reference software frame');
2012
2278
  }
@@ -2067,6 +2333,7 @@ export class Decoder {
2067
2333
  out.height = dstH;
2068
2334
  const ret = this.videoScaler.scaleFrameSync(out, frame);
2069
2335
  if (ret < 0) {
2336
+ this._decodeErrors++;
2070
2337
  if (this.options.exitOnError) {
2071
2338
  FFmpegError.throwIfError(ret, 'Failed to rescale video frame');
2072
2339
  }