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.
- package/README.md +0 -10
- package/dist/api/data/test_av1.ivf +0 -0
- package/dist/api/data/test_hevc.h265 +0 -0
- package/dist/api/data/test_vp9.ivf +0 -0
- package/dist/api/decoder.d.ts +117 -2
- package/dist/api/decoder.js +289 -22
- package/dist/api/decoder.js.map +1 -1
- package/dist/api/encoder.d.ts +26 -3
- package/dist/api/encoder.js +142 -76
- package/dist/api/encoder.js.map +1 -1
- package/dist/api/filter-presets.d.ts +25 -1
- package/dist/api/filter-presets.js +63 -3
- package/dist/api/filter-presets.js.map +1 -1
- package/dist/api/filter.d.ts +62 -3
- package/dist/api/filter.js +118 -36
- package/dist/api/filter.js.map +1 -1
- package/dist/api/fmp4-stream.d.ts +1 -0
- package/dist/api/fmp4-stream.js +34 -49
- package/dist/api/fmp4-stream.js.map +1 -1
- package/dist/api/hardware.js +12 -9
- package/dist/api/hardware.js.map +1 -1
- package/dist/api/muxer.js +33 -24
- package/dist/api/muxer.js.map +1 -1
- package/dist/api/rtp-stream.d.ts +10 -0
- package/dist/api/rtp-stream.js +70 -67
- package/dist/api/rtp-stream.js.map +1 -1
- package/dist/api/scaler.d.ts +17 -1
- package/dist/api/scaler.js +41 -10
- package/dist/api/scaler.js.map +1 -1
- package/dist/api/utilities/live-decoding.d.ts +15 -0
- package/dist/api/utilities/live-decoding.js +32 -0
- package/dist/api/utilities/live-decoding.js.map +1 -0
- package/dist/api/utilities/scale-size.d.ts +22 -0
- package/dist/api/utilities/scale-size.js +31 -0
- package/dist/api/utilities/scale-size.js.map +1 -0
- package/dist/api/utilities/video-transcode.d.ts +183 -0
- package/dist/api/utilities/video-transcode.js +567 -0
- package/dist/api/utilities/video-transcode.js.map +1 -0
- package/dist/lib/binding.d.ts +1 -0
- package/dist/lib/binding.js.map +1 -1
- package/dist/lib/native-types.d.ts +2 -0
- package/dist/lib/output-format.d.ts +42 -0
- package/dist/lib/output-format.js +46 -0
- package/dist/lib/output-format.js.map +1 -1
- package/dist/lib/utilities.d.ts +26 -0
- package/dist/lib/utilities.js +28 -0
- package/dist/lib/utilities.js.map +1 -1
- 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
|
package/dist/api/decoder.d.ts
CHANGED
|
@@ -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
|
|
21
|
-
* When disabled
|
|
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
|
*
|
package/dist/api/decoder.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
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
|
-
|
|
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
|
}
|