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
|
@@ -1,5 +1,70 @@
|
|
|
1
|
+
import { AV_NOPTS_VALUE, AV_SAMPLE_FMT_U8, AV_SAMPLE_FMT_U8P } from '../constants/constants.js';
|
|
1
2
|
import { AudioFifo } from '../lib/audio-fifo.js';
|
|
3
|
+
import { FFmpegError } from '../lib/error.js';
|
|
2
4
|
import { Frame } from '../lib/frame.js';
|
|
5
|
+
import { Rational } from '../lib/rational.js';
|
|
6
|
+
import { avGetBytesPerSample, avRescaleQ, avSampleFmtIsPlanar } from '../lib/utilities.js';
|
|
7
|
+
/**
|
|
8
|
+
* Deviation (seconds) between an input frame's timestamp and the sample count that still counts as continuous input.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
const RESYNC_TOLERANCE = 0.02;
|
|
13
|
+
/**
|
|
14
|
+
* Input (seconds) over which a larger deviation must persist before it is compensated.
|
|
15
|
+
*
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
const CONFIRM_DURATION = 0.2;
|
|
19
|
+
/**
|
|
20
|
+
* Frames over which a larger deviation must persist before it is compensated.
|
|
21
|
+
*
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
const CONFIRM_FRAMES = 3;
|
|
25
|
+
/**
|
|
26
|
+
* Largest overlap (seconds) dropped from the input instead of restarting the timeline.
|
|
27
|
+
*
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
const MAX_TRIM = 1;
|
|
31
|
+
/**
|
|
32
|
+
* Default largest gap (seconds) filled with silence instead of restarting the timeline.
|
|
33
|
+
*
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
const DEFAULT_MAX_GAP_FILL = 1;
|
|
37
|
+
/**
|
|
38
|
+
* Check the options of an audio frame buffer.
|
|
39
|
+
*
|
|
40
|
+
* @param options - Options to check
|
|
41
|
+
*
|
|
42
|
+
* @throws {RangeError} If an option is out of range
|
|
43
|
+
*
|
|
44
|
+
* @internal
|
|
45
|
+
*/
|
|
46
|
+
export function assertAudioFrameBufferOptions(options) {
|
|
47
|
+
const { timestamps, maxGapFill } = options;
|
|
48
|
+
if (timestamps !== undefined && timestamps !== 'input' && timestamps !== 'samples') {
|
|
49
|
+
throw new RangeError(`timestamps must be 'input' or 'samples', got ${String(timestamps)}`);
|
|
50
|
+
}
|
|
51
|
+
if (maxGapFill !== undefined && !(Number.isFinite(maxGapFill) && maxGapFill >= 0)) {
|
|
52
|
+
throw new RangeError(`maxGapFill must be a finite number of seconds >= 0, got ${maxGapFill}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Middle value of a list of timestamps.
|
|
57
|
+
*
|
|
58
|
+
* @param values - Non-empty list of timestamps
|
|
59
|
+
*
|
|
60
|
+
* @returns The value in the middle after sorting
|
|
61
|
+
*
|
|
62
|
+
* @internal
|
|
63
|
+
*/
|
|
64
|
+
function median(values) {
|
|
65
|
+
const sorted = [...values].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
66
|
+
return sorted[sorted.length >> 1];
|
|
67
|
+
}
|
|
3
68
|
/**
|
|
4
69
|
* Audio frame buffering utility for encoders with fixed frame size requirements.
|
|
5
70
|
*
|
|
@@ -7,6 +72,16 @@ import { Frame } from '../lib/frame.js';
|
|
|
7
72
|
* of samples (frame_size). This class buffers incoming frames and outputs frames
|
|
8
73
|
* with exactly the required size.
|
|
9
74
|
*
|
|
75
|
+
* Output frames keep the input timing: each carries the timestamp of its first
|
|
76
|
+
* sample, in the time base of the pushed frames, and counts on by samples while the
|
|
77
|
+
* input is continuous. Deviations of up to 20 ms count as continuous, and larger ones
|
|
78
|
+
* only once they persist over 0.2 s of input, so jitter and single frames with a wrong
|
|
79
|
+
* timestamp leave the audio untouched. A persistent gap of up to a second is then
|
|
80
|
+
* filled with silence and an overlap of up to a second is dropped, which keeps the
|
|
81
|
+
* audio contiguous and in sync; a larger jump restarts the timeline at the new
|
|
82
|
+
* timestamp on the next frame boundary. Frames without a timestamp or time base
|
|
83
|
+
* continue the previous one.
|
|
84
|
+
*
|
|
10
85
|
* Uses FFmpeg's AVAudioFifo internally for efficient sample buffering.
|
|
11
86
|
*
|
|
12
87
|
* @example
|
|
@@ -28,20 +103,49 @@ import { Frame } from '../lib/frame.js';
|
|
|
28
103
|
* }
|
|
29
104
|
* }
|
|
30
105
|
*
|
|
31
|
-
* // Flush remaining samples
|
|
106
|
+
* // Flush remaining samples (final partial frame is padded with silence)
|
|
32
107
|
* let outputFrame;
|
|
33
108
|
* while ((outputFrame = await buffer.pull()) !== null) {
|
|
34
109
|
* await encoder.encode(outputFrame);
|
|
35
110
|
* outputFrame.free();
|
|
36
111
|
* }
|
|
112
|
+
* const tail = await buffer.pullPartial();
|
|
113
|
+
* if (tail) {
|
|
114
|
+
* await encoder.encode(tail);
|
|
115
|
+
* tail.free();
|
|
116
|
+
* }
|
|
117
|
+
* ```
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```typescript
|
|
121
|
+
* // Real-time output: input gaps become timestamp jumps instead of silence
|
|
122
|
+
* using buffer = AudioFrameBuffer.create(960, AV_SAMPLE_FMT_FLT, 48000, 'mono', 1, { maxGapFill: 0 });
|
|
37
123
|
* ```
|
|
38
124
|
*/
|
|
39
125
|
export class AudioFrameBuffer {
|
|
40
126
|
fifo;
|
|
41
127
|
frame;
|
|
42
128
|
frameSize;
|
|
43
|
-
|
|
44
|
-
|
|
129
|
+
channels;
|
|
130
|
+
sampleStride;
|
|
131
|
+
staging;
|
|
132
|
+
stagingPlanes;
|
|
133
|
+
silenceValue;
|
|
134
|
+
sampleTimeBase;
|
|
135
|
+
timeBase = null;
|
|
136
|
+
countsInSamples = true;
|
|
137
|
+
followInput;
|
|
138
|
+
tolerance;
|
|
139
|
+
confirmSamples;
|
|
140
|
+
maxFill;
|
|
141
|
+
maxTrim;
|
|
142
|
+
headIndex = 0;
|
|
143
|
+
tailIndex = 0;
|
|
144
|
+
segments = [{ index: 0, pts: 0n }];
|
|
145
|
+
silence = [];
|
|
146
|
+
timed = false;
|
|
147
|
+
trimPending = 0;
|
|
148
|
+
deviation = null;
|
|
45
149
|
/**
|
|
46
150
|
* @param fifo - Underlying AudioFifo instance
|
|
47
151
|
*
|
|
@@ -53,18 +157,41 @@ export class AudioFrameBuffer {
|
|
|
53
157
|
*
|
|
54
158
|
* @param channelLayout - Channel layout
|
|
55
159
|
*
|
|
160
|
+
* @param options - Timing options
|
|
161
|
+
*
|
|
56
162
|
* @internal
|
|
57
163
|
*/
|
|
58
|
-
constructor(fifo, frameSize, sampleFormat, sampleRate, channelLayout) {
|
|
164
|
+
constructor(fifo, frameSize, sampleFormat, sampleRate, channelLayout, options) {
|
|
59
165
|
this.fifo = fifo;
|
|
60
166
|
this.frameSize = frameSize;
|
|
167
|
+
this.channels = channelLayout.nbChannels;
|
|
61
168
|
this.frame = new Frame();
|
|
62
169
|
this.frame.alloc();
|
|
63
170
|
this.frame.nbSamples = frameSize;
|
|
64
171
|
this.frame.format = sampleFormat;
|
|
65
172
|
this.frame.sampleRate = sampleRate;
|
|
66
173
|
this.frame.channelLayout = channelLayout;
|
|
174
|
+
// Until the first pushed frame brings a time base, timestamps count samples.
|
|
175
|
+
this.sampleTimeBase = new Rational(1, sampleRate);
|
|
176
|
+
this.frame.timeBase = this.sampleTimeBase;
|
|
67
177
|
this.frame.getBuffer(0); // Allocate buffer once
|
|
178
|
+
// Frame.data is a copy instead of a view where external buffers are not allowed (Electron),
|
|
179
|
+
// so samples are staged in JS memory and copied into the frame with fromBuffer()
|
|
180
|
+
const planar = avSampleFmtIsPlanar(sampleFormat);
|
|
181
|
+
const planeCount = planar ? this.channels : 1;
|
|
182
|
+
this.sampleStride = avGetBytesPerSample(sampleFormat) * (planar ? 1 : this.channels);
|
|
183
|
+
const planeSize = frameSize * this.sampleStride;
|
|
184
|
+
this.staging = Buffer.alloc(planeSize * planeCount);
|
|
185
|
+
this.stagingPlanes = Array.from({ length: planeCount }, (_, i) => this.staging.subarray(i * planeSize, (i + 1) * planeSize));
|
|
186
|
+
// av_samples_set_silence() semantics: unsigned 8-bit formats are silent at 0x80
|
|
187
|
+
this.silenceValue = sampleFormat === AV_SAMPLE_FMT_U8 || sampleFormat === AV_SAMPLE_FMT_U8P ? 0x80 : 0;
|
|
188
|
+
this.followInput = options.timestamps !== 'samples';
|
|
189
|
+
this.tolerance = Math.round(sampleRate * RESYNC_TOLERANCE);
|
|
190
|
+
this.confirmSamples = Math.round(sampleRate * CONFIRM_DURATION);
|
|
191
|
+
// At least one frame: a restart pads the current frame with up to that much silence anyway,
|
|
192
|
+
// and the padding must never reach past the new timestamp
|
|
193
|
+
this.maxFill = Math.max(Math.round(sampleRate * (options.maxGapFill ?? DEFAULT_MAX_GAP_FILL)), frameSize);
|
|
194
|
+
this.maxTrim = Math.round(sampleRate * MAX_TRIM);
|
|
68
195
|
}
|
|
69
196
|
/**
|
|
70
197
|
* Create an audio frame buffer.
|
|
@@ -79,23 +206,30 @@ export class AudioFrameBuffer {
|
|
|
79
206
|
*
|
|
80
207
|
* @param channels - Number of audio channels
|
|
81
208
|
*
|
|
209
|
+
* @param options - Timing options
|
|
210
|
+
*
|
|
82
211
|
* @returns Configured audio frame buffer
|
|
83
212
|
*
|
|
213
|
+
* @throws {RangeError} If an option is out of range
|
|
214
|
+
*
|
|
84
215
|
* @example
|
|
85
216
|
* ```typescript
|
|
86
217
|
* // For Opus encoder at 48kHz with 20ms frames
|
|
87
218
|
* const buffer = AudioFrameBuffer.create(960, AV_SAMPLE_FMT_FLT, 48000, 'mono', 1);
|
|
88
219
|
* ```
|
|
89
220
|
*/
|
|
90
|
-
static create(frameSize, sampleFormat, sampleRate, channelLayout, channels) {
|
|
221
|
+
static create(frameSize, sampleFormat, sampleRate, channelLayout, channels, options = {}) {
|
|
222
|
+
assertAudioFrameBufferOptions(options);
|
|
91
223
|
const fifo = new AudioFifo();
|
|
92
224
|
// Allocate FIFO with capacity for multiple frames
|
|
93
225
|
fifo.alloc(sampleFormat, channels, frameSize * 4);
|
|
94
|
-
return new AudioFrameBuffer(fifo, frameSize, sampleFormat, sampleRate, channelLayout);
|
|
226
|
+
return new AudioFrameBuffer(fifo, frameSize, sampleFormat, sampleRate, channelLayout, options);
|
|
95
227
|
}
|
|
96
228
|
/**
|
|
97
229
|
* Get number of samples currently in buffer.
|
|
98
230
|
*
|
|
231
|
+
* Includes silence inserted for gaps in the input.
|
|
232
|
+
*
|
|
99
233
|
* @returns Number of buffered samples
|
|
100
234
|
*
|
|
101
235
|
* @example
|
|
@@ -104,12 +238,12 @@ export class AudioFrameBuffer {
|
|
|
104
238
|
* ```
|
|
105
239
|
*/
|
|
106
240
|
get size() {
|
|
107
|
-
return this.
|
|
241
|
+
return this.available();
|
|
108
242
|
}
|
|
109
243
|
/**
|
|
110
244
|
* Check if a complete frame is available.
|
|
111
245
|
*
|
|
112
|
-
* Returns true if the
|
|
246
|
+
* Returns true if the buffer holds at least frameSize samples.
|
|
113
247
|
*
|
|
114
248
|
* @returns True if a full frame can be pulled
|
|
115
249
|
*
|
|
@@ -122,7 +256,7 @@ export class AudioFrameBuffer {
|
|
|
122
256
|
* ```
|
|
123
257
|
*/
|
|
124
258
|
hasFrame() {
|
|
125
|
-
return this.
|
|
259
|
+
return this.available() >= this.frameSize;
|
|
126
260
|
}
|
|
127
261
|
/**
|
|
128
262
|
* Push an audio frame into the buffer asynchronously.
|
|
@@ -130,8 +264,23 @@ export class AudioFrameBuffer {
|
|
|
130
264
|
* The frame's samples are added to the internal FIFO.
|
|
131
265
|
* Call hasFrame() and pull() to retrieve fixed-size output frames.
|
|
132
266
|
*
|
|
267
|
+
* The first frame with a valid time base fixes the time base of the output frames.
|
|
268
|
+
* When the frame timestamps deviate from the buffered samples by more than 20 ms
|
|
269
|
+
* over at least 0.2 s of input (and 3 frames), a gap is filled with silence (up to
|
|
270
|
+
* `maxGapFill`) and an overlap of up to 1 s is dropped from the input; a larger jump
|
|
271
|
+
* restarts the timeline at the new timestamp after padding the current output frame
|
|
272
|
+
* with silence. The frames that arrive until then still continue the previous
|
|
273
|
+
* timeline. Each such correction is a short dropout or cut in the audio: input whose
|
|
274
|
+
* timestamps drift against its sample count, such as a capture device stamped from
|
|
275
|
+
* the system clock, gets one per 20 ms of drift, which keeps it within about 20 ms
|
|
276
|
+
* of its timestamps.
|
|
277
|
+
*
|
|
133
278
|
* @param frame - Audio frame to buffer
|
|
134
279
|
*
|
|
280
|
+
* @throws {Error} If the frame is not an audio frame
|
|
281
|
+
*
|
|
282
|
+
* @throws {FFmpegError} If the samples cannot be written to the FIFO
|
|
283
|
+
*
|
|
135
284
|
* @example
|
|
136
285
|
* ```typescript
|
|
137
286
|
* await buffer.push(filterFrame);
|
|
@@ -140,17 +289,24 @@ export class AudioFrameBuffer {
|
|
|
140
289
|
* @see {@link pushSync} For synchronous version
|
|
141
290
|
*/
|
|
142
291
|
async push(frame) {
|
|
143
|
-
|
|
292
|
+
// Every Frame getter is a call into the addon, so each one is read only once per push
|
|
293
|
+
// (this is frame.isAudio() without its second nbSamples read)
|
|
294
|
+
const nbSamples = frame.nbSamples;
|
|
295
|
+
if (!(frame.sampleRate > 0 && nbSamples > 0)) {
|
|
144
296
|
throw new Error('AudioFrameBuffer.push() requires an audio frame');
|
|
145
297
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
298
|
+
const skip = this.planPush(frame, nbSamples);
|
|
299
|
+
const samples = nbSamples - skip;
|
|
300
|
+
if (samples <= 0) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
FFmpegError.throwIfError(await this.fifo.write(this.inputPlanes(frame, skip), samples), 'Failed to write samples to the audio FIFO');
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
this.resyncTail();
|
|
308
|
+
throw error;
|
|
151
309
|
}
|
|
152
|
-
// Write frame data to FIFO
|
|
153
|
-
await this.fifo.write(frame.data, frame.nbSamples);
|
|
154
310
|
}
|
|
155
311
|
/**
|
|
156
312
|
* Push an audio frame into the buffer synchronously.
|
|
@@ -158,9 +314,14 @@ export class AudioFrameBuffer {
|
|
|
158
314
|
*
|
|
159
315
|
* The frame's samples are added to the internal FIFO.
|
|
160
316
|
* Call hasFrame() and pullSync() to retrieve fixed-size output frames.
|
|
317
|
+
* Timestamps are handled as in push().
|
|
161
318
|
*
|
|
162
319
|
* @param frame - Audio frame to buffer
|
|
163
320
|
*
|
|
321
|
+
* @throws {Error} If the frame is not an audio frame
|
|
322
|
+
*
|
|
323
|
+
* @throws {FFmpegError} If the samples cannot be written to the FIFO
|
|
324
|
+
*
|
|
164
325
|
* @example
|
|
165
326
|
* ```typescript
|
|
166
327
|
* buffer.pushSync(filterFrame);
|
|
@@ -169,27 +330,35 @@ export class AudioFrameBuffer {
|
|
|
169
330
|
* @see {@link push} For async version
|
|
170
331
|
*/
|
|
171
332
|
pushSync(frame) {
|
|
172
|
-
|
|
333
|
+
const nbSamples = frame.nbSamples;
|
|
334
|
+
if (!(frame.sampleRate > 0 && nbSamples > 0)) {
|
|
173
335
|
throw new Error('AudioFrameBuffer.pushSync() requires an audio frame');
|
|
174
336
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
337
|
+
const skip = this.planPush(frame, nbSamples);
|
|
338
|
+
const samples = nbSamples - skip;
|
|
339
|
+
if (samples <= 0) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
FFmpegError.throwIfError(this.fifo.writeSync(this.inputPlanes(frame, skip), samples), 'Failed to write samples to the audio FIFO');
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
this.resyncTail();
|
|
347
|
+
throw error;
|
|
180
348
|
}
|
|
181
|
-
// Write frame data to FIFO
|
|
182
|
-
this.fifo.writeSync(frame.data, frame.nbSamples);
|
|
183
349
|
}
|
|
184
350
|
/**
|
|
185
351
|
* Pull a fixed-size audio frame from the buffer asynchronously.
|
|
186
352
|
*
|
|
187
|
-
* Reads exactly frameSize samples from the FIFO and returns a cloned Frame
|
|
353
|
+
* Reads exactly frameSize samples from the FIFO and returns a cloned Frame
|
|
354
|
+
* stamped with the timestamp of its first sample.
|
|
188
355
|
* Returns null if not enough samples are available.
|
|
189
356
|
* Reuses internal frame buffer for efficiency (like Decoder does).
|
|
190
357
|
*
|
|
191
358
|
* @returns Audio frame with exactly frameSize samples, or null if insufficient samples
|
|
192
359
|
*
|
|
360
|
+
* @throws {FFmpegError} If the FIFO cannot be read or the frame cannot be filled
|
|
361
|
+
*
|
|
193
362
|
* @throws {Error} If frame cloning fails (out of memory)
|
|
194
363
|
*
|
|
195
364
|
* @example
|
|
@@ -206,29 +375,25 @@ export class AudioFrameBuffer {
|
|
|
206
375
|
if (!this.hasFrame()) {
|
|
207
376
|
return null;
|
|
208
377
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
this.nextPts += BigInt(this.frameSize);
|
|
215
|
-
// Clone frame for user (like Decoder does)
|
|
216
|
-
const cloned = this.frame.clone();
|
|
217
|
-
if (!cloned) {
|
|
218
|
-
throw new Error('Failed to clone frame (out of memory)');
|
|
219
|
-
}
|
|
220
|
-
return cloned;
|
|
378
|
+
const pts = this.headPts();
|
|
379
|
+
// Read samples from FIFO and copy them into the reusable frame
|
|
380
|
+
await this.readStaging(this.frameSize);
|
|
381
|
+
this.commitStaging();
|
|
382
|
+
return this.emitFrame(pts);
|
|
221
383
|
}
|
|
222
384
|
/**
|
|
223
385
|
* Pull a fixed-size audio frame from the buffer synchronously.
|
|
224
386
|
* Synchronous version of pull.
|
|
225
387
|
*
|
|
226
|
-
* Reads exactly frameSize samples from the FIFO and returns a cloned Frame
|
|
388
|
+
* Reads exactly frameSize samples from the FIFO and returns a cloned Frame
|
|
389
|
+
* stamped with the timestamp of its first sample.
|
|
227
390
|
* Returns null if not enough samples are available.
|
|
228
391
|
* Reuses internal frame buffer for efficiency (like Decoder does).
|
|
229
392
|
*
|
|
230
393
|
* @returns Audio frame with exactly frameSize samples, or null if insufficient samples
|
|
231
394
|
*
|
|
395
|
+
* @throws {FFmpegError} If the FIFO cannot be read or the frame cannot be filled
|
|
396
|
+
*
|
|
232
397
|
* @throws {Error} If frame cloning fails (out of memory)
|
|
233
398
|
*
|
|
234
399
|
* @example
|
|
@@ -245,22 +410,104 @@ export class AudioFrameBuffer {
|
|
|
245
410
|
if (!this.hasFrame()) {
|
|
246
411
|
return null;
|
|
247
412
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
this.
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
413
|
+
const pts = this.headPts();
|
|
414
|
+
// Read samples from FIFO and copy them into the reusable frame
|
|
415
|
+
this.readStagingSync(this.frameSize);
|
|
416
|
+
this.commitStaging();
|
|
417
|
+
return this.emitFrame(pts);
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Pull the final partial frame from the buffer asynchronously.
|
|
421
|
+
*
|
|
422
|
+
* Reads the remaining samples (fewer than frameSize) and pads the tail with
|
|
423
|
+
* silence so the returned frame still carries exactly frameSize samples.
|
|
424
|
+
* Intended for end-of-stream flushing only - without it the tail (up to
|
|
425
|
+
* frameSize - 1 samples) would be dropped. Returns null if the buffer is
|
|
426
|
+
* empty, or while a complete frame is still available (drain those with
|
|
427
|
+
* pull() first). The padding counts as audio, so frames pushed afterwards are
|
|
428
|
+
* timed after it.
|
|
429
|
+
*
|
|
430
|
+
* @returns Silence-padded audio frame with frameSize samples, or null if nothing to drain
|
|
431
|
+
*
|
|
432
|
+
* @throws {FFmpegError} If the FIFO cannot be read or the frame cannot be filled
|
|
433
|
+
*
|
|
434
|
+
* @throws {Error} If frame cloning fails (out of memory)
|
|
435
|
+
*
|
|
436
|
+
* @example
|
|
437
|
+
* ```typescript
|
|
438
|
+
* // At end of stream, after pull() has returned null
|
|
439
|
+
* using tail = await buffer.pullPartial();
|
|
440
|
+
* if (tail) {
|
|
441
|
+
* await encoder.encode(tail);
|
|
442
|
+
* }
|
|
443
|
+
* ```
|
|
444
|
+
*
|
|
445
|
+
* @see {@link pullPartialSync} For synchronous version
|
|
446
|
+
* @see {@link pull} For pulling complete frames
|
|
447
|
+
*/
|
|
448
|
+
async pullPartial() {
|
|
449
|
+
const remaining = this.available();
|
|
450
|
+
if (remaining <= 0 || remaining >= this.frameSize) {
|
|
451
|
+
return null;
|
|
258
452
|
}
|
|
259
|
-
|
|
453
|
+
const pts = this.headPts();
|
|
454
|
+
// Read the remaining samples into the head of the staging buffer
|
|
455
|
+
await this.readStaging(remaining);
|
|
456
|
+
// Pad the tail with silence up to frameSize
|
|
457
|
+
this.fillSilence(remaining, this.frameSize);
|
|
458
|
+
this.commitStaging();
|
|
459
|
+
const frame = this.emitFrame(pts);
|
|
460
|
+
this.advanceTailToHead();
|
|
461
|
+
return frame;
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Pull the final partial frame from the buffer synchronously.
|
|
465
|
+
* Synchronous version of pullPartial.
|
|
466
|
+
*
|
|
467
|
+
* Reads the remaining samples (fewer than frameSize) and pads the tail with
|
|
468
|
+
* silence so the returned frame still carries exactly frameSize samples.
|
|
469
|
+
* Intended for end-of-stream flushing only. Returns null if the buffer is
|
|
470
|
+
* empty, or while a complete frame is still available.
|
|
471
|
+
*
|
|
472
|
+
* @returns Silence-padded audio frame with frameSize samples, or null if nothing to drain
|
|
473
|
+
*
|
|
474
|
+
* @throws {FFmpegError} If the FIFO cannot be read or the frame cannot be filled
|
|
475
|
+
*
|
|
476
|
+
* @throws {Error} If frame cloning fails (out of memory)
|
|
477
|
+
*
|
|
478
|
+
* @example
|
|
479
|
+
* ```typescript
|
|
480
|
+
* // At end of stream, after pullSync() has returned null
|
|
481
|
+
* using tail = buffer.pullPartialSync();
|
|
482
|
+
* if (tail) {
|
|
483
|
+
* encoder.encodeSync(tail);
|
|
484
|
+
* }
|
|
485
|
+
* ```
|
|
486
|
+
*
|
|
487
|
+
* @see {@link pullPartial} For async version
|
|
488
|
+
* @see {@link pullSync} For pulling complete frames
|
|
489
|
+
*/
|
|
490
|
+
pullPartialSync() {
|
|
491
|
+
const remaining = this.available();
|
|
492
|
+
if (remaining <= 0 || remaining >= this.frameSize) {
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
const pts = this.headPts();
|
|
496
|
+
// Read the remaining samples into the head of the staging buffer
|
|
497
|
+
this.readStagingSync(remaining);
|
|
498
|
+
// Pad the tail with silence up to frameSize
|
|
499
|
+
this.fillSilence(remaining, this.frameSize);
|
|
500
|
+
this.commitStaging();
|
|
501
|
+
const frame = this.emitFrame(pts);
|
|
502
|
+
this.advanceTailToHead();
|
|
503
|
+
return frame;
|
|
260
504
|
}
|
|
261
505
|
/**
|
|
262
506
|
* Reset the buffer, discarding all buffered samples.
|
|
263
507
|
*
|
|
508
|
+
* The next pushed frame starts a new timeline, and its time base becomes
|
|
509
|
+
* the output time base.
|
|
510
|
+
*
|
|
264
511
|
* @example
|
|
265
512
|
* ```typescript
|
|
266
513
|
* buffer.reset();
|
|
@@ -268,8 +515,501 @@ export class AudioFrameBuffer {
|
|
|
268
515
|
*/
|
|
269
516
|
reset() {
|
|
270
517
|
this.fifo.reset();
|
|
271
|
-
this.
|
|
272
|
-
this.
|
|
518
|
+
this.headIndex = 0;
|
|
519
|
+
this.tailIndex = 0;
|
|
520
|
+
this.segments = [{ index: 0, pts: 0n }];
|
|
521
|
+
this.silence = [];
|
|
522
|
+
this.timed = false;
|
|
523
|
+
this.trimPending = 0;
|
|
524
|
+
this.deviation = null;
|
|
525
|
+
this.timeBase = null;
|
|
526
|
+
this.countsInSamples = true;
|
|
527
|
+
this.frame.timeBase = this.sampleTimeBase;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Decide how a pushed frame continues the buffered samples.
|
|
531
|
+
*
|
|
532
|
+
* The only place that moves the write position: it records the silence before the
|
|
533
|
+
* frame and accounts for the samples the caller then writes, so the position always
|
|
534
|
+
* runs ahead of the buffered samples by exactly the writes still in flight.
|
|
535
|
+
*
|
|
536
|
+
* @param frame - Audio frame about to be written
|
|
537
|
+
*
|
|
538
|
+
* @param nbSamples - Number of samples in the frame
|
|
539
|
+
*
|
|
540
|
+
* @returns Samples to drop from the head of the frame
|
|
541
|
+
*
|
|
542
|
+
* @internal
|
|
543
|
+
*/
|
|
544
|
+
planPush(frame, nbSamples) {
|
|
545
|
+
this.timeBase ??= this.lockTimeBase(frame);
|
|
546
|
+
const pts = this.followInput ? this.inputPts(frame) : AV_NOPTS_VALUE;
|
|
547
|
+
if (pts !== AV_NOPTS_VALUE && nbSamples > 0) {
|
|
548
|
+
this.addSilence(this.timed ? this.trackInput(pts, nbSamples) : this.anchorInput(pts));
|
|
549
|
+
}
|
|
550
|
+
const skip = Math.min(this.trimPending, nbSamples);
|
|
551
|
+
this.trimPending -= skip;
|
|
552
|
+
this.tailIndex += nbSamples - skip;
|
|
553
|
+
return skip;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Append silence at the write position.
|
|
557
|
+
*
|
|
558
|
+
* Silence only exists as a run of sample indices and is generated while pulling, so
|
|
559
|
+
* a long gap costs neither a buffer of its size nor a FIFO grown to hold it (an
|
|
560
|
+
* AVAudioFifo never shrinks again).
|
|
561
|
+
*
|
|
562
|
+
* @param samples - Number of silent samples
|
|
563
|
+
*
|
|
564
|
+
* @internal
|
|
565
|
+
*/
|
|
566
|
+
addSilence(samples) {
|
|
567
|
+
if (samples <= 0) {
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
const last = this.silence[this.silence.length - 1];
|
|
571
|
+
if (last && last.index + last.length === this.tailIndex) {
|
|
572
|
+
last.length += samples;
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
this.silence.push({ index: this.tailIndex, length: samples });
|
|
576
|
+
}
|
|
577
|
+
this.tailIndex += samples;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Anchor the timeline at the first frame that carries a timestamp.
|
|
581
|
+
*
|
|
582
|
+
* @param pts - Timestamp of the frame in the output time base
|
|
583
|
+
*
|
|
584
|
+
* @returns Silence to write before the frame
|
|
585
|
+
*
|
|
586
|
+
* @internal
|
|
587
|
+
*/
|
|
588
|
+
anchorInput(pts) {
|
|
589
|
+
this.timed = true;
|
|
590
|
+
const first = this.segments[0];
|
|
591
|
+
if (this.segments.length === 1 && this.headIndex === first.index) {
|
|
592
|
+
// Nothing pulled yet: samples buffered without a timestamp end where this frame starts
|
|
593
|
+
first.pts = pts - this.ticks(this.tailIndex - first.index);
|
|
594
|
+
return 0;
|
|
595
|
+
}
|
|
596
|
+
// Samples without a timestamp already went out on a made-up timeline, which this
|
|
597
|
+
// frame cannot be measured against
|
|
598
|
+
return this.restart(pts);
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Compare a frame's timestamp with the buffered samples and compensate a persistent deviation.
|
|
602
|
+
*
|
|
603
|
+
* Within the tolerance the frame counts on by samples. A larger deviation is only
|
|
604
|
+
* acted on once it has lasted over enough input in the same direction: jitter and
|
|
605
|
+
* frames with a wrong timestamp come and go, while gaps, overlaps and clock steps
|
|
606
|
+
* persist. The correction is the median deviation of that run, so jitter on top of
|
|
607
|
+
* a real step does not leak into it.
|
|
608
|
+
*
|
|
609
|
+
* @param pts - Timestamp of the frame in the output time base
|
|
610
|
+
*
|
|
611
|
+
* @param nbSamples - Number of samples in the frame
|
|
612
|
+
*
|
|
613
|
+
* @returns Silence to write before the frame
|
|
614
|
+
*
|
|
615
|
+
* @internal
|
|
616
|
+
*/
|
|
617
|
+
trackInput(pts, nbSamples) {
|
|
618
|
+
// Where this frame belongs: after the buffered samples, minus the input still to be dropped
|
|
619
|
+
const expected = this.trimPending > 0 ? this.tailPts() - this.ticks(this.trimPending) : this.tailPts();
|
|
620
|
+
const offset = pts - expected;
|
|
621
|
+
const drift = this.samples(offset);
|
|
622
|
+
if (Math.abs(drift) <= this.tolerance) {
|
|
623
|
+
this.deviation = null;
|
|
624
|
+
return 0;
|
|
625
|
+
}
|
|
626
|
+
const sign = drift > 0 ? 1 : -1;
|
|
627
|
+
if (this.deviation?.sign !== sign) {
|
|
628
|
+
this.deviation = { sign, frames: 0, samples: 0, offsets: [] };
|
|
629
|
+
}
|
|
630
|
+
const deviation = this.deviation;
|
|
631
|
+
deviation.frames++;
|
|
632
|
+
deviation.samples += nbSamples;
|
|
633
|
+
deviation.offsets.push(offset);
|
|
634
|
+
if (deviation.frames < CONFIRM_FRAMES || deviation.samples < this.confirmSamples) {
|
|
635
|
+
return 0;
|
|
636
|
+
}
|
|
637
|
+
this.deviation = null;
|
|
638
|
+
return this.compensate(median(deviation.offsets), expected);
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Apply a confirmed deviation to the timeline.
|
|
642
|
+
*
|
|
643
|
+
* A gap is filled with silence and an overlap is dropped from the input, which keeps
|
|
644
|
+
* the output contiguous and on its frame grid. Beyond their limits the timeline
|
|
645
|
+
* restarts at the frame's timestamp instead.
|
|
646
|
+
*
|
|
647
|
+
* @param offset - Deviation of the frame in the output time base
|
|
648
|
+
*
|
|
649
|
+
* @param expected - Timestamp the frame would have without the deviation
|
|
650
|
+
*
|
|
651
|
+
* @returns Silence to write before the frame
|
|
652
|
+
*
|
|
653
|
+
* @internal
|
|
654
|
+
*/
|
|
655
|
+
compensate(offset, expected) {
|
|
656
|
+
const drift = this.samples(offset);
|
|
657
|
+
if (drift > 0) {
|
|
658
|
+
// Input still to be dropped is ahead of the expectation by the same amount
|
|
659
|
+
const cancelled = Math.min(drift, this.trimPending);
|
|
660
|
+
this.trimPending -= cancelled;
|
|
661
|
+
const gap = drift - cancelled;
|
|
662
|
+
if (gap <= this.maxFill) {
|
|
663
|
+
return gap;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
else if (this.trimPending - drift <= this.maxTrim) {
|
|
667
|
+
// Dropping keeps the output monotonic; following a small backward step would make
|
|
668
|
+
// the muxer clamp every packet of the overlap to one tick after the previous one
|
|
669
|
+
this.trimPending -= drift;
|
|
670
|
+
return 0;
|
|
671
|
+
}
|
|
672
|
+
return this.restart(expected + offset);
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Start a new segment at the next output frame boundary.
|
|
676
|
+
*
|
|
677
|
+
* Every output frame then lies within one segment, so its timestamp stays well
|
|
678
|
+
* defined; the current partial frame is padded with silence to get there.
|
|
679
|
+
*
|
|
680
|
+
* @param pts - Timestamp of the frame about to be written
|
|
681
|
+
*
|
|
682
|
+
* @returns Silence to write before the frame
|
|
683
|
+
*
|
|
684
|
+
* @internal
|
|
685
|
+
*/
|
|
686
|
+
restart(pts) {
|
|
687
|
+
const pad = (this.frameSize - (this.tailIndex % this.frameSize)) % this.frameSize;
|
|
688
|
+
const index = this.tailIndex + pad;
|
|
689
|
+
const last = this.segments[this.segments.length - 1];
|
|
690
|
+
if (last.index === index) {
|
|
691
|
+
last.pts = pts;
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
this.segments.push({ index, pts });
|
|
695
|
+
}
|
|
696
|
+
this.trimPending = 0;
|
|
697
|
+
this.deviation = null;
|
|
698
|
+
return pad;
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Fix the output time base from the first pushed frame.
|
|
702
|
+
*
|
|
703
|
+
* Frames without a valid time base leave the output counting samples (1/sample_rate).
|
|
704
|
+
*
|
|
705
|
+
* @param frame - First pushed frame
|
|
706
|
+
*
|
|
707
|
+
* @returns Output time base
|
|
708
|
+
*
|
|
709
|
+
* @internal
|
|
710
|
+
*/
|
|
711
|
+
lockTimeBase(frame) {
|
|
712
|
+
const tb = frame.timeBase;
|
|
713
|
+
const timeBase = tb.num > 0 && tb.den > 0 ? tb : this.sampleTimeBase;
|
|
714
|
+
this.countsInSamples = timeBase.num * this.sampleTimeBase.den === timeBase.den;
|
|
715
|
+
this.frame.timeBase = timeBase;
|
|
716
|
+
return timeBase;
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Read a frame's timestamp in the output time base.
|
|
720
|
+
*
|
|
721
|
+
* @param frame - Pushed frame
|
|
722
|
+
*
|
|
723
|
+
* @returns Timestamp, or AV_NOPTS_VALUE if the frame has no timestamp or time base
|
|
724
|
+
*
|
|
725
|
+
* @internal
|
|
726
|
+
*/
|
|
727
|
+
inputPts(frame) {
|
|
728
|
+
const pts = frame.pts;
|
|
729
|
+
if (pts === AV_NOPTS_VALUE) {
|
|
730
|
+
return pts;
|
|
731
|
+
}
|
|
732
|
+
const tb = frame.timeBase;
|
|
733
|
+
if (tb.num <= 0 || tb.den <= 0) {
|
|
734
|
+
return AV_NOPTS_VALUE;
|
|
735
|
+
}
|
|
736
|
+
const out = this.timeBase;
|
|
737
|
+
return tb.num === out.num && tb.den === out.den ? pts : avRescaleQ(pts, tb, out);
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Timestamp of the next sample to pull.
|
|
741
|
+
*
|
|
742
|
+
* Drops segments the head has reached the end of first, so the head lies in the first one.
|
|
743
|
+
*
|
|
744
|
+
* @returns Timestamp in the output time base
|
|
745
|
+
*
|
|
746
|
+
* @internal
|
|
747
|
+
*/
|
|
748
|
+
headPts() {
|
|
749
|
+
while (this.segments.length > 1 && this.segments[1].index <= this.headIndex) {
|
|
750
|
+
this.segments.shift();
|
|
751
|
+
}
|
|
752
|
+
const segment = this.segments[0];
|
|
753
|
+
return segment.pts + this.ticks(this.headIndex - segment.index);
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Timestamp right after the last buffered sample.
|
|
757
|
+
*
|
|
758
|
+
* @returns Timestamp in the output time base
|
|
759
|
+
*
|
|
760
|
+
* @internal
|
|
761
|
+
*/
|
|
762
|
+
tailPts() {
|
|
763
|
+
const segment = this.segments[this.segments.length - 1];
|
|
764
|
+
return segment.pts + this.ticks(this.tailIndex - segment.index);
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Treat the silence padding of a partial frame as written audio.
|
|
768
|
+
*
|
|
769
|
+
* @internal
|
|
770
|
+
*/
|
|
771
|
+
advanceTailToHead() {
|
|
772
|
+
this.tailIndex = this.headIndex;
|
|
773
|
+
// Deviations seen so far were measured against the unpadded end
|
|
774
|
+
this.deviation = null;
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Realign the write position with the FIFO after a failed write.
|
|
778
|
+
*
|
|
779
|
+
* @internal
|
|
780
|
+
*/
|
|
781
|
+
resyncTail() {
|
|
782
|
+
this.tailIndex = this.headIndex + this.available();
|
|
783
|
+
while (this.silence.length > 0 && this.silence[this.silence.length - 1].index >= this.tailIndex) {
|
|
784
|
+
this.silence.pop();
|
|
785
|
+
}
|
|
786
|
+
while (this.segments.length > 1 && this.segments[this.segments.length - 1].index > this.tailIndex) {
|
|
787
|
+
this.segments.pop();
|
|
788
|
+
}
|
|
789
|
+
this.deviation = null;
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Number of samples that can be pulled: buffered samples plus the silence between them.
|
|
793
|
+
*
|
|
794
|
+
* Stops at the first silence run that lies behind a write still in flight.
|
|
795
|
+
*
|
|
796
|
+
* @returns Number of samples available from the read position
|
|
797
|
+
*
|
|
798
|
+
* @internal
|
|
799
|
+
*/
|
|
800
|
+
available() {
|
|
801
|
+
let buffered = this.fifo.size;
|
|
802
|
+
if (this.silence.length === 0) {
|
|
803
|
+
return buffered;
|
|
804
|
+
}
|
|
805
|
+
let index = this.headIndex;
|
|
806
|
+
for (const run of this.silence) {
|
|
807
|
+
// Negative while the read position lies inside the run
|
|
808
|
+
const before = run.index - index;
|
|
809
|
+
if (before > buffered) {
|
|
810
|
+
break;
|
|
811
|
+
}
|
|
812
|
+
buffered -= Math.max(before, 0);
|
|
813
|
+
index = run.index + run.length;
|
|
814
|
+
}
|
|
815
|
+
return index - this.headIndex + buffered;
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Split the next samples to pull into FIFO reads and silence.
|
|
819
|
+
*
|
|
820
|
+
* @param count - Number of samples to pull
|
|
821
|
+
*
|
|
822
|
+
* @returns Parts in pull order
|
|
823
|
+
*
|
|
824
|
+
* @internal
|
|
825
|
+
*/
|
|
826
|
+
readRuns(count) {
|
|
827
|
+
const runs = [];
|
|
828
|
+
const end = this.headIndex + count;
|
|
829
|
+
let index = this.headIndex;
|
|
830
|
+
for (const run of this.silence) {
|
|
831
|
+
if (run.index + run.length <= index) {
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
const start = Math.max(run.index, index);
|
|
835
|
+
if (start >= end) {
|
|
836
|
+
break;
|
|
837
|
+
}
|
|
838
|
+
if (start > index) {
|
|
839
|
+
runs.push({ length: start - index, silent: false });
|
|
840
|
+
}
|
|
841
|
+
index = Math.min(run.index + run.length, end);
|
|
842
|
+
runs.push({ length: index - start, silent: true });
|
|
843
|
+
}
|
|
844
|
+
if (index < end) {
|
|
845
|
+
runs.push({ length: end - index, silent: false });
|
|
846
|
+
}
|
|
847
|
+
return runs;
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Read the next samples into the head of the staging buffer.
|
|
851
|
+
*
|
|
852
|
+
* @param count - Number of samples to read
|
|
853
|
+
*
|
|
854
|
+
* @throws {FFmpegError} If the FIFO cannot be read
|
|
855
|
+
*
|
|
856
|
+
* @internal
|
|
857
|
+
*/
|
|
858
|
+
async readStaging(count) {
|
|
859
|
+
if (this.silence.length === 0) {
|
|
860
|
+
FFmpegError.throwIfError(await this.fifo.read(this.stagingPlanes, count), 'Failed to read samples from the audio FIFO');
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
let offset = 0;
|
|
864
|
+
for (const run of this.readRuns(count)) {
|
|
865
|
+
if (run.silent) {
|
|
866
|
+
this.fillSilence(offset, offset + run.length);
|
|
867
|
+
}
|
|
868
|
+
else {
|
|
869
|
+
FFmpegError.throwIfError(await this.fifo.read(this.stagingAt(offset), run.length), 'Failed to read samples from the audio FIFO');
|
|
870
|
+
}
|
|
871
|
+
offset += run.length;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Read the next samples into the head of the staging buffer synchronously.
|
|
876
|
+
* Synchronous version of readStaging.
|
|
877
|
+
*
|
|
878
|
+
* @param count - Number of samples to read
|
|
879
|
+
*
|
|
880
|
+
* @throws {FFmpegError} If the FIFO cannot be read
|
|
881
|
+
*
|
|
882
|
+
* @internal
|
|
883
|
+
*/
|
|
884
|
+
readStagingSync(count) {
|
|
885
|
+
if (this.silence.length === 0) {
|
|
886
|
+
FFmpegError.throwIfError(this.fifo.readSync(this.stagingPlanes, count), 'Failed to read samples from the audio FIFO');
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
let offset = 0;
|
|
890
|
+
for (const run of this.readRuns(count)) {
|
|
891
|
+
if (run.silent) {
|
|
892
|
+
this.fillSilence(offset, offset + run.length);
|
|
893
|
+
}
|
|
894
|
+
else {
|
|
895
|
+
FFmpegError.throwIfError(this.fifo.readSync(this.stagingAt(offset), run.length), 'Failed to read samples from the audio FIFO');
|
|
896
|
+
}
|
|
897
|
+
offset += run.length;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Staging planes starting at a sample offset.
|
|
902
|
+
*
|
|
903
|
+
* @param offset - First sample to write to
|
|
904
|
+
*
|
|
905
|
+
* @returns Plane buffers for a FIFO read
|
|
906
|
+
*
|
|
907
|
+
* @internal
|
|
908
|
+
*/
|
|
909
|
+
stagingAt(offset) {
|
|
910
|
+
if (offset === 0) {
|
|
911
|
+
return this.stagingPlanes;
|
|
912
|
+
}
|
|
913
|
+
const start = offset * this.sampleStride;
|
|
914
|
+
return this.stagingPlanes.map((plane) => plane.subarray(start));
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Convert a sample count to output time base ticks.
|
|
918
|
+
*
|
|
919
|
+
* @param samples - Number of samples
|
|
920
|
+
*
|
|
921
|
+
* @returns Duration in the output time base
|
|
922
|
+
*
|
|
923
|
+
* @internal
|
|
924
|
+
*/
|
|
925
|
+
ticks(samples) {
|
|
926
|
+
return this.countsInSamples ? BigInt(samples) : avRescaleQ(samples, this.sampleTimeBase, this.timeBase ?? this.sampleTimeBase);
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Convert output time base ticks to a sample count.
|
|
930
|
+
*
|
|
931
|
+
* @param ticks - Duration in the output time base
|
|
932
|
+
*
|
|
933
|
+
* @returns Number of samples (rounded)
|
|
934
|
+
*
|
|
935
|
+
* @internal
|
|
936
|
+
*/
|
|
937
|
+
samples(ticks) {
|
|
938
|
+
return Number(this.countsInSamples ? ticks : avRescaleQ(ticks, this.timeBase ?? this.sampleTimeBase, this.sampleTimeBase));
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Plane buffers of a pushed frame, starting at a sample offset.
|
|
942
|
+
*
|
|
943
|
+
* @param frame - Pushed frame
|
|
944
|
+
*
|
|
945
|
+
* @param skip - Samples to leave out at the head
|
|
946
|
+
*
|
|
947
|
+
* @returns Plane buffers to write to the FIFO
|
|
948
|
+
*
|
|
949
|
+
* @internal
|
|
950
|
+
*/
|
|
951
|
+
inputPlanes(frame, skip) {
|
|
952
|
+
const planes = frame.data;
|
|
953
|
+
if (skip === 0) {
|
|
954
|
+
return planes;
|
|
955
|
+
}
|
|
956
|
+
const offset = skip * this.sampleStride;
|
|
957
|
+
return planes.map((plane) => plane.subarray(offset));
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Stamp the reusable frame, advance the read position by one frame and hand out a clone.
|
|
961
|
+
*
|
|
962
|
+
* @param pts - Timestamp of the frame's first sample
|
|
963
|
+
*
|
|
964
|
+
* @returns Cloned output frame
|
|
965
|
+
*
|
|
966
|
+
* @throws {Error} If frame cloning fails (out of memory)
|
|
967
|
+
*
|
|
968
|
+
* @internal
|
|
969
|
+
*/
|
|
970
|
+
emitFrame(pts) {
|
|
971
|
+
this.frame.pts = pts;
|
|
972
|
+
this.headIndex += this.frameSize;
|
|
973
|
+
while (this.silence.length > 0 && this.silence[0].index + this.silence[0].length <= this.headIndex) {
|
|
974
|
+
this.silence.shift();
|
|
975
|
+
}
|
|
976
|
+
// Clone frame for user (like Decoder does)
|
|
977
|
+
const cloned = this.frame.clone();
|
|
978
|
+
if (!cloned) {
|
|
979
|
+
throw new Error('Failed to clone frame (out of memory)');
|
|
980
|
+
}
|
|
981
|
+
return cloned;
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Fill the staged sample range [fromSample, toSample) with silence.
|
|
985
|
+
*
|
|
986
|
+
* Planar frames carry one channel per plane; packed frames interleave all
|
|
987
|
+
* channels in plane 0.
|
|
988
|
+
*
|
|
989
|
+
* @param fromSample - First sample index to silence
|
|
990
|
+
*
|
|
991
|
+
* @param toSample - Sample index after the last one to silence
|
|
992
|
+
*
|
|
993
|
+
* @internal
|
|
994
|
+
*/
|
|
995
|
+
fillSilence(fromSample, toSample) {
|
|
996
|
+
for (const plane of this.stagingPlanes) {
|
|
997
|
+
plane.fill(this.silenceValue, fromSample * this.sampleStride, toSample * this.sampleStride);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Copy the staged samples into the reusable frame.
|
|
1002
|
+
*
|
|
1003
|
+
* A frame pulled earlier may still share the frame's buffer, so the buffer is
|
|
1004
|
+
* made writable first instead of overwriting the samples under that frame.
|
|
1005
|
+
*
|
|
1006
|
+
* @throws {FFmpegError} If the frame buffer cannot be made writable or filled
|
|
1007
|
+
*
|
|
1008
|
+
* @internal
|
|
1009
|
+
*/
|
|
1010
|
+
commitStaging() {
|
|
1011
|
+
FFmpegError.throwIfError(this.frame.makeWritable(), 'Failed to make frame writable');
|
|
1012
|
+
FFmpegError.throwIfError(this.frame.fromBuffer(this.staging), 'Failed to copy samples into frame');
|
|
273
1013
|
}
|
|
274
1014
|
/**
|
|
275
1015
|
* Free the buffer and all resources.
|