node-av 6.2.0-beta.31 → 6.2.0-beta.33

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 (48) hide show
  1. package/README.md +0 -10
  2. package/dist/api/data/test_av1.ivf +0 -0
  3. package/dist/api/data/test_hevc.h265 +0 -0
  4. package/dist/api/data/test_vp9.ivf +0 -0
  5. package/dist/api/decoder.d.ts +117 -2
  6. package/dist/api/decoder.js +289 -22
  7. package/dist/api/decoder.js.map +1 -1
  8. package/dist/api/encoder.d.ts +26 -3
  9. package/dist/api/encoder.js +142 -76
  10. package/dist/api/encoder.js.map +1 -1
  11. package/dist/api/filter-presets.d.ts +25 -1
  12. package/dist/api/filter-presets.js +63 -3
  13. package/dist/api/filter-presets.js.map +1 -1
  14. package/dist/api/filter.d.ts +62 -3
  15. package/dist/api/filter.js +118 -36
  16. package/dist/api/filter.js.map +1 -1
  17. package/dist/api/fmp4-stream.d.ts +1 -0
  18. package/dist/api/fmp4-stream.js +34 -49
  19. package/dist/api/fmp4-stream.js.map +1 -1
  20. package/dist/api/hardware.js +12 -9
  21. package/dist/api/hardware.js.map +1 -1
  22. package/dist/api/muxer.js +33 -24
  23. package/dist/api/muxer.js.map +1 -1
  24. package/dist/api/rtp-stream.d.ts +10 -0
  25. package/dist/api/rtp-stream.js +70 -67
  26. package/dist/api/rtp-stream.js.map +1 -1
  27. package/dist/api/scaler.d.ts +17 -1
  28. package/dist/api/scaler.js +41 -10
  29. package/dist/api/scaler.js.map +1 -1
  30. package/dist/api/utilities/live-decoding.d.ts +15 -0
  31. package/dist/api/utilities/live-decoding.js +32 -0
  32. package/dist/api/utilities/live-decoding.js.map +1 -0
  33. package/dist/api/utilities/scale-size.d.ts +22 -0
  34. package/dist/api/utilities/scale-size.js +31 -0
  35. package/dist/api/utilities/scale-size.js.map +1 -0
  36. package/dist/api/utilities/video-transcode.d.ts +183 -0
  37. package/dist/api/utilities/video-transcode.js +567 -0
  38. package/dist/api/utilities/video-transcode.js.map +1 -0
  39. package/dist/lib/binding.d.ts +1 -0
  40. package/dist/lib/binding.js.map +1 -1
  41. package/dist/lib/native-types.d.ts +2 -0
  42. package/dist/lib/output-format.d.ts +42 -0
  43. package/dist/lib/output-format.js +46 -0
  44. package/dist/lib/output-format.js.map +1 -1
  45. package/dist/lib/utilities.d.ts +26 -0
  46. package/dist/lib/utilities.js +28 -0
  47. package/dist/lib/utilities.js.map +1 -1
  48. package/package.json +7 -18
package/README.md CHANGED
@@ -540,16 +540,6 @@ NodeAV fully supports Electron applications. The prebuilt binaries are ABI-compa
540
540
 
541
541
  NodeAV also supports Electron's [offscreen rendering with shared textures](https://www.electronjs.org/docs/latest/tutorial/offscreen-rendering), enabling zero-copy GPU texture import for hardware-accelerated encoding. See the [Electron Builder](https://github.com/seydx/node-av/tree/main/examples/electron/builder) and [Electron Forge](https://github.com/seydx/node-av/tree/main/examples/electron/forge) examples for complete implementations.
542
542
 
543
- If you encounter module resolution errors like `Cannot find module 'lib/binary-stream'`, add this override to your project's `package.json`:
544
-
545
- ```json
546
- {
547
- "overrides": {
548
- "@shinyoshiaki/binary-data": "npm:@seydx/binary-data@0.6.2"
549
- }
550
- }
551
- ```
552
-
553
543
  ## Examples
554
544
 
555
545
  The repository ships 60+ runnable examples covering both API levels — high-level (`api-*`: transcoding, hardware acceleration, RTSP, fMP4/DASH, RTP, Whisper, device capture, …) and low-level ports of the official FFmpeg C examples.
Binary file
Binary file
Binary file
@@ -17,8 +17,10 @@ export interface DecoderOptions<C = unknown> {
17
17
  /**
18
18
  * Exit immediately on first decode error.
19
19
  *
20
- * When enabled, the decoder will terminate on the first decode error.
21
- * When disabled, errors are logged but decoding continues with next packet.
20
+ * When enabled, the decoder throws on the first decode error or corrupt frame.
21
+ * When disabled it continues like FFmpeg CLI: failed packets are skipped and
22
+ * counted in {@link Decoder.decodeErrors}, and corrupt frames are returned with
23
+ * `decodeErrorFlags` or `AV_FRAME_FLAG_CORRUPT` set, so callers can drop them.
22
24
  *
23
25
  * @default true
24
26
  */
@@ -30,6 +32,19 @@ export interface DecoderOptions<C = unknown> {
30
32
  * Set to null to disable hardware acceleration.
31
33
  */
32
34
  hardware?: HardwareContext | null;
35
+ /**
36
+ * Decode in software when the hardware decoder produces nothing.
37
+ *
38
+ * Some drivers accept a stream and then fail on every frame, which with
39
+ * `exitOnError: false` leaves a stream without pictures. A new hardware
40
+ * decoder has to produce a frame within a few packets after its first
41
+ * keyframe, a running one counts as stuck once two keyframes in a row stay
42
+ * silent. The decoder then reopens in software and decodes the packets since
43
+ * the last silent keyframe again, so the frames carry on from there.
44
+ *
45
+ * @default false
46
+ */
47
+ hardwareFallback?: boolean;
33
48
  /**
34
49
  * Number of extra hardware frames to allocate.
35
50
  *
@@ -241,9 +256,13 @@ export declare class Decoder implements Disposable {
241
256
  private stream;
242
257
  private readonly streamIndex;
243
258
  private readonly streamFramerate;
259
+ private readonly guessedFramerate;
244
260
  private initialized;
245
261
  private isClosed;
246
262
  private options;
263
+ private _decodeErrors;
264
+ private hardwareWatch?;
265
+ private replayedFrames;
247
266
  private lastFramePts;
248
267
  private lastFrameDurationEst;
249
268
  private lastFrameTb;
@@ -422,6 +441,29 @@ export declare class Decoder implements Disposable {
422
441
  * ```
423
442
  */
424
443
  get isDecoderInitialized(): boolean;
444
+ /**
445
+ * Number of decode errors so far.
446
+ *
447
+ * Counts packets the decoder rejected, failed frame receives and frames that
448
+ * could not be transferred from the GPU, rescaled or cropped. With
449
+ * `exitOnError: false` these are skipped instead of thrown, so this is the way
450
+ * to notice them (e.g. a hardware decoder that keeps failing).
451
+ *
452
+ * Like FFmpeg CLI's decode error count. Corrupt frames are not counted; they are
453
+ * returned with `decodeErrorFlags` or `AV_FRAME_FLAG_CORRUPT` set.
454
+ *
455
+ * @returns Decode error count
456
+ *
457
+ * @example
458
+ * ```typescript
459
+ * const decoder = await Decoder.create(stream, { exitOnError: false });
460
+ * // ...
461
+ * if (decoder.decodeErrors > lastErrors) {
462
+ * console.warn(`${decoder.decodeErrors - lastErrors} decode errors`);
463
+ * }
464
+ * ```
465
+ */
466
+ get decodeErrors(): number;
425
467
  /**
426
468
  * Check if decoder uses hardware acceleration.
427
469
  *
@@ -1029,6 +1071,18 @@ export declare class Decoder implements Disposable {
1029
1071
  * @see {@link Stream} For stream details
1030
1072
  */
1031
1073
  getStream(): Stream;
1074
+ /**
1075
+ * Get the stream's guessed frame rate.
1076
+ *
1077
+ * av_guess_frame_rate() of the decoded stream, taken when the decoder was
1078
+ * created, so it outlives the demuxer. FFmpeg CLI feeds this rate into the
1079
+ * filter graph, from where it reaches the encoder.
1080
+ *
1081
+ * @returns Guessed frame rate, or 0/1 if unknown
1082
+ *
1083
+ * @internal
1084
+ */
1085
+ getGuessedFrameRate(): IRational;
1032
1086
  /**
1033
1087
  * Get decoder codec.
1034
1088
  *
@@ -1056,6 +1110,67 @@ export declare class Decoder implements Disposable {
1056
1110
  * @see {@link CodecContext} For context details
1057
1111
  */
1058
1112
  getCodecContext(): CodecContext | null;
1113
+ /**
1114
+ * Account for the packet that went in before this one and hold this one.
1115
+ *
1116
+ * A new hardware decoder has to produce a frame within {@link HW_DECODE_PROBE}
1117
+ * packets (plus its threads' delay) after its first keyframe. A running one
1118
+ * counts as stuck once two keyframes in a row stayed silent, because a
1119
+ * single corrupt keyframe leaves even a healthy decoder silent until the next
1120
+ * one. Packets since the last silent keyframe are held, so the software
1121
+ * decoder can start over from it.
1122
+ *
1123
+ * @param packet - The packet about to go in
1124
+ *
1125
+ * @returns Why the decoder counts as stuck, or null while it is not
1126
+ *
1127
+ * @internal
1128
+ */
1129
+ private watchHardware;
1130
+ /**
1131
+ * Reopen the decoder in software and decode the held packets again.
1132
+ *
1133
+ * The frames they produce are returned by the next receive calls, before
1134
+ * anything the new decoder makes of later packets.
1135
+ *
1136
+ * @param reason - Why the hardware decoder counts as stuck
1137
+ *
1138
+ * @throws {FFmpegError} If the software decoder fails to open or decode
1139
+ *
1140
+ * @internal
1141
+ */
1142
+ private fallBackToSoftware;
1143
+ /**
1144
+ * Reopen the decoder in software and decode the held packets again synchronously.
1145
+ * Synchronous version of fallBackToSoftware.
1146
+ *
1147
+ * @param reason - Why the hardware decoder counts as stuck
1148
+ *
1149
+ * @throws {FFmpegError} If the software decoder fails to open or decode
1150
+ *
1151
+ * @internal
1152
+ *
1153
+ * @see {@link fallBackToSoftware} For async version
1154
+ */
1155
+ private fallBackToSoftwareSync;
1156
+ /**
1157
+ * End the watch on the hardware decoder, which fell back once and for all.
1158
+ *
1159
+ * @param reason - Why the hardware decoder counts as stuck
1160
+ *
1161
+ * @returns The held packets, owned by the caller
1162
+ *
1163
+ * @internal
1164
+ */
1165
+ private takeHardwareWatch;
1166
+ /**
1167
+ * Take over the codec context of a freshly opened decoder and close it with the old one.
1168
+ *
1169
+ * @param software - Decoder opened in software for the same stream
1170
+ *
1171
+ * @internal
1172
+ */
1173
+ private adoptCodecContext;
1059
1174
  /**
1060
1175
  * Worker loop for push-based processing.
1061
1176
  *
@@ -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
  }
@@ -1402,6 +1472,14 @@ export class Decoder {
1402
1472
  this.videoScaler = undefined;
1403
1473
  this.scaledFrame?.free();
1404
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 = [];
1405
1483
  this.codecContext.freeContext();
1406
1484
  this.frame.free();
1407
1485
  this.initialized = false;
@@ -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
  }