Skip to content

Bug: TypeErrors with IOOutputCallbacks. Asynchronous methods are not accepted. #125

@virtuallyunknown

Description

@virtuallyunknown

Describe the Bug

Greetings!

See this exact example from the documentation. It demonstrates how to open muxer for writing. It also shows an example with custom callbacks.

// Custom I/O callbacks
const callbacks = {
  write: async (buffer: Buffer) => {
    // Write to custom destination
    return buffer.length;
  },
  seek: async (offset: bigint, whence: AVSeekWhence) => {
    // Seek in custom destination
    return offset;
  }
};

await using output = await Muxer.open(callbacks, {
  format: 'mp4',
  bufferSize: 8192
});

However, if you copy-paste the example, you will get type errors. That's because the IOOutputCallbacks interface does not allow a Promise as a return value from it's member methods - write, seek or read.

node-av/src/api/types.ts

Lines 757 to 786 in ffff4a7

export interface IOOutputCallbacks {
/**
* Write callback - called when FFmpeg needs to write data.
*
* @param buffer - Buffer containing data to write
*
* @returns Number of bytes written or void
*/
write: (buffer: Buffer) => number | void;
/**
* Seek callback - called when FFmpeg needs to seek in the output.
*
* @param offset - Offset to seek to
*
* @param whence - Seek origin (AVSEEK_SET, AVSEEK_CUR, AVSEEK_END)
*
* @returns New position or negative error code
*/
seek?: (offset: bigint, whence: AVSeekWhence) => bigint | number;
/**
* Read callback - some formats may need to read back data.
*
* @param size - Number of bytes to read
*
* @returns Buffer with data, null for EOF, or negative error code
*/
read?: (size: number) => Buffer | null | number;
}

To Reproduce

See example above.

Expected Behavior

I think async methods should be allowed and not throw any type errors? Unless there is some runtime limitations beyond my knowledge.

Code Sample

To give a real life example, I am currently trying to use Hono to stream a video file to the frontend. The problem is that streamWriter.write is an asynchronous method, but I can't use it inside of the callbacks because they have to be synchronous. So a bit of a catch 22 here.

import { Hono } from 'hono';
import { stream } from 'hono/streaming';
import { Demuxer, Muxer } from 'node-av';

const app = new Hono();
app.get('/stream', (c) => {
    try {
        return stream(c, async (streamWriter) => {
            c.header('Content-Type', 'video/mp4');
            c.header('Cache-Control', 'no-cache');

            await using input = await Demuxer.open('some-file.mp4');

            const videoStream = input.video();
            const audioStream = input.audio();

            if (!videoStream || !audioStream) {
                await streamWriter.write('No video stream found');
                return;
            }

            await using output = await Muxer.open(
                {
                    // ❌ Can't use async here
                    write: async (buffer: Buffer) => {
                        await streamWriter.write(buffer);
                        return buffer.length;
                    }
                },
                {
                    format: 'mp4',
                    options: {
                        movflags: 'frag_keyframe+empty_moov+default_base_moof',
                        frag_duration: '5000000', // 5 second fragments
                        min_frag_duration: '2000000', // Minimum 2 seconds
                    }
                }
            );

            // Add video and audio streams (copy codec, no transcoding)
            const videoOutputIndex = output.addStream(videoStream);
            const audioOutputIndex = output.addStream(audioStream);

            try {
                // Stream packets directly without decoding/encoding
                for await (const packet of input.packets()) {
                    if (!packet) continue;

                    if (packet.streamIndex === videoStream.index) {
                        await output.writePacket(packet, videoOutputIndex);
                    }
                    else if (packet.streamIndex === audioStream.index) {
                        await output.writePacket(packet, audioOutputIndex);
                    }

                    packet.free();
                }
            }
            catch (error) {
                console.error('Stream error:', error);
            }
        });
    }
    catch (error) {
        console.error('Failed to open stream:', error);
        return c.text('Failed to stream', 500);
    }
});

Relevant Log Output / Error Messages

No overload matches this call.
  Overload 1 of 2, '(target: string, options?: MuxerOptions | undefined): Promise<Muxer>', gave the following error.
    Argument of type '{ write: (buffer: Buffer) => Promise<number>; seek: (offset: bigint, whence: AVSeekWhence) => Promise<bigint>; }' is not assignable to parameter of type 'string'.
  Overload 2 of 2, '(target: IOOutputCallbacks, options: MuxerOptions & { format: string; }): Promise<Muxer>', gave the following error.
    Argument of type '{ write: (buffer: Buffer) => Promise<number>; seek: (offset: bigint, whence: AVSeekWhence) => Promise<bigint>; }' is not assignable to parameter of type 'IOOutputCallbacks'.
      The types returned by 'write(...)' are incompatible between these types.
        Type 'Promise<number>' is not assignable to type 'number | void'.ts(2769)

Node.js Version

25.2.1

NodeAV Version

5.0.3

Operating System

Linux

Installation Method

pnpm add node-av

API Used

Low-Level API

Hardware Acceleration

None

Additional Context

No response

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions