diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..6d5ad9a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,25 @@ +name: Test + +on: + push: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm run test diff --git a/.gitignore b/.gitignore index 177b87f..355e7e1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ -.vscode node_modules /dist /dist-docs .DS_Store /docs/.vitepress/cache +/docs/api packages/mp3-encoder/dist \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7a1398e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "editor.defaultFormatter": "dbaeumer.vscode-eslint", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + } +} diff --git a/README.md b/README.md index 8c846a3..53a5c3b 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,10 @@ Mediabunny is a JavaScript library for reading, writing, and converting media fi ### Gold sponsors
+ + Remotion + +      Gling AI @@ -180,6 +184,7 @@ npm run build # Production build with type definitions npm run check # Type checking npm run lint # ESLint +npm run docs:generate # Generates API docs npm run docs:dev # Start docs development server npm run dev # Start examples development server diff --git a/dev/convert.html b/dev/convert.html index 7ab5593..3f2ae08 100644 --- a/dev/convert.html +++ b/dev/convert.html @@ -18,13 +18,15 @@ const file = fileInput.files[0]; const source = new Mediabunny.BlobSource(file); - const target = new Mediabunny.BufferTarget() ?? new Mediabunny.StreamTarget(new WritableStream({ + const target = new Mediabunny.NullTarget() ?? new Mediabunny.BufferTarget() ?? new Mediabunny.StreamTarget(new WritableStream({ write: console.log }), { chunked: true, chunkSize: 2**20 }); - const outputFormat = new Mediabunny.Mp3OutputFormat({}); + const outputFormat = new Mediabunny.Mp4OutputFormat({ + onMoov: console.log + }); const button = document.createElement('button'); button.textContent = 'Cancel'; diff --git a/dev/demux.html b/dev/demux.html index 3dc518f..31a4a52 100644 --- a/dev/demux.html +++ b/dev/demux.html @@ -9,17 +9,166 @@ fileInput.addEventListener('change', async () => { const file = fileInput.files[0]; - const source = new Mediabunny.BlobSource(file); + const input = new Mediabunny.Input({ + formats: Mediabunny.ALL_FORMATS, + source: new Mediabunny.BlobSource(file), + }); + + console.log(await input.getMetadata()); + + + /* + const screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true }); + const micStream = await navigator.mediaDevices.getUserMedia({ audio: true }); + + const combinedStream = new MediaStream([ + ...screenStream.getTracks(), + ...micStream.getTracks() + ]); + + const recorder = new MediaRecorder(combinedStream); + + const { writable, readable } = new TransformStream({ + async transform(chunk, controller) { + const arrayBuffer = await chunk.arrayBuffer(); + controller.enqueue(new Uint8Array(arrayBuffer)); + } + }); + const writer = writable.getWriter(); + + const input = new Mediabunny.Input({ + source: new Mediabunny.ReadableStreamSource(readable), + formats: Mediabunny.ALL_FORMATS, + }); + (async () => { + const videoTrack = await input.getPrimaryVideoTrack(); + const sink = new Mediabunny.EncodedPacketSink(videoTrack); + + for await (const packet of sink.packets()) { + console.log(packet); + } + })(); + + recorder.onerror = console.error; + recorder.onstart = () => console.log("yes") + recorder.ondataavailable = async e => { + writer.write(e.data); + }; + recorder.onstop = (e) => { + writer.close(); + }; + + recorder.start(1000); + + + window.addEventListener('click', () => { + recorder.stop() + }, { once: true }) + */ + /* + + const transformStream = new TransformStream(); + + const file = fileInput.files[0]; + const source = new Mediabunny.ReadableStreamSource(transformStream.readable); + + file.stream().pipeThrough(transformStream); + + const input = new Mediabunny.Input({ + formats: Mediabunny.ALL_FORMATS, + source//: new Mediabunny.BlobSource(file), + }); + + + const audioTrack = await input.getPrimaryAudioTrack(); + console.log(audioTrack); + + const sink = new Mediabunny.EncodedPacketSink(audioTrack); + + + //console.log(await sink.getPacket(100)) + + console.log(await input.computeDuration()) + */ + + /* + const screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true }); + const micStream = await navigator.mediaDevices.getUserMedia({ audio: true }); + + const combinedStream = new MediaStream([ + ...screenStream.getTracks(), + ...micStream.getTracks() + ]); + + const recorder = new MediaRecorder(combinedStream); + + const { writable, readable } = new TransformStream({ + async transform(chunk, controller) { + const arrayBuffer = await chunk.arrayBuffer(); + controller.enqueue(new Uint8Array(arrayBuffer)); + } + }); + const writer = writable.getWriter(); + + const input = new Mediabunny.Input({ + source: new Mediabunny.ReadableStreamSource(readable), + formats: Mediabunny.ALL_FORMATS, + }); + (async () => { + const videoTrack = await input.getPrimaryVideoTrack(); + const sink = new Mediabunny.EncodedPacketSink(videoTrack); + + for await (const packet of sink.packets()) { + console.log(packet); + } + })(); + + recorder.onerror = console.error; + recorder.onstart = () => console.log("yes") + recorder.ondataavailable = async e => { + writer.write(e.data); + }; + recorder.onstop = (e) => { + writer.close(); + }; + + recorder.start(1000); + + window.addEventListener('click', () => { + recorder.stop() + }, { once: true }) + + //setTimeout(() => recorder.stop(), 5000) + */ + + /* + const transformStream = new TransformStream(); + + const file = fileInput.files[0]; + const source = new Mediabunny.ReadableStreamSource(transformStream.readable); + + file.stream().pipeTo(transformStream.writable); const input = new Mediabunny.Input({ formats: Mediabunny.ALL_FORMATS, source }); - //const blob = new Blob([(await input.getMetadata()).images[0].data], { type: (await input.getMetadata()).images[0].mimeType }); - //console.log(URL.createObjectURL(blob)) + const audioTrack = await input.getPrimaryAudioTrack(); + console.log(audioTrack); - console.log(await input.getMetadata()); + //return; + + const sink = new Mediabunny.EncodedPacketSink(audioTrack); + + for await (const packet of sink.packets()) { + console.log(packet); + } + + console.log("done"); + */ + + //console.log(await sink.getPacket(1)) /* const videoTrack = await input.getPrimaryVideoTrack(); @@ -29,6 +178,15 @@ console.log(videoTrack.internalCodecId); */ + /* + + const sink = new Mediabunny.AudioSampleSink(await input.getPrimaryAudioTrack()); + for await (const sample of sink.samples()) { + console.log(sample) + break; + } + */ + /* const sink = new Mediabunny.EncodedPacketSink(videoTrack); diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index bcb0330..6a1502a 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -3,6 +3,8 @@ import footnote from 'markdown-it-footnote'; import tailwindcss from '@tailwindcss/vite'; import llmstxt from 'vitepress-plugin-llms'; import { HeadConfig } from 'vitepress'; +// @ts-expect-error This file gets generated once docs:generate is run +import apiRoutes from '../api/index.json'; const DESCRIPTION = 'A JavaScript library for reading, writing, and converting media files. Directly in the browser,' + ' and faster than anybunny else.'; @@ -32,56 +34,62 @@ export default withMermaid({ // https://vitepress.dev/reference/default-theme-config nav: [ { text: 'Guide', link: '/guide/introduction', activeMatch: '/guide' }, + { text: 'API', link: '/api', activeMatch: '/api' }, + { text: 'LLMs', link: '/llms', activeMatch: '/llms' }, { text: 'Examples', link: '/examples', activeMatch: '/examples' }, { text: 'Sponsors', link: '/#sponsors', activeMatch: '/#sponsors' }, { text: 'License', link: 'https://github.com/Vanilagy/mediabunny#license' }, ], - sidebar: [ - { - text: 'Getting started', - items: [ - { text: 'Introduction', link: '/guide/introduction' }, - { text: 'Installation', link: '/guide/installation' }, - { text: 'Quick start', link: '/guide/quick-start' }, - ], - }, - { - text: 'Reading', - items: [ - { text: 'Reading media files', link: '/guide/reading-media-files' }, - { text: 'Media sinks', link: '/guide/media-sinks' }, - { text: 'Input formats', link: '/guide/input-formats' }, - ], - }, - { - text: 'Writing', - items: [ - { text: 'Writing media files', link: '/guide/writing-media-files' }, - { text: 'Media sources', link: '/guide/media-sources' }, - { text: 'Output formats', link: '/guide/output-formats' }, - ], - }, - { - text: 'Conversion', - items: [ - { text: 'Converting media files', link: '/guide/converting-media-files' }, - ], - }, - { - text: 'Miscellaneous', - items: [ - { text: 'Packets & samples', link: '/guide/packets-and-samples' }, - { text: 'Supported formats & codecs', link: '/guide/supported-formats-and-codecs' }, - ], - }, - { - text: 'Extensions', - items: [ - { text: 'mp3-encoder', link: '/guide/extensions/mp3-encoder' }, - ], - }, - ], + sidebar: { + '/guide': [ + { + text: 'Getting started', + items: [ + { text: 'Introduction', link: '/guide/introduction' }, + { text: 'Installation', link: '/guide/installation' }, + { text: 'Quick start', link: '/guide/quick-start' }, + ], + }, + { + text: 'Reading', + items: [ + { text: 'Reading media files', link: '/guide/reading-media-files' }, + { text: 'Media sinks', link: '/guide/media-sinks' }, + { text: 'Input formats', link: '/guide/input-formats' }, + ], + }, + { + text: 'Writing', + items: [ + { text: 'Writing media files', link: '/guide/writing-media-files' }, + { text: 'Media sources', link: '/guide/media-sources' }, + { text: 'Output formats', link: '/guide/output-formats' }, + ], + }, + { + text: 'Conversion', + items: [ + { text: 'Converting media files', link: '/guide/converting-media-files' }, + ], + }, + { + text: 'Miscellaneous', + items: [ + { text: 'Packets & samples', link: '/guide/packets-and-samples' }, + { text: 'Supported formats & codecs', link: '/guide/supported-formats-and-codecs' }, + ], + }, + { + text: 'Extensions', + items: [ + { text: 'mp3-encoder', link: '/guide/extensions/mp3-encoder' }, + ], + }, + ], + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + '/api': apiRoutes, + }, socialLinks: [ { icon: 'github', link: 'https://github.com/Vanilagy/mediabunny' }, @@ -114,7 +122,11 @@ export default withMermaid({ plugins: [ // eslint-disable-next-line @typescript-eslint/no-explicit-any tailwindcss() as any, - llmstxt(), + llmstxt({ + ignoreFiles: [ + 'api/*', + ], + }), ], }, outDir: '../dist-docs', diff --git a/docs/api-config.json b/docs/api-config.json new file mode 100644 index 0000000..e160b55 --- /dev/null +++ b/docs/api-config.json @@ -0,0 +1,22 @@ +{ + "heading": "Mediabunny API reference", + "intro": "Here you can find detailed documentation for all classes, functions, constants and types exposed by Mediabunny's public API.", + + "Samples": "Raw, unencoded chunks of media data, such as video frames or sections of audio.", + "Packets": "Chunks of encoded media data.", + "Input files & tracks": "Read input files and their tracks; demuxer API.", + "Input formats": "Container formats that Mediabunny can read.", + "Input sources": "The sources that can provide data to an `Input`.", + "Output files": "Create and write new media files; muxer API.", + "Output formats": "Container formats that Mediabunny can write.", + "Output targets": "The targets where `Output` writes data to.", + "Media sinks": "Methods for extracting media data from input files.", + "Media sources": "Methods for adding media data to output files.", + "Conversion": "A simple API for converting and transforming media files.", + "Codecs": "Codecs understood by Mediabunny.", + "Encoding": "Encoder configuration and encodability checks.", + "Custom coders": "API for adding custom encoders and decoders.", + "Miscellaneous": "Whatever's left.", + + "@mediabunny/mp3-encoder": "Adds MP3 encoder support to Mediabunny." +} diff --git a/docs/assets/mediarobot.svg b/docs/assets/mediarobot.svg new file mode 100644 index 0000000..a931796 --- /dev/null +++ b/docs/assets/mediarobot.svg @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/guide/converting-media-files.md b/docs/guide/converting-media-files.md index 6e4b8ce..c96e629 100644 --- a/docs/guide/converting-media-files.md +++ b/docs/guide/converting-media-files.md @@ -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: diff --git a/docs/guide/reading-media-files.md b/docs/guide/reading-media-files.md index 8e24aaf..d9af1c8 100644 --- a/docs/guide/reading-media-files.md +++ b/docs/guide/reading-media-files.md @@ -420,7 +420,7 @@ This source is the fastest but requires the entire input file to be held in memo ### `BlobSource` -This source is backed by an underlying [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) object. Since [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) extends `Blob`, this source is perfect for reading data directly from disk. +This source is backed by an underlying [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) object. Since [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) extends `Blob`, this source is perfect for reading data directly from disk (in the browser). ```ts import { BlobSource } from 'mediabunny'; @@ -430,21 +430,26 @@ fileInput.addEventListener('change', (event) => { }); ``` -### `UrlSource` +`BlobSource` accepts additional options as a second parameter: +```ts +type BlobSourceOptions = { + // The maximum number of bytes the cache is allowed to hold + // in memory. Defaults to 8 MiB. + maxCacheSize?: number; +}; +``` -::: warning -This is a **beta** feature. `UrlSource` tends to make tons of requests and is potentially slow. This is something that will be fixed in the near future. +### `UrlSource` -It still works, but keep in mind it's going to be much higher-latency than reading directly from disk or from memory. -::: - -This source fetches data from a URL. This is useful for reading files over the network. +This source fetches data from a remote URL, useful for reading files over the network. ```ts import { UrlSource } from 'mediabunny'; const source = new UrlSource('https://example.com/bigbuckbunny.mp4'); ``` +`UrlSource` will do some pretty crazy stuff to prefetch data intelligently based on observed access patterns to minimize request count and latency. + ::: warning If you're using this source in the browser and the URL is on a different origin, make sure [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS) is properly configured. ::: @@ -454,6 +459,10 @@ If you're using this source in the browser and the URL is on a different origin, type UrlSourceOptions = { requestInit?: RequestInit; getRetryDelay?: (previousAttempts: number) => number | null; + + // The maximum number of bytes the cache is allowed to hold + // in memory. Defaults to 8 MiB. + maxCacheSize?: number; }; ``` @@ -476,13 +485,31 @@ const source = new UrlSource('https://example.com/bigbuckbunny.mp4', { }); ``` -Not setting `getRetryDelay` means requests will not be retried. +Not setting `getRetryDelay` will default to an infinite, capped exponential backoff pattern. + +### `FilePathSource` + +This input source can be used to load data directly from a file, given a file path. It requires a server-side environment such as Node, Bun, or Deno. +```ts +import { FilePathSource } from 'mediabunny'; + +const source = new FilePathSource('/home/david/Downloads/bigbuckbunny.mp4'); +``` + +`FilePathSource` accepts additional options as a second parameter: +```ts +type FilePathSourceOptions = { + // The maximum number of bytes the cache is allowed to hold + // in memory. Defaults to 8 MiB. + maxCacheSize?: number; +}; +``` ### `StreamSource` -This is a general-purpose input source you can use to read data from anywhere. All other input sources can be implemented on top of `StreamSource`. +This is a general-purpose input source you can use to read data from anywhere. -For example, here we're reading a file from disk using the Node.js file system: +For example, here we're reading a file from disk using the Node.js file system (although you should use [`FilePathSource`](#filepathsource) for that): ```ts import { StreamSource } from 'mediabunny'; import { open } from 'node:fs/promises'; @@ -505,11 +532,101 @@ const source = new StreamSource({ The options of `StreamSource` have the following type: ```ts type StreamSourceOptions = { - // Called when data is requested. - // Should return or resolve to the bytes from the specified byte range. - read: (start: number, end: number) => Uint8Array | Promise; - // Called when the size of the entire file is requested. - // Should return or resolve to the size in bytes. - getSize: () => number | Promise; + getSize: () => MaybePromise; + read: (start: number, end: number) => MaybePromise>; + maxCacheSize?: number; + prefetchProfile?: 'none' | 'fileSystem' | 'network'; }; + +type MaybePromise = T | Promise; +``` + +- `getSize`\ + Called when the size of the entire file is requested. Must return or resolve to the size in bytes. This function is guaranteed to be called before `read`. +- `read`\ + Called when data is requested. Must return or resolve to the bytes from the specified byte range, or a stream that yields these bytes. +- `maxCacheSize`\ + The maximum number of bytes the cache is allowed to hold in memory. Defaults to 8 MiB. +- `prefetchProfile`\ + 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. + +### `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(); +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({ + 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 ``` \ No newline at end of file diff --git a/docs/guide/writing-media-files.md b/docs/guide/writing-media-files.md index 5a20e6a..5e58081 100644 --- a/docs/guide/writing-media-files.md +++ b/docs/guide/writing-media-files.md @@ -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. diff --git a/docs/index.md b/docs/index.md index b1411f5..74fe63b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -14,6 +14,9 @@ hero: - theme: brand text: Hop in link: /guide/introduction + - theme: alt + text: API + link: /api - theme: alt text: Examples link: /examples @@ -88,6 +91,7 @@ const bundleSizes = [ const sponsors = { gold: [ + { image: '/sponsors/remotion.png', name: 'Remotion', url: 'https://remotion.dev/' }, { image: '/sponsors/gling.svg', name: 'Gling AI', url: 'https://www.gling.ai/' }, { image: '/sponsors/diffusionstudio.png', name: 'Diffusion Studio', url: 'https://diffusion.studio/' }, { image: '/sponsors/kino.jpg', name: 'Kino', url: 'https://kino.ai/' }, @@ -99,12 +103,14 @@ const sponsors = { { image: 'https://avatars.githubusercontent.com/u/84167135', name: 'Memenome', url: 'https://github.com/memenome' }, { image: 'https://avatars.githubusercontent.com/u/5913254', name: 'Brandon McConnell', url: 'https://github.com/brandonmcconnell' }, { image: 'https://avatars.githubusercontent.com/u/9549394', name: 'studnitz', url: 'https://github.com/studnitz' }, + { image: 'https://avatars.githubusercontent.com/u/504909', name: 'Hirbod', url: 'https://github.com/hirbod' }, { image: 'https://avatars.githubusercontent.com/u/30229596', name: 'Pablo Bonilla', url: 'https://github.com/devPablo' }, { image: 'https://avatars.githubusercontent.com/u/63088713', name: 'taf2000', url: 'https://github.com/taf2000' }, { image: 'https://avatars.githubusercontent.com/u/58149663', name: 'H7GhosT', url: 'https://github.com/H7GhosT' }, { image: 'https://avatars.githubusercontent.com/u/91711202', name: 'ihasq', url: 'https://github.com/ihasq' }, { image: 'https://avatars.githubusercontent.com/u/61233224', name: 'Allwhy', url: 'https://github.com/Allwhy' }, { image: 'https://avatars.githubusercontent.com/u/97225946', name: '808vita', url: 'https://github.com/808vita' }, + { image: 'https://avatars.githubusercontent.com/u/3709646', name: 'Rodrigo Belfiore', url: 'https://github.com/roprgm' }, ], }; diff --git a/docs/llms.md b/docs/llms.md new file mode 100644 index 0000000..b81b70d --- /dev/null +++ b/docs/llms.md @@ -0,0 +1,28 @@ +# Mediabunny and LLMs + +
+
+
+ +
+
+ +While Mediabunny is proudly human-coded, we want to encourage any and all usage of Mediabunny, even when the vibes are high. + +Mediabunny is still new and is unlikely to be in the training data of modern LLMs, but we can still make the AI perform extremely well but just giving it a little more context. + +--- + +Give one or more of these files to your LLM: + +### [mediabunny.d.ts](/mediabunny.d.ts) + +This file contains the entire public TypeScript API of Mediabunny and is commented extremely thoroughly. + +### [llms.txt](/llms.txt) + +This file provides an index of Mediabunny's guide, which the AI can then further dive into if it wants to. + +### [llms-full.txt](/llms-full.txt) + +This is just the entire Mediabunny guide in a single file. diff --git a/docs/public/sponsors/remotion.png b/docs/public/sponsors/remotion.png new file mode 100644 index 0000000..f76b061 Binary files /dev/null and b/docs/public/sponsors/remotion.png differ diff --git a/examples/file-compression/file-compression.ts b/examples/file-compression/file-compression.ts index 7cd0118..310eaca 100644 --- a/examples/file-compression/file-compression.ts +++ b/examples/file-compression/file-compression.ts @@ -2,6 +2,7 @@ import { Input, ALL_FORMATS, BlobSource, + UrlSource, Output, BufferTarget, Mp4OutputFormat, @@ -12,7 +13,8 @@ import { import SampleFileUrl from '../../docs/assets/big-buck-bunny-trimmed.mp4'; (document.querySelector('#sample-file-download') as HTMLAnchorElement).href = SampleFileUrl; -const selectMediaButton = document.querySelector('button') as HTMLButtonElement; +const selectMediaButton = document.querySelector('#select-file') as HTMLButtonElement; +const loadUrlButton = document.querySelector('#load-url') as HTMLButtonElement; const fileNameElement = document.querySelector('#file-name') as HTMLParagraphElement; const horizontalRule = document.querySelector('hr') as HTMLHRElement; const progressBarContainer = document.querySelector('#progress-bar-container') as HTMLDivElement; @@ -25,11 +27,11 @@ const errorElement = document.querySelector('#error-element') as HTMLParagraphEl let currentConversion: Conversion | null = null; let currentIntervalId = -1; -const compressFile = async (file: File) => { +const compressFile = async (resource: File | string) => { clearInterval(currentIntervalId); await currentConversion?.cancel(); - fileNameElement.textContent = file.name; + fileNameElement.textContent = resource instanceof File ? resource.name : resource; horizontalRule.style.display = ''; progressBarContainer.style.display = ''; speedometer.style.display = ''; @@ -39,12 +41,17 @@ const compressFile = async (file: File) => { errorElement.textContent = ''; try { - // Create a new input from the file + // Create a new input from the resource + const source = resource instanceof File + ? new BlobSource(resource) + : new UrlSource(resource); const input = new Input({ - source: new BlobSource(file), + source, formats: ALL_FORMATS, // Accept all formats }); + const fileSize = await source.getSize(); + // Define the output file const output = new Output({ target: new BufferTarget(), @@ -96,7 +103,7 @@ const compressFile = async (file: File) => { compressionFacts.style.display = ''; compressionFacts.textContent - = `${(output.target.buffer!.byteLength / file.size * 100).toPrecision(3)}% of original size`; + = `${(output.target.buffer!.byteLength / fileSize * 100).toPrecision(3)}% of original size`; } catch (error) { console.error(error); @@ -130,6 +137,19 @@ selectMediaButton.addEventListener('click', () => { fileInput.click(); }); +loadUrlButton.addEventListener('click', () => { + const url = prompt( + 'Please enter a URL of a media file. Note that it must be HTTPS and support cross-origin requests, so have the' + + ' right CORS headers set.', + 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4', + ); + if (!url) { + return; + } + + void compressFile(url); +}); + document.addEventListener('dragover', (event) => { event.preventDefault(); event.dataTransfer!.dropEffect = 'copy'; diff --git a/examples/file-compression/index.html b/examples/file-compression/index.html index 6ec1051..8a788d6 100644 --- a/examples/file-compression/index.html +++ b/examples/file-compression/index.html @@ -15,15 +15,22 @@

File compression example

Select or drop a media file, and Mediabunny will convert it to a heavily-compressed MP4 file.

-
- + +

diff --git a/examples/media-player/index.html b/examples/media-player/index.html index 17eaa76..f780c14 100644 --- a/examples/media-player/index.html +++ b/examples/media-player/index.html @@ -15,21 +15,29 @@

Media player example

Select or drop a media file, and a fully custom, Mediabunny-powered player will appear.

-
- + +

+ - +