node-av 3.1.3 → 5.0.0
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 +88 -52
- package/binding.gyp +23 -11
- package/dist/api/audio-frame-buffer.d.ts +201 -0
- package/dist/api/audio-frame-buffer.js +275 -0
- package/dist/api/audio-frame-buffer.js.map +1 -0
- package/dist/api/bitstream-filter.d.ts +320 -78
- package/dist/api/bitstream-filter.js +684 -151
- package/dist/api/bitstream-filter.js.map +1 -1
- package/dist/api/constants.d.ts +44 -0
- package/dist/api/constants.js +45 -0
- package/dist/api/constants.js.map +1 -0
- package/dist/api/data/test_av1.ivf +0 -0
- package/dist/api/data/test_mjpeg.mjpeg +0 -0
- package/dist/api/data/test_vp8.ivf +0 -0
- package/dist/api/data/test_vp9.ivf +0 -0
- package/dist/api/decoder.d.ts +454 -77
- package/dist/api/decoder.js +1081 -271
- package/dist/api/decoder.js.map +1 -1
- package/dist/api/{media-input.d.ts → demuxer.d.ts} +295 -45
- package/dist/api/demuxer.js +1965 -0
- package/dist/api/demuxer.js.map +1 -0
- package/dist/api/encoder.d.ts +423 -132
- package/dist/api/encoder.js +1089 -240
- package/dist/api/encoder.js.map +1 -1
- package/dist/api/filter-complex.d.ts +769 -0
- package/dist/api/filter-complex.js +1596 -0
- package/dist/api/filter-complex.js.map +1 -0
- package/dist/api/filter-presets.d.ts +80 -5
- package/dist/api/filter-presets.js +117 -7
- package/dist/api/filter-presets.js.map +1 -1
- package/dist/api/filter.d.ts +561 -125
- package/dist/api/filter.js +1083 -274
- package/dist/api/filter.js.map +1 -1
- package/dist/api/{fmp4.d.ts → fmp4-stream.d.ts} +141 -140
- package/dist/api/fmp4-stream.js +539 -0
- package/dist/api/fmp4-stream.js.map +1 -0
- package/dist/api/hardware.d.ts +58 -6
- package/dist/api/hardware.js +127 -11
- package/dist/api/hardware.js.map +1 -1
- package/dist/api/index.d.ts +8 -4
- package/dist/api/index.js +17 -8
- package/dist/api/index.js.map +1 -1
- package/dist/api/io-stream.d.ts +6 -6
- package/dist/api/io-stream.js +5 -4
- package/dist/api/io-stream.js.map +1 -1
- package/dist/api/{media-output.d.ts → muxer.d.ts} +280 -66
- package/dist/api/muxer.js +1934 -0
- package/dist/api/muxer.js.map +1 -0
- package/dist/api/pipeline.d.ts +77 -29
- package/dist/api/pipeline.js +449 -439
- package/dist/api/pipeline.js.map +1 -1
- package/dist/api/rtp-stream.d.ts +312 -0
- package/dist/api/rtp-stream.js +630 -0
- package/dist/api/rtp-stream.js.map +1 -0
- package/dist/api/types.d.ts +533 -56
- package/dist/api/utilities/async-queue.d.ts +91 -0
- package/dist/api/utilities/async-queue.js +162 -0
- package/dist/api/utilities/async-queue.js.map +1 -0
- package/dist/api/utilities/audio-sample.d.ts +11 -1
- package/dist/api/utilities/audio-sample.js +10 -0
- package/dist/api/utilities/audio-sample.js.map +1 -1
- package/dist/api/utilities/channel-layout.d.ts +1 -0
- package/dist/api/utilities/channel-layout.js +1 -0
- package/dist/api/utilities/channel-layout.js.map +1 -1
- package/dist/api/utilities/image.d.ts +39 -1
- package/dist/api/utilities/image.js +38 -0
- package/dist/api/utilities/image.js.map +1 -1
- package/dist/api/utilities/index.d.ts +3 -0
- package/dist/api/utilities/index.js +6 -0
- package/dist/api/utilities/index.js.map +1 -1
- package/dist/api/utilities/media-type.d.ts +2 -1
- package/dist/api/utilities/media-type.js +1 -0
- package/dist/api/utilities/media-type.js.map +1 -1
- package/dist/api/utilities/pixel-format.d.ts +4 -1
- package/dist/api/utilities/pixel-format.js +3 -0
- package/dist/api/utilities/pixel-format.js.map +1 -1
- package/dist/api/utilities/sample-format.d.ts +6 -1
- package/dist/api/utilities/sample-format.js +5 -0
- package/dist/api/utilities/sample-format.js.map +1 -1
- package/dist/api/utilities/scheduler.d.ts +138 -0
- package/dist/api/utilities/scheduler.js +98 -0
- package/dist/api/utilities/scheduler.js.map +1 -0
- package/dist/api/utilities/streaming.d.ts +105 -15
- package/dist/api/utilities/streaming.js +201 -12
- package/dist/api/utilities/streaming.js.map +1 -1
- package/dist/api/utilities/timestamp.d.ts +15 -1
- package/dist/api/utilities/timestamp.js +14 -0
- package/dist/api/utilities/timestamp.js.map +1 -1
- package/dist/api/utilities/whisper-model.d.ts +310 -0
- package/dist/api/utilities/whisper-model.js +528 -0
- package/dist/api/utilities/whisper-model.js.map +1 -0
- package/dist/api/webrtc-stream.d.ts +288 -0
- package/dist/api/webrtc-stream.js +440 -0
- package/dist/api/webrtc-stream.js.map +1 -0
- package/dist/api/whisper.d.ts +324 -0
- package/dist/api/whisper.js +362 -0
- package/dist/api/whisper.js.map +1 -0
- package/dist/constants/constants.d.ts +54 -2
- package/dist/constants/constants.js +48 -1
- package/dist/constants/constants.js.map +1 -1
- package/dist/constants/encoders.d.ts +2 -1
- package/dist/constants/encoders.js +4 -3
- package/dist/constants/encoders.js.map +1 -1
- package/dist/constants/hardware.d.ts +26 -0
- package/dist/constants/hardware.js +27 -0
- package/dist/constants/hardware.js.map +1 -0
- package/dist/constants/index.d.ts +1 -0
- package/dist/constants/index.js +1 -0
- package/dist/constants/index.js.map +1 -1
- package/dist/ffmpeg/index.d.ts +3 -3
- package/dist/ffmpeg/index.js +3 -3
- package/dist/ffmpeg/utils.d.ts +27 -0
- package/dist/ffmpeg/utils.js +28 -16
- package/dist/ffmpeg/utils.js.map +1 -1
- package/dist/lib/binding.d.ts +22 -11
- package/dist/lib/binding.js.map +1 -1
- package/dist/lib/codec-context.d.ts +87 -0
- package/dist/lib/codec-context.js +125 -4
- package/dist/lib/codec-context.js.map +1 -1
- package/dist/lib/codec-parameters.d.ts +229 -1
- package/dist/lib/codec-parameters.js +264 -0
- package/dist/lib/codec-parameters.js.map +1 -1
- package/dist/lib/codec-parser.d.ts +23 -0
- package/dist/lib/codec-parser.js +25 -0
- package/dist/lib/codec-parser.js.map +1 -1
- package/dist/lib/codec.d.ts +26 -4
- package/dist/lib/codec.js +35 -0
- package/dist/lib/codec.js.map +1 -1
- package/dist/lib/dictionary.js +1 -0
- package/dist/lib/dictionary.js.map +1 -1
- package/dist/lib/error.js +1 -1
- package/dist/lib/error.js.map +1 -1
- package/dist/lib/fifo.d.ts +416 -0
- package/dist/lib/fifo.js +453 -0
- package/dist/lib/fifo.js.map +1 -0
- package/dist/lib/filter-context.d.ts +52 -11
- package/dist/lib/filter-context.js +56 -12
- package/dist/lib/filter-context.js.map +1 -1
- package/dist/lib/filter-graph.d.ts +9 -0
- package/dist/lib/filter-graph.js +13 -0
- package/dist/lib/filter-graph.js.map +1 -1
- package/dist/lib/filter.d.ts +21 -0
- package/dist/lib/filter.js +28 -0
- package/dist/lib/filter.js.map +1 -1
- package/dist/lib/format-context.d.ts +48 -14
- package/dist/lib/format-context.js +76 -7
- package/dist/lib/format-context.js.map +1 -1
- package/dist/lib/frame.d.ts +264 -1
- package/dist/lib/frame.js +351 -1
- package/dist/lib/frame.js.map +1 -1
- package/dist/lib/hardware-device-context.d.ts +3 -2
- package/dist/lib/hardware-device-context.js.map +1 -1
- package/dist/lib/index.d.ts +2 -0
- package/dist/lib/index.js +4 -0
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/input-format.d.ts +21 -0
- package/dist/lib/input-format.js +42 -2
- package/dist/lib/input-format.js.map +1 -1
- package/dist/lib/native-types.d.ts +76 -27
- package/dist/lib/option.d.ts +25 -13
- package/dist/lib/option.js +28 -0
- package/dist/lib/option.js.map +1 -1
- package/dist/lib/output-format.d.ts +22 -1
- package/dist/lib/output-format.js +28 -0
- package/dist/lib/output-format.js.map +1 -1
- package/dist/lib/packet.d.ts +35 -0
- package/dist/lib/packet.js +52 -2
- package/dist/lib/packet.js.map +1 -1
- package/dist/lib/rational.d.ts +18 -0
- package/dist/lib/rational.js +19 -0
- package/dist/lib/rational.js.map +1 -1
- package/dist/lib/stream.d.ts +126 -0
- package/dist/lib/stream.js +188 -5
- package/dist/lib/stream.js.map +1 -1
- package/dist/lib/sync-queue.d.ts +179 -0
- package/dist/lib/sync-queue.js +197 -0
- package/dist/lib/sync-queue.js.map +1 -0
- package/dist/lib/types.d.ts +49 -1
- package/dist/lib/utilities.d.ts +281 -53
- package/dist/lib/utilities.js +298 -55
- package/dist/lib/utilities.js.map +1 -1
- package/install/check.js +2 -2
- package/package.json +37 -26
- package/dist/api/fmp4.js +0 -710
- package/dist/api/fmp4.js.map +0 -1
- package/dist/api/media-input.js +0 -1075
- package/dist/api/media-input.js.map +0 -1
- package/dist/api/media-output.js +0 -1040
- package/dist/api/media-output.js.map +0 -1
- package/dist/api/webrtc.d.ts +0 -664
- package/dist/api/webrtc.js +0 -1132
- package/dist/api/webrtc.js.map +0 -1
package/dist/api/media-input.js
DELETED
|
@@ -1,1075 +0,0 @@
|
|
|
1
|
-
import { closeSync, openSync, readSync } from 'fs';
|
|
2
|
-
import { open } from 'fs/promises';
|
|
3
|
-
import { resolve } from 'path';
|
|
4
|
-
import { AVFLAG_NONE, AVFMT_FLAG_CUSTOM_IO, AVFMT_FLAG_NONBLOCK, AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_VIDEO } from '../constants/constants.js';
|
|
5
|
-
import { avGetPixFmtName, avGetSampleFmtName, Dictionary, FFmpegError, FormatContext, InputFormat, IOContext, Packet, Rational } from '../lib/index.js';
|
|
6
|
-
import { IOStream } from './io-stream.js';
|
|
7
|
-
/**
|
|
8
|
-
* High-level media input for reading and demuxing media files.
|
|
9
|
-
*
|
|
10
|
-
* Provides simplified access to media streams, packets, and metadata.
|
|
11
|
-
* Handles file opening, format detection, and stream information extraction.
|
|
12
|
-
* Supports files, URLs, buffers, and raw data input with automatic cleanup.
|
|
13
|
-
* Essential component for media processing pipelines and transcoding.
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* ```typescript
|
|
17
|
-
* import { MediaInput } from 'node-av/api';
|
|
18
|
-
*
|
|
19
|
-
* // Open media file
|
|
20
|
-
* await using input = await MediaInput.open('video.mp4');
|
|
21
|
-
* console.log(`Format: ${input.formatName}`);
|
|
22
|
-
* console.log(`Duration: ${input.duration}s`);
|
|
23
|
-
*
|
|
24
|
-
* // Process packets
|
|
25
|
-
* for await (const packet of input.packets()) {
|
|
26
|
-
* console.log(`Packet from stream ${packet.streamIndex}`);
|
|
27
|
-
* packet.free();
|
|
28
|
-
* }
|
|
29
|
-
* ```
|
|
30
|
-
*
|
|
31
|
-
* @example
|
|
32
|
-
* ```typescript
|
|
33
|
-
* // From buffer
|
|
34
|
-
* const buffer = await fs.readFile('video.mp4');
|
|
35
|
-
* await using input = await MediaInput.open(buffer);
|
|
36
|
-
*
|
|
37
|
-
* // Access streams
|
|
38
|
-
* const videoStream = input.video();
|
|
39
|
-
* const audioStream = input.audio();
|
|
40
|
-
* ```
|
|
41
|
-
*
|
|
42
|
-
* @see {@link MediaOutput} For writing media files
|
|
43
|
-
* @see {@link Decoder} For decoding packets to frames
|
|
44
|
-
* @see {@link FormatContext} For low-level API
|
|
45
|
-
*/
|
|
46
|
-
export class MediaInput {
|
|
47
|
-
formatContext;
|
|
48
|
-
_streams = [];
|
|
49
|
-
ioContext;
|
|
50
|
-
isClosed = false;
|
|
51
|
-
/**
|
|
52
|
-
* @param formatContext - Opened format context
|
|
53
|
-
*
|
|
54
|
-
* @param ioContext - Optional IO context for custom I/O (e.g., from Buffer)
|
|
55
|
-
*
|
|
56
|
-
* @internal
|
|
57
|
-
*/
|
|
58
|
-
constructor(formatContext, ioContext) {
|
|
59
|
-
this.formatContext = formatContext;
|
|
60
|
-
this.ioContext = ioContext;
|
|
61
|
-
this._streams = formatContext.streams ?? [];
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Probe media format without fully opening the file.
|
|
65
|
-
*
|
|
66
|
-
* Detects format by analyzing file headers and content.
|
|
67
|
-
* Useful for format validation before processing.
|
|
68
|
-
*
|
|
69
|
-
* Direct mapping to av_probe_input_format().
|
|
70
|
-
*
|
|
71
|
-
* @param input - File path or buffer to probe
|
|
72
|
-
*
|
|
73
|
-
* @returns Format information or null if unrecognized
|
|
74
|
-
*
|
|
75
|
-
* @example
|
|
76
|
-
* ```typescript
|
|
77
|
-
* const info = await MediaInput.probeFormat('video.mp4');
|
|
78
|
-
* if (info) {
|
|
79
|
-
* console.log(`Format: ${info.format}`);
|
|
80
|
-
* console.log(`Confidence: ${info.confidence}%`);
|
|
81
|
-
* }
|
|
82
|
-
* ```
|
|
83
|
-
*
|
|
84
|
-
* @example
|
|
85
|
-
* ```typescript
|
|
86
|
-
* // Probe from buffer
|
|
87
|
-
* const buffer = await fs.readFile('video.webm');
|
|
88
|
-
* const info = await MediaInput.probeFormat(buffer);
|
|
89
|
-
* console.log(`MIME type: ${info?.mimeType}`);
|
|
90
|
-
* ```
|
|
91
|
-
*
|
|
92
|
-
* @see {@link InputFormat.probe} For low-level probing
|
|
93
|
-
*/
|
|
94
|
-
static async probeFormat(input) {
|
|
95
|
-
try {
|
|
96
|
-
if (Buffer.isBuffer(input)) {
|
|
97
|
-
// Probe from buffer
|
|
98
|
-
const format = InputFormat.probe(input);
|
|
99
|
-
if (!format) {
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
return {
|
|
103
|
-
format: format.name ?? 'unknown',
|
|
104
|
-
longName: format.longName ?? undefined,
|
|
105
|
-
extensions: format.extensions ?? undefined,
|
|
106
|
-
mimeType: format.mimeType ?? undefined,
|
|
107
|
-
confidence: 100, // Direct probe always has high confidence
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
else {
|
|
111
|
-
// For files, read first part and probe
|
|
112
|
-
let fileHandle;
|
|
113
|
-
try {
|
|
114
|
-
fileHandle = await open(input, 'r');
|
|
115
|
-
// Read first 64KB for probing
|
|
116
|
-
const buffer = Buffer.alloc(65536);
|
|
117
|
-
const { bytesRead } = await fileHandle.read(buffer, 0, 65536, 0);
|
|
118
|
-
const probeBuffer = buffer.subarray(0, bytesRead);
|
|
119
|
-
const format = InputFormat.probe(probeBuffer, input);
|
|
120
|
-
if (!format) {
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
return {
|
|
124
|
-
format: format.name ?? 'unknown',
|
|
125
|
-
longName: format.longName ?? undefined,
|
|
126
|
-
extensions: format.extensions ?? undefined,
|
|
127
|
-
mimeType: format.mimeType ?? undefined,
|
|
128
|
-
confidence: 90, // File-based probe with filename hint
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
catch {
|
|
132
|
-
// If file reading fails, return null
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
finally {
|
|
136
|
-
await fileHandle?.close();
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
catch {
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
/**
|
|
145
|
-
* Probe media format without fully opening the file synchronously.
|
|
146
|
-
* Synchronous version of probeFormat.
|
|
147
|
-
*
|
|
148
|
-
* Detects format by analyzing file headers and content.
|
|
149
|
-
* Useful for format validation before processing.
|
|
150
|
-
*
|
|
151
|
-
* Direct mapping to av_probe_input_format().
|
|
152
|
-
*
|
|
153
|
-
* @param input - File path or buffer to probe
|
|
154
|
-
*
|
|
155
|
-
* @returns Format information or null if unrecognized
|
|
156
|
-
*
|
|
157
|
-
* @example
|
|
158
|
-
* ```typescript
|
|
159
|
-
* const info = MediaInput.probeFormatSync('video.mp4');
|
|
160
|
-
* if (info) {
|
|
161
|
-
* console.log(`Format: ${info.format}`);
|
|
162
|
-
* console.log(`Confidence: ${info.confidence}%`);
|
|
163
|
-
* }
|
|
164
|
-
* ```
|
|
165
|
-
*
|
|
166
|
-
* @example
|
|
167
|
-
* ```typescript
|
|
168
|
-
* // Probe from buffer
|
|
169
|
-
* const buffer = fs.readFileSync('video.webm');
|
|
170
|
-
* const info = MediaInput.probeFormatSync(buffer);
|
|
171
|
-
* console.log(`MIME type: ${info?.mimeType}`);
|
|
172
|
-
* ```
|
|
173
|
-
*
|
|
174
|
-
* @see {@link probeFormat} For async version
|
|
175
|
-
*/
|
|
176
|
-
static probeFormatSync(input) {
|
|
177
|
-
try {
|
|
178
|
-
if (Buffer.isBuffer(input)) {
|
|
179
|
-
// Probe from buffer
|
|
180
|
-
const format = InputFormat.probe(input);
|
|
181
|
-
if (!format) {
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
return {
|
|
185
|
-
format: format.name ?? 'unknown',
|
|
186
|
-
longName: format.longName ?? undefined,
|
|
187
|
-
extensions: format.extensions ?? undefined,
|
|
188
|
-
mimeType: format.mimeType ?? undefined,
|
|
189
|
-
confidence: 100, // Direct probe always has high confidence
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
else {
|
|
193
|
-
// For files, read first part and probe
|
|
194
|
-
let fd;
|
|
195
|
-
try {
|
|
196
|
-
fd = openSync(input, 'r');
|
|
197
|
-
// Read first 64KB for probing
|
|
198
|
-
const buffer = Buffer.alloc(65536);
|
|
199
|
-
const bytesRead = readSync(fd, buffer, 0, 65536, 0);
|
|
200
|
-
const probeBuffer = buffer.subarray(0, bytesRead);
|
|
201
|
-
const format = InputFormat.probe(probeBuffer, input);
|
|
202
|
-
if (!format) {
|
|
203
|
-
return null;
|
|
204
|
-
}
|
|
205
|
-
return {
|
|
206
|
-
format: format.name ?? 'unknown',
|
|
207
|
-
longName: format.longName ?? undefined,
|
|
208
|
-
extensions: format.extensions ?? undefined,
|
|
209
|
-
mimeType: format.mimeType ?? undefined,
|
|
210
|
-
confidence: 90, // File-based probe with filename hint
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
catch {
|
|
214
|
-
// If file reading fails, return null
|
|
215
|
-
return null;
|
|
216
|
-
}
|
|
217
|
-
finally {
|
|
218
|
-
if (fd !== undefined)
|
|
219
|
-
closeSync(fd);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
catch {
|
|
224
|
-
return null;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
static async open(input, options = {}) {
|
|
228
|
-
// Check if input is raw data
|
|
229
|
-
if (typeof input === 'object' && 'type' in input && ('width' in input || 'sampleRate' in input)) {
|
|
230
|
-
// Build options for raw data
|
|
231
|
-
const rawOptions = {
|
|
232
|
-
bufferSize: options.bufferSize,
|
|
233
|
-
format: options.format ?? (input.type === 'video' ? 'rawvideo' : 's16le'),
|
|
234
|
-
options: {
|
|
235
|
-
...options.options,
|
|
236
|
-
},
|
|
237
|
-
};
|
|
238
|
-
if (input.type === 'video') {
|
|
239
|
-
rawOptions.options = {
|
|
240
|
-
...rawOptions.options,
|
|
241
|
-
video_size: `${input.width}x${input.height}`,
|
|
242
|
-
pixel_format: avGetPixFmtName(input.pixelFormat) ?? 'yuv420p',
|
|
243
|
-
framerate: new Rational(input.frameRate.num, input.frameRate.den).toString(),
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
else {
|
|
247
|
-
rawOptions.options = {
|
|
248
|
-
...rawOptions.options,
|
|
249
|
-
sample_rate: input.sampleRate,
|
|
250
|
-
channels: input.channels,
|
|
251
|
-
sample_fmt: avGetSampleFmtName(input.sampleFormat) ?? 's16le',
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
// Open with the raw data source
|
|
255
|
-
return MediaInput.open(input.input, rawOptions);
|
|
256
|
-
}
|
|
257
|
-
// Original implementation for non-raw data
|
|
258
|
-
const formatContext = new FormatContext();
|
|
259
|
-
let ioContext;
|
|
260
|
-
let optionsDict = null;
|
|
261
|
-
let inputFormat = null;
|
|
262
|
-
try {
|
|
263
|
-
// Create options dictionary if options are provided
|
|
264
|
-
if (options.options) {
|
|
265
|
-
optionsDict = Dictionary.fromObject(options.options);
|
|
266
|
-
}
|
|
267
|
-
// Find input format if specified
|
|
268
|
-
if (options.format) {
|
|
269
|
-
inputFormat = InputFormat.findInputFormat(options.format);
|
|
270
|
-
if (!inputFormat) {
|
|
271
|
-
throw new Error(`Input format '${options.format}' not found`);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
if (typeof input === 'string') {
|
|
275
|
-
// File path or URL - resolve relative paths to absolute
|
|
276
|
-
// Check if it's a URL (starts with protocol://) or a file path
|
|
277
|
-
const isUrl = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(input);
|
|
278
|
-
const resolvedInput = isUrl ? input : resolve(input);
|
|
279
|
-
const ret = await formatContext.openInput(resolvedInput, inputFormat, optionsDict);
|
|
280
|
-
FFmpegError.throwIfError(ret, 'Failed to open input');
|
|
281
|
-
formatContext.setFlags(AVFMT_FLAG_NONBLOCK);
|
|
282
|
-
}
|
|
283
|
-
else if (Buffer.isBuffer(input)) {
|
|
284
|
-
// Validate buffer is not empty
|
|
285
|
-
if (input.length === 0) {
|
|
286
|
-
throw new Error('Cannot open media from empty buffer');
|
|
287
|
-
}
|
|
288
|
-
// From buffer - allocate context first for custom I/O
|
|
289
|
-
formatContext.allocContext();
|
|
290
|
-
ioContext = IOStream.create(input, { bufferSize: options.bufferSize });
|
|
291
|
-
formatContext.pb = ioContext;
|
|
292
|
-
const ret = await formatContext.openInput('', inputFormat, optionsDict);
|
|
293
|
-
FFmpegError.throwIfError(ret, 'Failed to open input from buffer');
|
|
294
|
-
}
|
|
295
|
-
else if (typeof input === 'object' && 'read' in input) {
|
|
296
|
-
// Custom I/O with callbacks - format is required
|
|
297
|
-
if (!options.format) {
|
|
298
|
-
throw new Error('Format must be specified for custom I/O');
|
|
299
|
-
}
|
|
300
|
-
// Allocate context first for custom I/O
|
|
301
|
-
formatContext.allocContext();
|
|
302
|
-
// Setup custom I/O with callbacks
|
|
303
|
-
ioContext = new IOContext();
|
|
304
|
-
ioContext.allocContextWithCallbacks(options.bufferSize ?? 8192, 0, input.read, null, input.seek);
|
|
305
|
-
formatContext.pb = ioContext;
|
|
306
|
-
formatContext.flags = AVFMT_FLAG_CUSTOM_IO;
|
|
307
|
-
const ret = await formatContext.openInput('', inputFormat, optionsDict);
|
|
308
|
-
FFmpegError.throwIfError(ret, 'Failed to open input from custom I/O');
|
|
309
|
-
}
|
|
310
|
-
else {
|
|
311
|
-
throw new TypeError('Invalid input type. Expected file path, URL, Buffer, or IOInputCallbacks');
|
|
312
|
-
}
|
|
313
|
-
// Find stream information
|
|
314
|
-
const ret = await formatContext.findStreamInfo(null);
|
|
315
|
-
FFmpegError.throwIfError(ret, 'Failed to find stream info');
|
|
316
|
-
const mediaInput = new MediaInput(formatContext, ioContext);
|
|
317
|
-
return mediaInput;
|
|
318
|
-
}
|
|
319
|
-
catch (error) {
|
|
320
|
-
// Clean up only on error
|
|
321
|
-
if (ioContext) {
|
|
322
|
-
// Clear the pb reference first
|
|
323
|
-
formatContext.pb = null;
|
|
324
|
-
// Free the IOContext (for both custom I/O and buffer-based I/O)
|
|
325
|
-
ioContext.freeContext();
|
|
326
|
-
}
|
|
327
|
-
// Clean up FormatContext
|
|
328
|
-
await formatContext.closeInput();
|
|
329
|
-
throw error;
|
|
330
|
-
}
|
|
331
|
-
finally {
|
|
332
|
-
// Clean up options dictionary
|
|
333
|
-
if (optionsDict) {
|
|
334
|
-
optionsDict.free();
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
static openSync(input, options = {}) {
|
|
339
|
-
// Check if input is raw data
|
|
340
|
-
if (typeof input === 'object' && 'type' in input && ('width' in input || 'sampleRate' in input)) {
|
|
341
|
-
// Build options for raw data
|
|
342
|
-
const rawOptions = {
|
|
343
|
-
bufferSize: options.bufferSize,
|
|
344
|
-
format: options.format ?? (input.type === 'video' ? 'rawvideo' : 's16le'),
|
|
345
|
-
options: {
|
|
346
|
-
...options.options,
|
|
347
|
-
},
|
|
348
|
-
};
|
|
349
|
-
if (input.type === 'video') {
|
|
350
|
-
rawOptions.options = {
|
|
351
|
-
...rawOptions.options,
|
|
352
|
-
video_size: `${input.width}x${input.height}`,
|
|
353
|
-
pixel_format: avGetPixFmtName(input.pixelFormat) ?? 'yuv420p',
|
|
354
|
-
framerate: new Rational(input.frameRate.num, input.frameRate.den).toString(),
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
else {
|
|
358
|
-
rawOptions.options = {
|
|
359
|
-
...rawOptions.options,
|
|
360
|
-
sample_rate: input.sampleRate,
|
|
361
|
-
channels: input.channels,
|
|
362
|
-
sample_fmt: avGetSampleFmtName(input.sampleFormat) ?? 's16le',
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
// Open with the raw data source
|
|
366
|
-
return MediaInput.openSync(input.input, rawOptions);
|
|
367
|
-
}
|
|
368
|
-
// Original implementation for non-raw data
|
|
369
|
-
const formatContext = new FormatContext();
|
|
370
|
-
let ioContext;
|
|
371
|
-
let optionsDict = null;
|
|
372
|
-
let inputFormat = null;
|
|
373
|
-
try {
|
|
374
|
-
// Create options dictionary if options are provided
|
|
375
|
-
if (options.options) {
|
|
376
|
-
optionsDict = Dictionary.fromObject(options.options);
|
|
377
|
-
}
|
|
378
|
-
// Find input format if specified
|
|
379
|
-
if (options.format) {
|
|
380
|
-
inputFormat = InputFormat.findInputFormat(options.format);
|
|
381
|
-
if (!inputFormat) {
|
|
382
|
-
throw new Error(`Input format '${options.format}' not found`);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
if (typeof input === 'string') {
|
|
386
|
-
// File path or URL - resolve relative paths to absolute
|
|
387
|
-
// Check if it's a URL (starts with protocol://) or a file path
|
|
388
|
-
const isUrl = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(input);
|
|
389
|
-
const resolvedInput = isUrl ? input : resolve(input);
|
|
390
|
-
const ret = formatContext.openInputSync(resolvedInput, inputFormat, optionsDict);
|
|
391
|
-
FFmpegError.throwIfError(ret, 'Failed to open input');
|
|
392
|
-
formatContext.setFlags(AVFMT_FLAG_NONBLOCK);
|
|
393
|
-
}
|
|
394
|
-
else if (Buffer.isBuffer(input)) {
|
|
395
|
-
// Validate buffer is not empty
|
|
396
|
-
if (input.length === 0) {
|
|
397
|
-
throw new Error('Cannot open media from empty buffer');
|
|
398
|
-
}
|
|
399
|
-
// From buffer - allocate context first for custom I/O
|
|
400
|
-
formatContext.allocContext();
|
|
401
|
-
ioContext = IOStream.create(input, { bufferSize: options.bufferSize });
|
|
402
|
-
formatContext.pb = ioContext;
|
|
403
|
-
const ret = formatContext.openInputSync('', inputFormat, optionsDict);
|
|
404
|
-
FFmpegError.throwIfError(ret, 'Failed to open input from buffer');
|
|
405
|
-
}
|
|
406
|
-
else if (typeof input === 'object' && 'read' in input) {
|
|
407
|
-
// Custom I/O with callbacks - format is required
|
|
408
|
-
if (!options.format) {
|
|
409
|
-
throw new Error('Format must be specified for custom I/O');
|
|
410
|
-
}
|
|
411
|
-
// Allocate context first for custom I/O
|
|
412
|
-
formatContext.allocContext();
|
|
413
|
-
// Setup custom I/O with callbacks
|
|
414
|
-
ioContext = new IOContext();
|
|
415
|
-
ioContext.allocContextWithCallbacks(options.bufferSize ?? 8192, 0, input.read, null, input.seek);
|
|
416
|
-
formatContext.pb = ioContext;
|
|
417
|
-
formatContext.flags = AVFMT_FLAG_CUSTOM_IO;
|
|
418
|
-
const ret = formatContext.openInputSync('', inputFormat, optionsDict);
|
|
419
|
-
FFmpegError.throwIfError(ret, 'Failed to open input from custom I/O');
|
|
420
|
-
}
|
|
421
|
-
else {
|
|
422
|
-
throw new TypeError('Invalid input type. Expected file path, URL, Buffer, or IOInputCallbacks');
|
|
423
|
-
}
|
|
424
|
-
// Find stream information
|
|
425
|
-
const ret = formatContext.findStreamInfoSync(null);
|
|
426
|
-
FFmpegError.throwIfError(ret, 'Failed to find stream info');
|
|
427
|
-
const mediaInput = new MediaInput(formatContext, ioContext);
|
|
428
|
-
return mediaInput;
|
|
429
|
-
}
|
|
430
|
-
catch (error) {
|
|
431
|
-
// Clean up only on error
|
|
432
|
-
if (ioContext) {
|
|
433
|
-
// Clear the pb reference first
|
|
434
|
-
formatContext.pb = null;
|
|
435
|
-
// Free the IOContext (for both custom I/O and buffer-based I/O)
|
|
436
|
-
ioContext.freeContext();
|
|
437
|
-
}
|
|
438
|
-
// Clean up FormatContext
|
|
439
|
-
formatContext.closeInputSync();
|
|
440
|
-
throw error;
|
|
441
|
-
}
|
|
442
|
-
finally {
|
|
443
|
-
// Clean up options dictionary
|
|
444
|
-
if (optionsDict) {
|
|
445
|
-
optionsDict.free();
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
/**
|
|
450
|
-
* Check if input is open.
|
|
451
|
-
*
|
|
452
|
-
* @example
|
|
453
|
-
* ```typescript
|
|
454
|
-
* if (!input.isInputOpen) {
|
|
455
|
-
* console.log('Input is not open');
|
|
456
|
-
* }
|
|
457
|
-
* ```
|
|
458
|
-
*/
|
|
459
|
-
get isInputOpen() {
|
|
460
|
-
return !this.isClosed;
|
|
461
|
-
}
|
|
462
|
-
/**
|
|
463
|
-
* Get all streams in the media.
|
|
464
|
-
*
|
|
465
|
-
* @example
|
|
466
|
-
* ```typescript
|
|
467
|
-
* for (const stream of input.streams) {
|
|
468
|
-
* console.log(`Stream ${stream.index}: ${stream.codecpar.codecType}`);
|
|
469
|
-
* }
|
|
470
|
-
* ```
|
|
471
|
-
*/
|
|
472
|
-
get streams() {
|
|
473
|
-
return this._streams;
|
|
474
|
-
}
|
|
475
|
-
/**
|
|
476
|
-
* Get media duration in seconds.
|
|
477
|
-
*
|
|
478
|
-
* Returns 0 if duration is unknown or not available or input is closed.
|
|
479
|
-
*
|
|
480
|
-
* @example
|
|
481
|
-
* ```typescript
|
|
482
|
-
* console.log(`Duration: ${input.duration} seconds`);
|
|
483
|
-
* ```
|
|
484
|
-
*/
|
|
485
|
-
get duration() {
|
|
486
|
-
if (this.isClosed) {
|
|
487
|
-
return 0;
|
|
488
|
-
}
|
|
489
|
-
const duration = this.formatContext.duration;
|
|
490
|
-
if (!duration || duration <= 0) {
|
|
491
|
-
return 0;
|
|
492
|
-
}
|
|
493
|
-
// Convert from AV_TIME_BASE (microseconds) to seconds
|
|
494
|
-
return Number(duration) / 1000000;
|
|
495
|
-
}
|
|
496
|
-
/**
|
|
497
|
-
* Get media bitrate in kilobits per second.
|
|
498
|
-
*
|
|
499
|
-
* Returns 0 if bitrate is unknown or not available or input is closed.
|
|
500
|
-
*
|
|
501
|
-
* @example
|
|
502
|
-
* ```typescript
|
|
503
|
-
* console.log(`Bitrate: ${input.bitRate} kbps`);
|
|
504
|
-
* ```
|
|
505
|
-
*/
|
|
506
|
-
get bitRate() {
|
|
507
|
-
if (this.isClosed) {
|
|
508
|
-
return 0;
|
|
509
|
-
}
|
|
510
|
-
const bitrate = this.formatContext.bitRate;
|
|
511
|
-
if (!bitrate || bitrate <= 0) {
|
|
512
|
-
return 0;
|
|
513
|
-
}
|
|
514
|
-
// Convert from bits per second to kilobits per second
|
|
515
|
-
return Number(bitrate) / 1000;
|
|
516
|
-
}
|
|
517
|
-
/**
|
|
518
|
-
* Get media metadata.
|
|
519
|
-
*
|
|
520
|
-
* Returns all metadata tags as key-value pairs.
|
|
521
|
-
*
|
|
522
|
-
* @example
|
|
523
|
-
* ```typescript
|
|
524
|
-
* const metadata = input.metadata;
|
|
525
|
-
* console.log(`Title: ${metadata.title}`);
|
|
526
|
-
* console.log(`Artist: ${metadata.artist}`);
|
|
527
|
-
* ```
|
|
528
|
-
*/
|
|
529
|
-
get metadata() {
|
|
530
|
-
if (this.isClosed) {
|
|
531
|
-
return {};
|
|
532
|
-
}
|
|
533
|
-
return this.formatContext.metadata?.getAll() ?? {};
|
|
534
|
-
}
|
|
535
|
-
/**
|
|
536
|
-
* Get format name.
|
|
537
|
-
*
|
|
538
|
-
* Returns 'unknown' if input is closed or format is not available.
|
|
539
|
-
*
|
|
540
|
-
* @example
|
|
541
|
-
* ```typescript
|
|
542
|
-
* console.log(`Format: ${input.formatName}`); // "mov,mp4,m4a,3gp,3g2,mj2"
|
|
543
|
-
* ```
|
|
544
|
-
*/
|
|
545
|
-
get formatName() {
|
|
546
|
-
if (this.isClosed) {
|
|
547
|
-
return 'unknown';
|
|
548
|
-
}
|
|
549
|
-
return this.formatContext.iformat?.name ?? 'unknown';
|
|
550
|
-
}
|
|
551
|
-
/**
|
|
552
|
-
* Get format long name.
|
|
553
|
-
*
|
|
554
|
-
* Returns 'Unknown Format' if input is closed or format is not available.
|
|
555
|
-
*
|
|
556
|
-
* @example
|
|
557
|
-
* ```typescript
|
|
558
|
-
* console.log(`Format: ${input.formatLongName}`); // "QuickTime / MOV"
|
|
559
|
-
* ```
|
|
560
|
-
*/
|
|
561
|
-
get formatLongName() {
|
|
562
|
-
if (this.isClosed) {
|
|
563
|
-
return 'Unknown Format';
|
|
564
|
-
}
|
|
565
|
-
return this.formatContext.iformat?.longName ?? 'Unknown Format';
|
|
566
|
-
}
|
|
567
|
-
/**
|
|
568
|
-
* Get MIME type of the input format.
|
|
569
|
-
*
|
|
570
|
-
* Returns null if input is closed or format is not available.
|
|
571
|
-
*
|
|
572
|
-
* @example
|
|
573
|
-
* ```typescript
|
|
574
|
-
* console.log(`MIME Type: ${input.mimeType}`); // "video/mp4"
|
|
575
|
-
* ```
|
|
576
|
-
*/
|
|
577
|
-
get mimeType() {
|
|
578
|
-
if (this.isClosed) {
|
|
579
|
-
return null;
|
|
580
|
-
}
|
|
581
|
-
return this.formatContext.iformat?.mimeType ?? null;
|
|
582
|
-
}
|
|
583
|
-
/**
|
|
584
|
-
* Get input stream by index.
|
|
585
|
-
*
|
|
586
|
-
* Returns the stream at the specified index.
|
|
587
|
-
*
|
|
588
|
-
* @param index - Stream index
|
|
589
|
-
*
|
|
590
|
-
* @returns Stream or undefined if index is invalid
|
|
591
|
-
*
|
|
592
|
-
* @example
|
|
593
|
-
* ```typescript
|
|
594
|
-
* const input = await MediaInput.open('input.mp4');
|
|
595
|
-
*
|
|
596
|
-
* // Get the input stream to inspect codec parameters
|
|
597
|
-
* const stream = input.getStream(1); // Get stream at index 1
|
|
598
|
-
* if (stream) {
|
|
599
|
-
* console.log(`Input codec: ${stream.codecpar.codecId}`);
|
|
600
|
-
* }
|
|
601
|
-
* ```
|
|
602
|
-
*
|
|
603
|
-
* @see {@link video} For getting video streams
|
|
604
|
-
* @see {@link audio} For getting audio streams
|
|
605
|
-
*/
|
|
606
|
-
getStream(index) {
|
|
607
|
-
const streams = this.formatContext.streams;
|
|
608
|
-
if (!streams || index < 0 || index >= streams.length) {
|
|
609
|
-
return undefined;
|
|
610
|
-
}
|
|
611
|
-
return streams[index];
|
|
612
|
-
}
|
|
613
|
-
/**
|
|
614
|
-
* Get video stream by index.
|
|
615
|
-
*
|
|
616
|
-
* Returns the nth video stream (0-based index).
|
|
617
|
-
* Returns undefined if stream doesn't exist.
|
|
618
|
-
*
|
|
619
|
-
* @param index - Video stream index (default: 0)
|
|
620
|
-
*
|
|
621
|
-
* @returns Video stream or undefined
|
|
622
|
-
*
|
|
623
|
-
* @example
|
|
624
|
-
* ```typescript
|
|
625
|
-
* const videoStream = input.video();
|
|
626
|
-
* if (videoStream) {
|
|
627
|
-
* console.log(`Video: ${videoStream.codecpar.width}x${videoStream.codecpar.height}`);
|
|
628
|
-
* }
|
|
629
|
-
* ```
|
|
630
|
-
*
|
|
631
|
-
* @example
|
|
632
|
-
* ```typescript
|
|
633
|
-
* // Get second video stream
|
|
634
|
-
* const secondVideo = input.video(1);
|
|
635
|
-
* ```
|
|
636
|
-
*
|
|
637
|
-
* @see {@link audio} For audio streams
|
|
638
|
-
* @see {@link findBestStream} For automatic selection
|
|
639
|
-
*/
|
|
640
|
-
video(index = 0) {
|
|
641
|
-
const streams = this._streams.filter((s) => s.codecpar.codecType === AVMEDIA_TYPE_VIDEO);
|
|
642
|
-
return streams[index];
|
|
643
|
-
}
|
|
644
|
-
/**
|
|
645
|
-
* Get audio stream by index.
|
|
646
|
-
*
|
|
647
|
-
* Returns the nth audio stream (0-based index).
|
|
648
|
-
* Returns undefined if stream doesn't exist.
|
|
649
|
-
*
|
|
650
|
-
* @param index - Audio stream index (default: 0)
|
|
651
|
-
*
|
|
652
|
-
* @returns Audio stream or undefined
|
|
653
|
-
*
|
|
654
|
-
* @example
|
|
655
|
-
* ```typescript
|
|
656
|
-
* const audioStream = input.audio();
|
|
657
|
-
* if (audioStream) {
|
|
658
|
-
* console.log(`Audio: ${audioStream.codecpar.sampleRate}Hz`);
|
|
659
|
-
* }
|
|
660
|
-
* ```
|
|
661
|
-
*
|
|
662
|
-
* @example
|
|
663
|
-
* ```typescript
|
|
664
|
-
* // Get second audio stream
|
|
665
|
-
* const secondAudio = input.audio(1);
|
|
666
|
-
* ```
|
|
667
|
-
*
|
|
668
|
-
* @see {@link video} For video streams
|
|
669
|
-
* @see {@link findBestStream} For automatic selection
|
|
670
|
-
*/
|
|
671
|
-
audio(index = 0) {
|
|
672
|
-
const streams = this._streams.filter((s) => s.codecpar.codecType === AVMEDIA_TYPE_AUDIO);
|
|
673
|
-
return streams[index];
|
|
674
|
-
}
|
|
675
|
-
/**
|
|
676
|
-
* Get input format details.
|
|
677
|
-
*
|
|
678
|
-
* Returns null if input is closed or format is not available.
|
|
679
|
-
*
|
|
680
|
-
* @returns Input format or null
|
|
681
|
-
*
|
|
682
|
-
* @example
|
|
683
|
-
* ```typescript
|
|
684
|
-
* const inputFormat = input.inputFormat;
|
|
685
|
-
* if (inputFormat) {
|
|
686
|
-
* console.log(`Input Format: ${inputFormat.name}`);
|
|
687
|
-
* }
|
|
688
|
-
* ```
|
|
689
|
-
*/
|
|
690
|
-
inputFormat() {
|
|
691
|
-
return this.formatContext.iformat;
|
|
692
|
-
}
|
|
693
|
-
/**
|
|
694
|
-
* Find the best stream of a given type.
|
|
695
|
-
*
|
|
696
|
-
* Uses FFmpeg's stream selection algorithm.
|
|
697
|
-
* Considers codec support, default flags, and quality.
|
|
698
|
-
*
|
|
699
|
-
* Direct mapping to av_find_best_stream().
|
|
700
|
-
*
|
|
701
|
-
* @param type - Media type to find
|
|
702
|
-
*
|
|
703
|
-
* @returns Best stream or undefined if not found or input is closed
|
|
704
|
-
*
|
|
705
|
-
* @example
|
|
706
|
-
* ```typescript
|
|
707
|
-
* import { AVMEDIA_TYPE_VIDEO } from 'node-av/constants';
|
|
708
|
-
*
|
|
709
|
-
* const bestVideo = input.findBestStream(AVMEDIA_TYPE_VIDEO);
|
|
710
|
-
* if (bestVideo) {
|
|
711
|
-
* const decoder = await Decoder.create(bestVideo);
|
|
712
|
-
* }
|
|
713
|
-
* ```
|
|
714
|
-
*
|
|
715
|
-
* @see {@link video} For direct video stream access
|
|
716
|
-
* @see {@link audio} For direct audio stream access
|
|
717
|
-
*/
|
|
718
|
-
findBestStream(type) {
|
|
719
|
-
if (this.isClosed) {
|
|
720
|
-
return undefined;
|
|
721
|
-
}
|
|
722
|
-
const bestStreamIndex = this.formatContext.findBestStream(type);
|
|
723
|
-
return this._streams.find((s) => s.index === bestStreamIndex);
|
|
724
|
-
}
|
|
725
|
-
/**
|
|
726
|
-
* Read packets from media as async generator.
|
|
727
|
-
*
|
|
728
|
-
* Yields demuxed packets for processing.
|
|
729
|
-
* Automatically handles packet memory management.
|
|
730
|
-
* Optionally filters packets by stream index.
|
|
731
|
-
*
|
|
732
|
-
* Direct mapping to av_read_frame().
|
|
733
|
-
*
|
|
734
|
-
* @param index - Optional stream index to filter
|
|
735
|
-
*
|
|
736
|
-
* @yields {Packet} Demuxed packets (must be freed by caller)
|
|
737
|
-
*
|
|
738
|
-
* @throws {Error} If packet cloning fails
|
|
739
|
-
*
|
|
740
|
-
* @example
|
|
741
|
-
* ```typescript
|
|
742
|
-
* // Read all packets
|
|
743
|
-
* for await (const packet of input.packets()) {
|
|
744
|
-
* console.log(`Packet: stream=${packet.streamIndex}, pts=${packet.pts}`);
|
|
745
|
-
* packet.free();
|
|
746
|
-
* }
|
|
747
|
-
* ```
|
|
748
|
-
*
|
|
749
|
-
* @example
|
|
750
|
-
* ```typescript
|
|
751
|
-
* // Read only video packets
|
|
752
|
-
* const videoStream = input.video();
|
|
753
|
-
* for await (const packet of input.packets(videoStream.index)) {
|
|
754
|
-
* // Process video packet
|
|
755
|
-
* packet.free();
|
|
756
|
-
* }
|
|
757
|
-
* ```
|
|
758
|
-
*
|
|
759
|
-
* @see {@link Decoder.frames} For decoding packets
|
|
760
|
-
*/
|
|
761
|
-
async *packets(index) {
|
|
762
|
-
const packet = new Packet();
|
|
763
|
-
packet.alloc();
|
|
764
|
-
try {
|
|
765
|
-
while (!this.isClosed) {
|
|
766
|
-
const ret = await this.formatContext.readFrame(packet);
|
|
767
|
-
if (ret < 0) {
|
|
768
|
-
// End of file or error
|
|
769
|
-
break;
|
|
770
|
-
}
|
|
771
|
-
if (index === undefined || packet.streamIndex === index) {
|
|
772
|
-
// Clone the packet for the user
|
|
773
|
-
// This creates a new Packet object that shares the same data buffer
|
|
774
|
-
// through reference counting. The data won't be freed until both
|
|
775
|
-
// the original and the clone are unreferenced.
|
|
776
|
-
const cloned = packet.clone();
|
|
777
|
-
if (!cloned) {
|
|
778
|
-
throw new Error('Failed to clone packet (out of memory)');
|
|
779
|
-
}
|
|
780
|
-
yield cloned;
|
|
781
|
-
}
|
|
782
|
-
// Unreference the original packet's data buffer
|
|
783
|
-
// This allows us to reuse the packet object for the next readFrame()
|
|
784
|
-
// The data itself is still alive because the clone has a reference
|
|
785
|
-
packet.unref();
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
finally {
|
|
789
|
-
packet.free();
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
/**
|
|
793
|
-
* Read packets from media as generator synchronously.
|
|
794
|
-
* Synchronous version of packets.
|
|
795
|
-
*
|
|
796
|
-
* Yields demuxed packets for processing.
|
|
797
|
-
* Automatically handles packet memory management.
|
|
798
|
-
* Optionally filters packets by stream index.
|
|
799
|
-
*
|
|
800
|
-
* Direct mapping to av_read_frame().
|
|
801
|
-
*
|
|
802
|
-
* @param index - Optional stream index to filter
|
|
803
|
-
*
|
|
804
|
-
* @yields {Packet} Demuxed packets (must be freed by caller)
|
|
805
|
-
*
|
|
806
|
-
* @throws {Error} If packet cloning fails
|
|
807
|
-
*
|
|
808
|
-
* @example
|
|
809
|
-
* ```typescript
|
|
810
|
-
* // Read all packets
|
|
811
|
-
* for (const packet of input.packetsSync()) {
|
|
812
|
-
* console.log(`Packet: stream=${packet.streamIndex}, pts=${packet.pts}`);
|
|
813
|
-
* packet.free();
|
|
814
|
-
* }
|
|
815
|
-
* ```
|
|
816
|
-
*
|
|
817
|
-
* @example
|
|
818
|
-
* ```typescript
|
|
819
|
-
* // Read only video packets
|
|
820
|
-
* const videoStream = input.video();
|
|
821
|
-
* for (const packet of input.packetsSync(videoStream.index)) {
|
|
822
|
-
* // Process video packet
|
|
823
|
-
* packet.free();
|
|
824
|
-
* }
|
|
825
|
-
* ```
|
|
826
|
-
*
|
|
827
|
-
* @see {@link packets} For async version
|
|
828
|
-
*/
|
|
829
|
-
*packetsSync(index) {
|
|
830
|
-
const packet = new Packet();
|
|
831
|
-
packet.alloc();
|
|
832
|
-
try {
|
|
833
|
-
while (!this.isClosed) {
|
|
834
|
-
const ret = this.formatContext.readFrameSync(packet);
|
|
835
|
-
if (ret < 0) {
|
|
836
|
-
// End of file or error
|
|
837
|
-
break;
|
|
838
|
-
}
|
|
839
|
-
if (index === undefined || packet.streamIndex === index) {
|
|
840
|
-
// Clone the packet for the user
|
|
841
|
-
// This creates a new Packet object that shares the same data buffer
|
|
842
|
-
// through reference counting. The data won't be freed until both
|
|
843
|
-
// the original and the clone are unreferenced.
|
|
844
|
-
const cloned = packet.clone();
|
|
845
|
-
if (!cloned) {
|
|
846
|
-
throw new Error('Failed to clone packet (out of memory)');
|
|
847
|
-
}
|
|
848
|
-
yield cloned;
|
|
849
|
-
}
|
|
850
|
-
// Unreference the original packet's data buffer
|
|
851
|
-
// This allows us to reuse the packet object for the next readFrame()
|
|
852
|
-
// The data itself is still alive because the clone has a reference
|
|
853
|
-
packet.unref();
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
finally {
|
|
857
|
-
packet.free();
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
/**
|
|
861
|
-
* Seek to timestamp in media.
|
|
862
|
-
*
|
|
863
|
-
* Seeks to the specified position in seconds.
|
|
864
|
-
* Can seek in specific stream or globally.
|
|
865
|
-
*
|
|
866
|
-
* Direct mapping to av_seek_frame().
|
|
867
|
-
*
|
|
868
|
-
* @param timestamp - Target position in seconds
|
|
869
|
-
*
|
|
870
|
-
* @param streamIndex - Stream index or -1 for global (default: -1)
|
|
871
|
-
*
|
|
872
|
-
* @param flags - Seek flags (default: AVFLAG_NONE)
|
|
873
|
-
*
|
|
874
|
-
* @returns 0 on success, negative on error
|
|
875
|
-
*
|
|
876
|
-
* @throws {Error} If input is closed
|
|
877
|
-
*
|
|
878
|
-
* @example
|
|
879
|
-
* ```typescript
|
|
880
|
-
* // Seek to 30 seconds
|
|
881
|
-
* const ret = await input.seek(30);
|
|
882
|
-
* FFmpegError.throwIfError(ret, 'seek failed');
|
|
883
|
-
* ```
|
|
884
|
-
*
|
|
885
|
-
* @example
|
|
886
|
-
* ```typescript
|
|
887
|
-
* import { AVSEEK_FLAG_BACKWARD } from 'node-av/constants';
|
|
888
|
-
*
|
|
889
|
-
* // Seek to keyframe before 60 seconds
|
|
890
|
-
* await input.seek(60, -1, AVSEEK_FLAG_BACKWARD);
|
|
891
|
-
* ```
|
|
892
|
-
*
|
|
893
|
-
* @see {@link AVSeekFlag} For seek flags
|
|
894
|
-
*/
|
|
895
|
-
async seek(timestamp, streamIndex = -1, flags = AVFLAG_NONE) {
|
|
896
|
-
if (this.isClosed) {
|
|
897
|
-
throw new Error('Cannot seek on closed input');
|
|
898
|
-
}
|
|
899
|
-
// Convert seconds to AV_TIME_BASE
|
|
900
|
-
const ts = BigInt(Math.floor(timestamp * 1000000));
|
|
901
|
-
return this.formatContext.seekFrame(streamIndex, ts, flags);
|
|
902
|
-
}
|
|
903
|
-
/**
|
|
904
|
-
* Seek to timestamp in media synchronously.
|
|
905
|
-
* Synchronous version of seek.
|
|
906
|
-
*
|
|
907
|
-
* Seeks to the specified position in seconds.
|
|
908
|
-
* Can seek in specific stream or globally.
|
|
909
|
-
*
|
|
910
|
-
* Direct mapping to av_seek_frame().
|
|
911
|
-
*
|
|
912
|
-
* @param timestamp - Target position in seconds
|
|
913
|
-
*
|
|
914
|
-
* @param streamIndex - Stream index or -1 for global (default: -1)
|
|
915
|
-
*
|
|
916
|
-
* @param flags - Seek flags (default: AVFLAG_NONE)
|
|
917
|
-
*
|
|
918
|
-
* @returns 0 on success, negative on error
|
|
919
|
-
*
|
|
920
|
-
* @throws {Error} If input is closed
|
|
921
|
-
*
|
|
922
|
-
* @example
|
|
923
|
-
* ```typescript
|
|
924
|
-
* // Seek to 30 seconds
|
|
925
|
-
* const ret = input.seekSync(30);
|
|
926
|
-
* FFmpegError.throwIfError(ret, 'seek failed');
|
|
927
|
-
* ```
|
|
928
|
-
*
|
|
929
|
-
* @example
|
|
930
|
-
* ```typescript
|
|
931
|
-
* import { AVSEEK_FLAG_BACKWARD } from 'node-av/constants';
|
|
932
|
-
*
|
|
933
|
-
* // Seek to keyframe before 60 seconds
|
|
934
|
-
* input.seekSync(60, -1, AVSEEK_FLAG_BACKWARD);
|
|
935
|
-
* ```
|
|
936
|
-
*
|
|
937
|
-
* @see {@link seek} For async version
|
|
938
|
-
*/
|
|
939
|
-
seekSync(timestamp, streamIndex = -1, flags = AVFLAG_NONE) {
|
|
940
|
-
if (this.isClosed) {
|
|
941
|
-
throw new Error('Cannot seek on closed input');
|
|
942
|
-
}
|
|
943
|
-
// Convert seconds to AV_TIME_BASE
|
|
944
|
-
const ts = BigInt(Math.floor(timestamp * 1000000));
|
|
945
|
-
return this.formatContext.seekFrameSync(streamIndex, ts, flags);
|
|
946
|
-
}
|
|
947
|
-
/**
|
|
948
|
-
* Close media input and free resources.
|
|
949
|
-
*
|
|
950
|
-
* Releases format context and I/O context.
|
|
951
|
-
* Safe to call multiple times.
|
|
952
|
-
* Automatically called by Symbol.asyncDispose.
|
|
953
|
-
*
|
|
954
|
-
* Direct mapping to avformat_close_input().
|
|
955
|
-
*
|
|
956
|
-
* @example
|
|
957
|
-
* ```typescript
|
|
958
|
-
* const input = await MediaInput.open('video.mp4');
|
|
959
|
-
* try {
|
|
960
|
-
* // Use input
|
|
961
|
-
* } finally {
|
|
962
|
-
* await input.close();
|
|
963
|
-
* }
|
|
964
|
-
* ```
|
|
965
|
-
*
|
|
966
|
-
* @see {@link Symbol.asyncDispose} For automatic cleanup
|
|
967
|
-
*/
|
|
968
|
-
async close() {
|
|
969
|
-
if (this.isClosed) {
|
|
970
|
-
return;
|
|
971
|
-
}
|
|
972
|
-
this.isClosed = true;
|
|
973
|
-
// IMPORTANT: Clear pb reference FIRST to prevent use-after-free
|
|
974
|
-
if (this.ioContext) {
|
|
975
|
-
this.formatContext.pb = null;
|
|
976
|
-
}
|
|
977
|
-
// Close FormatContext
|
|
978
|
-
await this.formatContext.closeInput();
|
|
979
|
-
// NOW we can safely free the IOContext
|
|
980
|
-
if (this.ioContext) {
|
|
981
|
-
this.ioContext.freeContext();
|
|
982
|
-
this.ioContext = undefined;
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
/**
|
|
986
|
-
* Close media input and free resources synchronously.
|
|
987
|
-
* Synchronous version of close.
|
|
988
|
-
*
|
|
989
|
-
* Releases format context and I/O context.
|
|
990
|
-
* Safe to call multiple times.
|
|
991
|
-
* Automatically called by Symbol.dispose.
|
|
992
|
-
*
|
|
993
|
-
* Direct mapping to avformat_close_input().
|
|
994
|
-
*
|
|
995
|
-
* @example
|
|
996
|
-
* ```typescript
|
|
997
|
-
* const input = MediaInput.openSync('video.mp4');
|
|
998
|
-
* try {
|
|
999
|
-
* // Use input
|
|
1000
|
-
* } finally {
|
|
1001
|
-
* input.closeSync();
|
|
1002
|
-
* }
|
|
1003
|
-
* ```
|
|
1004
|
-
*
|
|
1005
|
-
* @see {@link close} For async version
|
|
1006
|
-
*/
|
|
1007
|
-
closeSync() {
|
|
1008
|
-
if (this.isClosed) {
|
|
1009
|
-
return;
|
|
1010
|
-
}
|
|
1011
|
-
this.isClosed = true;
|
|
1012
|
-
// IMPORTANT: Clear pb reference FIRST to prevent use-after-free
|
|
1013
|
-
if (this.ioContext) {
|
|
1014
|
-
this.formatContext.pb = null;
|
|
1015
|
-
}
|
|
1016
|
-
// Close FormatContext
|
|
1017
|
-
this.formatContext.closeInputSync();
|
|
1018
|
-
// NOW we can safely free the IOContext
|
|
1019
|
-
if (this.ioContext) {
|
|
1020
|
-
this.ioContext.freeContext();
|
|
1021
|
-
this.ioContext = undefined;
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
/**
|
|
1025
|
-
* Get underlying format context.
|
|
1026
|
-
*
|
|
1027
|
-
* Returns the internal format context for advanced operations.
|
|
1028
|
-
*
|
|
1029
|
-
* @returns Format context
|
|
1030
|
-
*
|
|
1031
|
-
* @internal
|
|
1032
|
-
*/
|
|
1033
|
-
getFormatContext() {
|
|
1034
|
-
return this.formatContext;
|
|
1035
|
-
}
|
|
1036
|
-
/**
|
|
1037
|
-
* Dispose of media input.
|
|
1038
|
-
*
|
|
1039
|
-
* Implements AsyncDisposable interface for automatic cleanup.
|
|
1040
|
-
* Equivalent to calling close().
|
|
1041
|
-
*
|
|
1042
|
-
* @example
|
|
1043
|
-
* ```typescript
|
|
1044
|
-
* {
|
|
1045
|
-
* await using input = await MediaInput.open('video.mp4');
|
|
1046
|
-
* // Process media...
|
|
1047
|
-
* } // Automatically closed
|
|
1048
|
-
* ```
|
|
1049
|
-
*
|
|
1050
|
-
* @see {@link close} For manual cleanup
|
|
1051
|
-
*/
|
|
1052
|
-
async [Symbol.asyncDispose]() {
|
|
1053
|
-
await this.close();
|
|
1054
|
-
}
|
|
1055
|
-
/**
|
|
1056
|
-
* Dispose of media input synchronously.
|
|
1057
|
-
*
|
|
1058
|
-
* Implements Disposable interface for automatic cleanup.
|
|
1059
|
-
* Equivalent to calling closeSync().
|
|
1060
|
-
*
|
|
1061
|
-
* @example
|
|
1062
|
-
* ```typescript
|
|
1063
|
-
* {
|
|
1064
|
-
* using input = MediaInput.openSync('video.mp4');
|
|
1065
|
-
* // Process media...
|
|
1066
|
-
* } // Automatically closed
|
|
1067
|
-
* ```
|
|
1068
|
-
*
|
|
1069
|
-
* @see {@link closeSync} For manual cleanup
|
|
1070
|
-
*/
|
|
1071
|
-
[Symbol.dispose]() {
|
|
1072
|
-
this.closeSync();
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
//# sourceMappingURL=media-input.js.map
|