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/dev/demux.html b/dev/demux.html index 29f4027..9cb536d 100644 --- a/dev/demux.html +++ b/dev/demux.html @@ -16,11 +16,32 @@ source }); + const audioTrack = await input.getPrimaryAudioTrack(); + + for (let i = 0; i < 10; i++) { + console.time() + const stats = await audioTrack.computePacketStats(); + console.log(stats) + console.timeEnd() + } + //console.log(stats); + + /* const videoTrack = await input.getPrimaryVideoTrack(); console.log(await videoTrack.getFirstTimestamp(), await videoTrack.computeDuration()); console.log(videoTrack.name); 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/guide/reading-media-files.md b/docs/guide/reading-media-files.md index 8e24aaf..30e6753 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,23 @@ 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'; }; -``` \ No newline at end of file + +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. \ No newline at end of file diff --git a/examples/file-compression/file-compression.ts b/examples/file-compression/file-compression.ts index 7cd0118..7c5ea5f 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 support cross-origin requests, so have the right' + + ' CORS headers set.', + 'http://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..5952604 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.

-
- + +

+