Skip to content

FFmpegKit Stream Protocol

Taner Sener edited this page Jul 28, 2026 · 2 revisions

ffkitstream: lets FFmpegKit exchange bytes with a command while it is running. Use it when the complete input or output is not available up front. Stream inputs can be read by FFmpeg or FFprobe; stream outputs are produced by FFmpeg.

Streams are not seekable. They work best with formats that can be read or written sequentially.

Android

Streaming Input

Use streaming input when your app produces data that FFmpeg or FFprobe should consume.

val stream = FFmpegKitStreamInput.create("mp3")

FFmpegKit.executeAsync(
    "-i ${stream.getUrl()} -c:a aac ${context.cacheDir}/out.m4a"
) {
    stream.close()
}

try {
    while (hasMoreAudio()) {
        val chunk = nextAudioChunk()
        stream.write(chunk)
    }

    stream.closeInput()
} catch (t: Throwable) {
    stream.close()
    throw t
}

closeInput() tells the command that no more input bytes are coming. close() releases the native stream.

Writes can block when the stream buffer is full. To use a timeout:

val accepted = stream.write(chunk, timeoutMs = 1000)

The returned value is the number of bytes accepted. It can be smaller than the chunk size when a timeout happens.

Streaming Output

Use streaming output when FFmpeg produces data that your app wants to consume while the command is running.

val stream = FFmpegKitStreamOutput.create("mp3")

FFmpegKit.executeAsync(
    "-i ${context.cacheDir}/input.wav -f mp3 ${stream.getUrl()}"
) {
    stream.close()
}

try {
    while (true) {
        val chunk = stream.read(maxBytes = 32 * 1024, timeoutMs = 1000)

        if (chunk == null) {
            // No data before timeout. Keep waiting or check session state.
            continue
        }

        if (chunk.isEmpty()) {
            // FFmpeg closed the output stream.
            break
        }

        consumeOutput(chunk)
    }
} finally {
    stream.close()
}

Native Android reads return null on timeout and an empty ByteArray at end of output.

Apple

Apple uses Objective-C wrappers:

FFmpegKitStreamInput *stream = [FFmpegKitStreamInput create:@"mp3"];

[FFmpegKit executeAsync:
    [NSString stringWithFormat:@"-i %@ -c:a aac out.m4a", [stream getUrl]]
    withCompleteCallback:^(FFmpegSession *session) {
        [stream close];
    }];

[stream write:data timeout:1000];
[stream closeInput];

For streaming output:

FFmpegKitStreamOutput *stream = [FFmpegKitStreamOutput create:@"mp3"];

[FFmpegKit executeAsync:
    [NSString stringWithFormat:@"-i input.wav -f mp3 %@", [stream getUrl]]
    withCompleteCallback:^(FFmpegSession *session) {
        [stream close];
    }];

NSData *chunk = [stream read:32768 timeout:1000];

Apple reads return nil on timeout and empty NSData at end of output.

Linux

Linux uses C++ wrappers:

auto stream = ffmpegkit::FFmpegKitStreamInput::create("mp3");

ffmpegkit::FFmpegKit::executeAsync(
    "-i " + stream->getUrl() + " -c:a aac out.m4a",
    [stream](auto session) {
        stream->close();
    });

stream->write(data, 1000);
stream->closeInput();

For streaming output:

auto stream = ffmpegkit::FFmpegKitStreamOutput::create("mp3");

ffmpegkit::FFmpegKit::executeAsync(
    "-i input.wav -f mp3 " + stream->getUrl(),
    [stream](auto session) {
        stream->close();
    });

auto chunk = stream->read(32768, 1000);

Linux reads return nullptr on timeout and an empty vector at end of output.

Flutter

Flutter uses asynchronous stream wrappers:

final stream = await FFmpegKitStreamInput.create(extension: 'mp3');

await FFmpegKit.executeAsync(
  '-i ${stream.getUrl()} -c:a aac out.m4a',
  (session) async {
    await stream.close();
  },
);

await stream.write(chunk, timeoutMs: 1000);
await stream.closeInput();

For streaming output:

final stream = await FFmpegKitStreamOutput.create(extension: 'mp3');

await FFmpegKit.executeAsync(
  '-i input.wav -f mp3 ${stream.getUrl()}',
  (session) async {
    await stream.close();
  },
);

final chunk = await stream.read(32 * 1024, timeoutMs: 1000);

Flutter returns an empty Uint8List when no bytes are returned by the native layer, including end-of-output.

React Native

React Native exchanges stream data as base64 strings.

const stream = await FFmpegKitStreamInput.create('mp3');

await FFmpegKit.executeAsync(
  `-i ${stream.getUrl()} -c:a aac out.m4a`,
  async () => {
    await stream.close();
  }
);

await stream.write(base64Chunk, 1000);
await stream.closeInput();

For streaming output:

const stream = await FFmpegKitStreamOutput.create('mp3');

await FFmpegKit.executeAsync(
  `-i input.wav -f mp3 ${stream.getUrl()}`,
  async () => {
    await stream.close();
  }
);

const chunkBase64 = await stream.read(32 * 1024, 1000);

React Native returns an empty string when no base64 data is returned by the native layer, including end-of-output.

Web

Web stream input writes Uint8Array chunks. write is non-blocking and returns the number of bytes accepted, which can be smaller than the offered chunk.

const stream = await FFmpegKitStreamInput.create('mp3');

await FFmpegKit.executeAsync(`-i ${stream.getUrl()} -c:a aac out.m4a`);

let offset = 0;
while (offset < bytes.length) {
  offset += await stream.write(bytes.subarray(offset));
}
await stream.closeInput();
await stream.close();

For streaming output:

const stream = await FFmpegKitStreamOutput.create('mp3');

await FFmpegKit.executeAsync(`-i input.wav -f mp3 ${stream.getUrl()}`);

while (true) {
  const chunk = await stream.read(32 * 1024);
  if (chunk === null) {
    continue;
  }
  if (chunk.length === 0) {
    break;
  }

  consumeOutput(chunk);
}
await stream.close();

Web reads return null when nothing is ready yet and an empty Uint8Array at end-of-output.

Notes

  • Default stream capacity is 1 MiB unless you pass a custom capacity.
  • Use timeoutMs = -1 or omit the timeout to wait indefinitely.
  • Use timeoutMs = 0 for an immediate timeout check.
  • Always call close() when done, even if the command fails.

Clone this wiki locally