Skip to content

Commit e3f77b2

Browse files
committed
Add example for AVIO async read callback using Promises
1 parent df64d5a commit e3f77b2

File tree

1 file changed

+281
-0
lines changed

1 file changed

+281
-0
lines changed
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
/**
2+
* AVIO Async Read Callback Example - Low Level API
3+
*
4+
* Demonstrates using async (Promise-returning) callbacks with IOContext
5+
* for custom I/O operations. This is useful when reading from async sources
6+
* like network streams, databases, or cloud storage.
7+
*
8+
* This example simulates async I/O by reading from a buffer with artificial delays,
9+
* similar to how you would read from a network or async file API.
10+
*
11+
* Usage: tsx examples/avio-async-read-callback.ts <input>
12+
* Example: tsx examples/avio-async-read-callback.ts testdata/video.mp4
13+
*/
14+
15+
import { existsSync, readFileSync } from 'node:fs';
16+
17+
import { AVERROR_EOF, AVSEEK_CUR, AVSEEK_END, AVSEEK_SET, AVSEEK_SIZE, FFmpegError, FormatContext, IOContext } from '../src/index.js';
18+
19+
import type { AVSeekWhence } from '../src/index.js';
20+
21+
/**
22+
* Async buffer data structure for reading
23+
* Simulates an async data source like a network stream
24+
*/
25+
class AsyncBufferData {
26+
private buffer: Buffer;
27+
private position = 0;
28+
private readCount = 0;
29+
private seekCount = 0;
30+
31+
constructor(buffer: Buffer) {
32+
this.buffer = buffer;
33+
}
34+
35+
/**
36+
* Async read data from buffer
37+
* Simulates network latency or async file I/O
38+
*/
39+
async read(size: number): Promise<Buffer | number> {
40+
this.readCount++;
41+
42+
// Simulate async operation (e.g., network fetch, cloud storage read)
43+
await this.simulateAsyncDelay();
44+
45+
if (this.position >= this.buffer.length) {
46+
console.log(`[Async Read #${this.readCount}] EOF reached`);
47+
return AVERROR_EOF; // Signal EOF
48+
}
49+
50+
const bytesToRead = Math.min(size, this.buffer.length - this.position);
51+
const data = this.buffer.subarray(this.position, this.position + bytesToRead);
52+
this.position += bytesToRead;
53+
54+
console.log(`[Async Read #${this.readCount}] Read ${bytesToRead} bytes from position ${this.position - bytesToRead}`);
55+
return data;
56+
}
57+
58+
/**
59+
* Async seek in buffer
60+
* Simulates async seek operation
61+
*/
62+
async seek(offset: bigint, whence: AVSeekWhence): Promise<bigint> {
63+
this.seekCount++;
64+
65+
// Simulate async operation
66+
await this.simulateAsyncDelay();
67+
68+
// Handle AVSEEK_SIZE
69+
if (whence === AVSEEK_SIZE) {
70+
console.log(`[Async Seek #${this.seekCount}] AVSEEK_SIZE -> ${this.buffer.length}`);
71+
return BigInt(this.buffer.length);
72+
}
73+
74+
let newPos: number;
75+
const offsetNum = Number(offset);
76+
77+
switch (whence) {
78+
case AVSEEK_SET:
79+
newPos = offsetNum;
80+
break;
81+
case AVSEEK_CUR:
82+
newPos = this.position + offsetNum;
83+
break;
84+
case AVSEEK_END:
85+
newPos = this.buffer.length + offsetNum;
86+
break;
87+
default:
88+
return BigInt(-1);
89+
}
90+
91+
if (newPos < 0 || newPos > this.buffer.length) {
92+
return BigInt(-1);
93+
}
94+
95+
this.position = newPos;
96+
console.log(`[Async Seek #${this.seekCount}] Seek to position ${this.position}`);
97+
return BigInt(this.position);
98+
}
99+
100+
/**
101+
* Simulate async delay (e.g., network latency)
102+
*/
103+
private async simulateAsyncDelay(): Promise<void> {
104+
// Small random delay to simulate real-world async behavior
105+
const delay = Math.random() * 5; // 0-5ms
106+
await new Promise((resolve) => setTimeout(resolve, delay));
107+
}
108+
109+
get stats() {
110+
return {
111+
size: this.buffer.length,
112+
position: this.position,
113+
remaining: this.buffer.length - this.position,
114+
readCount: this.readCount,
115+
seekCount: this.seekCount,
116+
};
117+
}
118+
}
119+
120+
/**
121+
* Main function demonstrating async custom I/O with callbacks
122+
*/
123+
async function asyncCustomIORead(inputFile: string): Promise<void> {
124+
let fmtCtx: FormatContext | null = null;
125+
let ioCtx: IOContext | null = null;
126+
127+
try {
128+
// Read entire file into memory (in real use, this could be a remote source)
129+
console.log(`Reading file: ${inputFile}`);
130+
const fileBuffer = readFileSync(inputFile);
131+
console.log(`File size: ${fileBuffer.length} bytes`);
132+
console.log('');
133+
134+
// Create async buffer data structure
135+
const asyncData = new AsyncBufferData(fileBuffer);
136+
137+
console.log('Setting up async I/O callbacks...');
138+
console.log('(Each read/seek operation will have simulated async delay)');
139+
console.log('');
140+
141+
// Create custom I/O context with ASYNC callbacks
142+
ioCtx = new IOContext();
143+
ioCtx.allocContextWithCallbacks(
144+
4096, // Buffer size
145+
0, // Read mode
146+
// ASYNC Read callback - returns Promise
147+
async (size: number): Promise<Buffer | number> => {
148+
return asyncData.read(size);
149+
},
150+
undefined, // No write callback
151+
// ASYNC Seek callback - returns Promise
152+
async (offset: bigint, whence: AVSeekWhence): Promise<bigint> => {
153+
return asyncData.seek(offset, whence);
154+
},
155+
);
156+
157+
// Create format context
158+
fmtCtx = new FormatContext();
159+
fmtCtx.allocContext();
160+
161+
// Assign custom I/O context
162+
fmtCtx.pb = ioCtx;
163+
164+
// Open input (with null filename since we're using custom I/O)
165+
console.log('Opening input with async callbacks...');
166+
const openRet = await fmtCtx.openInput('dummy', null, null);
167+
FFmpegError.throwIfError(openRet, 'openInput');
168+
169+
// Find stream information
170+
console.log('Finding stream info...');
171+
const findRet = await fmtCtx.findStreamInfo(null);
172+
FFmpegError.throwIfError(findRet, 'findStreamInfo');
173+
174+
// Dump format information
175+
console.log('\n' + '='.repeat(60));
176+
console.log('Format information:');
177+
console.log('='.repeat(60));
178+
fmtCtx.dumpFormat(0, inputFile, false);
179+
180+
// Show that we successfully read through our async callbacks
181+
console.log('\n' + '='.repeat(60));
182+
console.log('Successfully read format through ASYNC I/O callbacks!');
183+
console.log('='.repeat(60));
184+
console.log(`Number of streams: ${fmtCtx.nbStreams}`);
185+
186+
const streams = fmtCtx.streams;
187+
if (streams) {
188+
for (let i = 0; i < streams.length; i++) {
189+
const stream = streams[i];
190+
if (stream?.codecpar) {
191+
const codecType = stream.codecpar.codecType;
192+
const typeName = codecType === 0 ? 'Video' : codecType === 1 ? 'Audio' : codecType === 3 ? 'Subtitle' : 'Other';
193+
console.log(`Stream #${i}: ${typeName}`);
194+
195+
if (codecType === 0) {
196+
// Video
197+
console.log(` Resolution: ${stream.codecpar.width}x${stream.codecpar.height}`);
198+
} else if (codecType === 1) {
199+
// Audio
200+
console.log(` Sample rate: ${stream.codecpar.sampleRate} Hz`);
201+
console.log(` Channels: ${stream.codecpar.channels}`);
202+
}
203+
}
204+
}
205+
}
206+
207+
// Show metadata if available
208+
const metadata = fmtCtx.metadata;
209+
if (metadata) {
210+
console.log('\nFile metadata:');
211+
const entries = metadata.getAll();
212+
for (const key of Object.keys(entries)) {
213+
console.log(` ${key}: ${entries[key]}`);
214+
}
215+
metadata.free();
216+
}
217+
218+
// Show async I/O statistics
219+
const stats = asyncData.stats;
220+
console.log('\n' + '='.repeat(60));
221+
console.log('Async I/O Statistics:');
222+
console.log('='.repeat(60));
223+
console.log(`Total async read operations: ${stats.readCount}`);
224+
console.log(`Total async seek operations: ${stats.seekCount}`);
225+
console.log(`Final buffer position: ${stats.position} / ${stats.size} bytes`);
226+
} catch (error) {
227+
console.error('Error occurred:', error);
228+
throw error;
229+
} finally {
230+
// Cleanup
231+
if (ioCtx) {
232+
ioCtx.freeContext();
233+
}
234+
if (fmtCtx) {
235+
await fmtCtx.closeInput();
236+
fmtCtx.freeContext();
237+
}
238+
}
239+
}
240+
241+
/**
242+
* Main entry point
243+
*/
244+
async function main(): Promise<void> {
245+
const args = process.argv.slice(2);
246+
247+
if (args.length < 1) {
248+
console.log('Usage: tsx examples/avio-async-read-callback.ts <input>');
249+
console.log('');
250+
console.log('API example program to show how to read from a custom buffer');
251+
console.log('using ASYNC AVIOContext callbacks that return Promises.');
252+
console.log('');
253+
console.log('This demonstrates that async callbacks are supported, allowing');
254+
console.log('integration with async data sources like network streams,');
255+
console.log('databases, or cloud storage.');
256+
process.exit(1);
257+
}
258+
259+
const [inputFile] = args;
260+
261+
// Check if file exists
262+
if (!existsSync(inputFile)) {
263+
console.error(`Error: File not found: ${inputFile}`);
264+
process.exit(1);
265+
}
266+
267+
try {
268+
await asyncCustomIORead(inputFile);
269+
process.exit(0);
270+
} catch (error) {
271+
if (error instanceof FFmpegError) {
272+
console.error(`FFmpeg Error: ${error.message} (code: ${error.code})`);
273+
} else {
274+
console.error('Unexpected error:', error);
275+
}
276+
process.exit(1);
277+
}
278+
}
279+
280+
// Run if executed directly
281+
main().catch(console.error);

0 commit comments

Comments
 (0)