Merge main into release for tag v1.13.0

This commit is contained in:
github-actions[bot]
2025-09-02 16:00:43 +00:00
23 changed files with 937 additions and 195 deletions
+11 -2
View File
@@ -77,10 +77,19 @@ This callback is called each time the progress of the conversion advances.
A progress of `1` doesn't indicate the conversion has finished; the conversion is only finished once the promise returned by `.execute()` resolves.
:::
::: info
Tracking conversion progress may slightly affect performance, as it requires knowledge of the input file's total duration - but this is usually negligible.
::: warning
Tracking conversion progress can slightly affect performance as it requires knowledge of the input file's total duration. This is usually negligible but should be avoided when using append-only input sources such as [`ReadableStreamSource`](./reading-media-files#readablestreamsource).
:::
If you want to monitor the output size of the conversion (in bytes), simply use the `onwrite` callback on your `Target`:
```ts
let currentFileSize = 0;
output.target.onwrite = (start, end) => {
currentFileSize = Math.max(currentFileSize, end);
};
```
### Canceling a conversion
Sometimes, you may want to cancel an ongoing conversion process. For this, use the `cancel` method:
+79 -1
View File
@@ -551,4 +551,82 @@ type MaybePromise<T> = T | Promise<T>;
Specifies the prefetch profile that the reader should use with this source. A prefetch propfile specifies the pattern with which bytes outside of the requested range are preloaded to reduce latency for future reads.
- `'none'` (default): No prefetching; only the data needed in the moment is requested.
- `'fileSystem'`: File system-optimized prefetching: a small amount of data is prefetched bidirectionally, aligned with page boundaries.
- `'network'`: Network-optimized prefetching, or more generally, prefetching optimized for any high-latency environment: tries to minimize the amount of read calls and aggressively prefetches data when sequential access patterns are detected.
- `'network'`: Network-optimized prefetching, or more generally, prefetching optimized for any high-latency environment: tries to minimize the amount of read calls and aggressively prefetches data when sequential access patterns are detected.
### `ReadableStreamSource`
This is a source backed by a `ReadableStream` of `Uint8Array`, representing an append-only byte stream of unknown length. This is the source to use for incrementally streaming in input files that are still being constructed and whose size we don't yet know. You could also use it to stream in existing files, but other sources (such as [`BlobSource`](#blobsource) or [`FilePathSource`](#filepathsource)) are recommended instead because they offer random access.
```ts
import { ReadableStreamSource } from 'mediabunny';
const { writable, readable } = new TransformStream<Uint8Array, Uint8Array>();
const source = new ReadableStreamSource(readable);
// Append chunks of data
const writer = writable.getWriter();
writer.write(chunk1);
writer.write(chunk2);
writer.close();
```
This source is *unsized*, meaning calls to `.getSize()` will throw and readers are more limited due to the lack of random file access. You should only use this source with sequential access patterns, such as reading all packets from start to end or doing conversions. This source does not work well with random access patterns unless you increase its max cache size.
```ts
type ReadableStreamSourceOptions = {
// The maximum number of bytes the cache is allowed to hold
// in memory. Defaults to 16 MiB.
maxCacheSize?: number;
};
```
#### Use with [`MediaRecorder`](https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder)
You can combine `MediaRecorder` with `ReadableStreamSource` to stream recorded data into Mediabunny while the recording is taking place. Here's an example where we pipe `MediaRecorder`'s output into Mediabunny's Conversion API to create a WAVE file:
```ts
import {
Input,
Output,
Conversion,
ReadableStreamSource,
ALL_FORMATS,
WavOutputFormat,
BufferTarget,
} from 'mediabunny';
// Set up a TransformStream to convert MediaRecorder's Blobs into Uint8Arrays
const { writable, readable } = new TransformStream<Blob, Uint8Array>({
async transform(chunk, controller) {
const arrayBuffer = await chunk.arrayBuffer();
controller.enqueue(new Uint8Array(arrayBuffer));
},
});
const input = new Input({
source: new ReadableStreamSource(readable),
formats: ALL_FORMATS,
});
const output = new Output({
format: new WavOutputFormat(),
target: new BufferTarget(),
});
const conversionPromise = Conversion.init({ input, output })
.then(conversion => conversion.execute());
const micStream = await navigator.mediaDevices.getUserMedia({ audio: true });
const recorder = new MediaRecorder(micStream);
const writer = writable.getWriter();
recorder.ondataavailable = e => writer.write(e.data);
recorder.onstop = async () => {
await writer.close();
await conversionPromise;
// Get the final .wav file
const wavFile = output.target.buffer!; // => ArrayBuffer
};
recorder.start(1000);
setTimeout(() => recorder.stop(), 10_000); // Stop recording after 10s
```
+51 -1
View File
@@ -197,7 +197,18 @@ output.state; // => 'pending' | 'started' | 'canceled' | 'finalizing' | 'finaliz
## Output targets
The _output target_ determines where the data created by the `Output` will be written. This library offers two targets:
The _output target_ determines where the data created by the `Output` will be written. This library offers a couple of targets.
---
All targets have an optional `onwrite` callback you can set to monitor which byte regions are being written to:
```ts
target.onwrite = (start, end) => {
// ...
};
```
You can use this to track the size of the output file as it grows. But be warned, this function is chatty and gets called *extremely* frequently.
### `BufferTarget`
@@ -297,6 +308,45 @@ const output = new Output({
await output.finalize(); // Will automatically close the writable stream
```
### `NullTarget`
This target simply discards all data that is passed into it. It is useful for when you need an `Output` but extract data from it differently, for example through output format-specific callbacks or encoder events.
As an example, here we create a fragmented MP4 file and directly handle the individual fragments:
```ts
import { Output, NullTarget, Mp4OutputFormat } from 'mediabunny';
let ftyp: Uint8Array;
let lastMoof: Uint8Array;
const output = new Output({
target: new NullTarget(),
format: new Mp4OutputFormat({
fastStart: 'fragmented',
onFtyp: (data) => {
ftyp = data;
},
onMoov: (data) => {
const header = new Uint8Array(ftyp.length + data.length);
header.set(ftyp, 0);
header.set(data, ftyp.length);
// Do something with the header...
},
onMoof: (data) => {
lastMoof = data;
},
onMdat: (data) => {
const segment = new Uint8Array(lastMoof.length + data.length);
segment.set(lastMoof, 0);
segment.set(data, lastMoof.length);
// Do something with the segment...
},
}),
});
```
## Packet buffering
Some [output formats](./output-formats) require *packet buffering* for multi-track outputs. Packet buffering occurs because the `Output` must wait for data from all tracks for a given timestamp to continue writing data. For example, should you first encode all your video frames and then encode the audio afterward, the `Output` will have to hold all of the video frames in memory until the audio packets start coming in. This might lead to memory exhaustion should your video be very long. When there is only one media track, this issue does not arise.