TurboRes Apple ProRes Video Decoder
repository·main·Indexed 40 days ago
https://github.com/vanilagy/turboresA high-performance Apple ProRes video decoder library for browsers and JavaScript environments, written in Zig and TypeScript. TurboRes 1.2.2 provides faster-than-real-time decoding (including 4K) without hardware acceleration, supporting shared-memory multithreading and custom pixel format conversions.
What's inside TurboRes
- TurboRes is licensed under the Mozilla Public License 2.0. This license permits both private and commercial use, but requires that any modifications to MPL-covered files be made available to recipients when the software is distributed.
Configure Multithreading and Concurrency
mainTurboRes supports two multithreading models:
- Shared-memory multithreading: The fastest option. Multiple threads operate on the same memory to decode a single packet. Requires cross-origin isolation in browsers (see COOP/COEP headers).
- Worker pool-based multithreading: Multiple workers decode different packets in parallel. This has higher latency due to message passing.
Configuration Options
useSharedMemory: Set totruefor shared-memory multithreading.concurrency: Number of threads to use. Defaults tonavigator.hardwareConcurrency. Set to0to force synchronous decoding on the main thread.
Browser Requirements for Shared Memory
To use
useSharedMemory: truein a browser, you must set one of the following sets of HTTP response headers:Option A (Standard):
Cross-Origin-Opener-Policy: same-originCross-Origin-Embedder-Policy: require-corp
Option B (More permissive, but not supported in Safari):
Cross-Origin-Opener-Policy: same-originCross-Origin-Embedder-Policy: credentialless
// Enable shared-memory multithreading with 8 threads const decoder = await Decoder.create({ useSharedMemory: true, concurrency: 8, }); // Force synchronous decoding const syncDecoder = await Decoder.create({ useSharedMemory: true, concurrency: 0, });Build and develop TurboRes locally
mainTo set up a local development environment for TurboRes, you need Zig 0.16 and a modern version of Node.js and npm.
Follow these steps to clone, install, and build the project:
- Install dependencies: Run
npm installafter cloning the repository. - Compile Zig code: Use the provided shell script to compile the core decoding logic.
- For a debug build:
./scripts/build-zig.sh - For an optimized release build:
./scripts/build-zig.sh --release
- For a debug build:
- Build the library: Run
npm run buildto generate the final library. - Run the development server: Use
npm run devto start the Vite development server, which can serve the project's HTML files. - Work with the demo: If you are working on the demo, ensure the Zig code is already built, then use
npm run demo:devto run it ornpm run demo:buildto build it.
npm install ./scripts/build-zig.sh --release npm run build- Install dependencies: Run
Run tests and linting for TurboRes
mainTo ensure code quality and verify the implementation, use the following npm scripts:
- Typechecking:
npm run check - Linting:
npm run lint - Testing:
npm test(Note: This runs the full test suite using the Zig debug build).
npm run check npm run lint npm test- Typechecking:
Import TurboRes via ESM or UMD
mainIf you are using the ESM build downloaded from releases:
import { /* ... */ } from './turbores.js';If you are using the UMD build via a script tag in HTML, it exposes a global
TurboResnamespace:<script src="./turbores.umd.cjs"></script>Benchmark TurboRes, ffmpeg.wasm, and native FFmpeg
mainYou can compare the decoding performance of TurboRes, ffmpeg.wasm, and native FFmpeg using the provided benchmark files. All benchmarks measure the time taken to decode a specific video file ten times to determine decoding speed.
Web-based Benchmarks (TurboRes and ffmpeg.wasm)
Use the following HTML files for web-based testing:
benchmark-turbores.htmlbenchmark-ffmpeg-wasm.html
Important: When running these HTML benchmarks, ensure your browser console is closed, as an open console can slow down WASM execution and skew results.
Native FFmpeg Benchmark
Use the shell script to benchmark native FFmpeg via the CLI:
./benchmark-ffmpeg-native.sh -t <threads> [-a] <video-file>Flags:
-t <threads>: Specify the number of threads to use.-a: Enable hardware acceleration. Note: This requires running on a Mac with VideoToolbox available.
./benchmark-ffmpeg-native.sh -t 8 -a input.movInstall TurboRes
mainYou can install TurboRes via npm for most JavaScript environments:
npm install turboresAlternatively, you can download the ESM build (
turbores.jsandturbores.d.ts) or the UMD build (turbores.umd.cjs) directly from the releases page and import them manually.Basic Usage: Decode ProRes packets
mainTo decode ProRes media, create a
Decoderinstance and use it to decode individual packets intoFrameobjects. TurboRes uses an "errors as values" pattern, so you should check if the returned result is an instance ofError.- Create a Decoder: Use
Decoder.create()with configuration options likeproresFourCc(e.g.,'apch') anduseSharedMemory. - Create a Frame: Instantiate a
new Frame()to hold the output data. - Decode: Call
decoder.decode(packetData, frame)wherepacketDatais aUint8Arraycontaining a single ProRes frame. - Cleanup: Always call
frame.clear()anddecoder.close()to free resources.
import { Decoder, Frame } from 'turbores'; // Create a decoder const decoder = await Decoder.create({ proresFourCc: 'apch', useSharedMemory: true, }); if (decoder instanceof Error) { // Handle the error... } // Create a frame to hold the output data const frame = new Frame(); const result = await decoder.decode( packetData, // Uint8Array containing a single ProRes frame frame, ); if (result instanceof Error) { // Handle the error... } // When done, clean up frame.clear(); decoder.close();- Create a Decoder: Use
Understand Frame pixel formats and scan types
mainTurboRes supports various YUV pixel formats and scan types. The pixel formats are compatible with the WebCodecs API's
VideoPixelFormat.Supported Pixel Formats
Formats include various chroma subsampling (4:2:0, 4:2:2, 4:4:4), bit depths (8, 10, 12), and alpha channel presence (e.g.,
I420,I420P10,I422A,I444AP12).Scan Types
'progressive': All lines are stored in a single pass.'interlaced-top-field-first': Interlaced, with the top field appearing first.'interlaced-bottom-field-first': Interlaced, with the bottom field appearing first.
import { PIXEL_FORMATS, type PixelFormat, type ScanType } from './frame.js'; // PIXEL_FORMATS contains: // 'I420', 'I420P10', 'I420P12', 'I420A', 'I420AP10', 'I420AP12', // 'I422', 'I422P10', 'I422P12', 'I422A', 'I422AP10', 'I422AP12', // 'I444', 'I444P10', 'I444P12', 'I444A', 'I444AP10', 'I444AP12'Manage decoder lifecycle and backpressure
mainThe
Decoderclass provides mechanisms to manage resources and prevent memory exhaustion during high-throughput decoding.Backpressure and Queue Management:
decodeQueueSize: Returns the number of decoding tasks currently queued but not yet finished. Monitor this to apply backpressure to your packet source.desiredSize: Returns how many more packets can be queued before reaching the internal_highWaterMark. Keep queuing while this value is positive to maximize performance.dequeued: A promise that resolves whenever a packet finishes decoding. Use this to synchronize with the decoder's progress.
Resource Cleanup:
close(): Closes the decoder and releases internal resources. It returns a promise that resolves once all currently queued decodes are complete. This method is idempotent.- The decoder also supports the
DisposableandAsyncDisposableinterfaces (usingSymbol.disposeandSymbol.asyncDispose).
How SharedMemoryRuntime and MessagePassingRuntime work
mainTurboRes uses two different runtime models for managing workers and memory, depending on the performance requirements and environment capabilities.
SharedMemoryRuntimeDesigned for high-performance scenarios using WebAssembly shared memory. It initializes a single
WebAssembly.Memoryobject that is shared across all workers. This allows for zero-copy data access but requires careful management of thread-local state (TLS).- Use
getSharedMemoryRuntime()to access the singleton instance. - Use
ensureWorkers(count)to scale the number of workers.
MessagePassingRuntimeA more traditional model where workers communicate via message passing. This is useful when shared memory is not desired or available.
- Use
getMessagePassingRuntime()to access the singleton instance. - Use
registerDecoder(decoderId, bitDepth, allowedOutputFormats)to inform workers about available decoders. - Use
ensureWorkers(count)to scale the number of workers.
- Use
Inspect Decoded Frame Data
mainWhen
decoder.decode()succeeds, it returns aFilledFrameobject containing the decoded pixel data and metadata.Key properties include:
frameData:Uint8Arraycontaining the pixel data.pixelFormat: The format string (e.g.,'I422P10').scanType:'progressive'or'interlaced'.codedWidth/codedHeight: The dimensions of the encoded packet.visibleWidth/visibleHeight: The dimensions of the visible area.pixelAspectRatio: An object with{ num, den }.colorPrimaries,colorTransfer,colorMatrix: Integer identifiers for color metadata.colorRangeFull: Boolean indicating if the color range is full.
Note: The frame's data is valid until it is used for a new decoding task. To keep multiple frames, create multiple
Frameinstances.// Example of result properties result.frameData; // => Uint8Array result.pixelFormat; // => 'I422P10' result.scanType; // => 'progressive' result.codedWidth; // => 1920 result.codedHeight; // => 1088 result.visibleWidth; // => 1920 result.visibleHeight; // => 1080 result.pixelAspectRatio; // => { num: 1, den: 1 } result.colorPrimaries; // => 9 (ITU-R BT.2020) result.colorTransfer; // => 18 (ITU-R BT.2100-2 (HLG)) result.colorMatrix; // => 9 (ITU-R BT.2020) result.colorRangeFull; // => false