@browser-mc/browser-movie-converter
v1.3.0
Published
Browser movie conversion package using Mediabunny for demux/mux/encode orchestration and the local WebCodecs color helpers for resizing.
Readme
@browser-mc/browser-movie-converter
Browser movie conversion package using Mediabunny for demux/mux/encode orchestration and the local WebCodecs color helpers for resizing.
- Caller-provided Mediabunny
Inputfor MP4/MOV/WebM and other supported sources - MP4/WebM output through Mediabunny
Conversion - HLS output through local Mediabunny helpers
- Streaming scene detection and keyframe forcing through
@browser-mc/mediabunny-scene-keyframes - Raw planar resize through
@browser-mc/webcodecs-color - Input color-space inspection and color metadata copying for CPU-resized samples
Install
pnpm add @browser-mc/browser-movie-converter mediabunnyBuild Mediabunny conversion options
import {
BlobSource,
BufferTarget,
Conversion,
Input,
Mp4OutputFormat,
Output,
QuickTimeInputFormat,
} from 'mediabunny';
import {
buildMovieConversionOptions,
} from '@browser-mc/browser-movie-converter';
const input = new Input({
source: new BlobSource(file),
formats: [new QuickTimeInputFormat()],
});
const target = new BufferTarget();
const output = new Output({
target,
format: new Mp4OutputFormat({ fastStart: 'in-memory' }),
});
const plan = await buildMovieConversionOptions({
input,
output,
videoTrackQuery: {
filter: (track) => track.number === 1,
},
video: {
codec: 'avc',
bitrate: { quality: 0.75 },
},
resize: {
width: 1280,
rawBitDepth: 8,
rawChromaSubsampling: '420',
},
sceneDetection: {
sampleRate: 'all',
threshold: 0.2,
},
colorMetadata: 'preserve',
});
const conversion = await Conversion.init(plan.options);
if (!conversion.isValid) {
throw new Error('Mediabunny could not create a valid conversion');
}
await conversion.execute();
console.log(plan.sceneKeyFrames?.state.keyFrameTimestamps);
console.log(plan.videoColor?.colorSpace);
console.log(plan.resize);
console.log(target.buffer);Capability Checks
import {
checkMovieConversionSupport,
checkMovieRawFrameSupport,
checkMovieVideoEncoderBitDepthSupport,
checkMovieVideoEncoderConfigSupport,
} from '@browser-mc/browser-movie-converter';
const rawFrame = checkMovieRawFrameSupport({
width: 1280,
height: 720,
sourceFormat: 'I444P10',
rawBitDepth: 8,
rawChromaSubsampling: '420',
});
const encoder = await checkMovieVideoEncoderConfigSupport({
codec: 'avc1.64001f',
width: 1280,
height: 720,
bitrate: 4_000_000,
framerate: 30,
});
const support = await checkMovieConversionSupport({
input,
output,
video: { codec: 'avc', bitrate: 4_000_000 },
resize: { width: 1280 },
});
const encoderBitDepths = await checkMovieVideoEncoderBitDepthSupport();
console.log(support.supported, support.decodable, support.convertible);
console.log(support.conversion?.discardedTracks);
console.log(rawFrame.supported, rawFrame.format);
console.log(encoder.supported, encoder.config?.codec);
console.table(encoderBitDepths.map(({ codec, bitDepth, chromaSubsampling, fullCodecString, supported }) => ({
codec,
bitDepth,
chromaSubsampling,
fullCodecString,
supported,
})));HLS
import {
BlobSource,
Input,
QuickTimeInputFormat,
} from 'mediabunny';
import {
convertMovieToHls,
decodeMovieHlsText,
} from '@browser-mc/browser-movie-converter';
const input = new Input({
source: new BlobSource(file),
formats: [new QuickTimeInputFormat()],
});
for await (const asset of convertMovieToHls({
input,
tracks: 'primary',
videoTrackQuery: {
filter: (track) => track.number === 1,
},
targetDuration: 2,
resize: {
width: 640,
},
variants: [
{
resize: {
width: 1280,
},
video: {
bitrate: 4_000_000,
},
},
{
video: {
bitrate: 1_500_000,
},
},
],
sceneDetection: {
sampleRate: 'all',
threshold: 0.2,
},
})) {
if (asset.path.endsWith('.m3u8')) {
const bytes = new Uint8Array(await new Response(asset.data).arrayBuffer());
console.log(asset.path, decodeMovieHlsText(bytes));
} else {
await uploadSegment(asset.path, asset.data);
}
}Advanced Options
quantizer is an advanced encoder-control option for AVC/H.264 style quantizer tuning:
const plan = await buildMovieConversionOptions({
input,
output,
video: {
codec: 'avc',
bitrate: 4_000_000,
},
quantizer: {
keyFrame: 28,
deltaFrame: 36,
},
});It accepts either a single integer or { keyFrame, deltaFrame }. Lower values preserve more quality, and higher values compress more aggressively. Values must be integers from 0 to 63.
When using quantizer, still set video.bitrate as a compatibility hint. Mediabunny uses bitrate, not quantizer values, when choosing the AVC/H.264 level for the generated codec string.
When split quantizer values are used with keyFrameInterval, the interval is handled by this package so interval key frames can receive the keyFrame quantizer.
Notes
- This package builds Mediabunny
ConversionOptions; callers choose theOutput, target, and finalConversionlifecycle.checkMovieConversionSupport()loads the selected tracks, calls each selected track'scanDecode(), builds the conversion plan, and initializes MediabunnyConversionwithout executing it. It reportssupported: trueonly when all selected tracks are decodable and the initialized conversion is valid. - When
resizeis set, the generated video options useVideoSample.toVideoFrame()pluswebcodecs-color.resizeVideoFrame()inside Mediabunny'sprocesshook. Supported planar YUV/YUVA andNV12frames use CPU planar resize.RGBA,RGBX,BGRA,BGRX, and unsupported or unknownVideoFrameformats resize through Canvas and returnRGBAwith a warning.resize.rawBitDepthandresize.rawChromaSubsamplingcan additionally convert supported planar frames before encoding; both default topreserve.NV12frames are preserved asNV12during resize when both controls are preserved, or unpacked toI420when raw planar conversion is requested. rawBitDepthcontrols the raw planarVideoFrameproduced before encoding; it does not by itself prove that the chosen movie codec accepts that frame bit depth. UsecheckMovieRawFrameSupportfor the planned raw frame format,checkMovieVideoEncoderConfigSupportfor the exactVideoEncoderConfigyou intend to use, andcheckMovieVideoEncoderBitDepthSupport()to compare default 1080p 8-bit/10-bit and 4:2:0/4:2:2/4:4:4 encoder configs across codecs.convertMovieToHlsstreams HLS assets throughReadableStream<Uint8Array>and requiresvariants, producing one HLS video encode per variant. Top-level resize, scene detection, quantizer, color metadata, force transcode, and key-frame options act as defaults; variant values override them. Audio is encoded once and paired with every video variant.- HLS segment formats are controlled by
segmentFormat: { mpegts?: boolean; cmaf?: boolean }, bothtrueby default. Per playlist, Mediabunny picks the first enabled format that supports all of its codecs: MPEG-TS handles avc/hevc video, so codecs it cannot contain (such asav1orvp9variants) automatically fall through to CMAF (fragmented MP4) segments with aninit-{n}.mp4init segment. TS and CMAF variants can coexist in one master playlist. - For AVC/H.264 transcodes, Mediabunny currently builds
avc1.64....codec strings by default, which corresponds to High Profile. If the video track is copied without transcoding, the source profile is preserved instead. - Resize dimensions are rounded down to a multiple of
dimensionAlignment, defaulting to2, which avoids odd-size 4:2:0/NV12 artifacts and encoder constraints. - Scene detection defaults to
sampleRate: 'all', so every decoded video sample is considered while conversion runs. Detected scene samples are marked immediately with MediabunnyVideoSampleencode options to force key frames. colorMetadata: 'preserve'copies the source sample'sVideoColorSpacemetadata to CPU-resized planar samples. Canvas-resized samples use the generated frame color space.colorMetadata: 'canvas-sdr'draws frames through an sRGB Canvas path and marks output samples as BT.709 SDR; it is a practical browser conversion path, not a dedicated HDR tone-mapping engine.- For browser tests with H.264/AAC material, use a browser build that has proprietary codec support, such as installed Chrome/Electron rather than Playwright's bundled Chromium.
