import { createContext, useContext } from 'react';
import { useRemotionEnvironment } from 'remotion';
* Decoder behind a video layer. 'webcodecs' is @remotion/media's <Video>
* (Mediabunny + the browser's WebCodecs decoder); 'offthread' is Remotion's
* <OffthreadVideo> (the FFmpeg compositor, outside the browser).
* remotion/video-decoder.mjs picks the server render's value per platform.
*/
export type ServerVideoDecoder = 'webcodecs' | 'offthread';
export function isServerVideoDecoder(value: unknown): value is ServerVideoDecoder {
return value === 'webcodecs' || value === 'offthread';
}
export const ServerVideoDecoderContext = createContext<ServerVideoDecoder>('webcodecs');
export interface VideoDecodeEnvironment {
isRendering: boolean;
isClientSideRendering: boolean;
isPlayer: boolean;
}
* Only the headless server render honours the server's choice. The Player and
* the in-browser export (@remotion/web-renderer, which cannot run
* <OffthreadVideo> at all) always decode with @remotion/media.
*/
export function runtimeVideoDecoder(
requested: ServerVideoDecoder,
environment: VideoDecodeEnvironment,
browserRenderer = false,
): ServerVideoDecoder {
if (browserRenderer || environment.isClientSideRendering || environment.isPlayer
|| !environment.isRendering) return 'webcodecs';
return requested;
}
* Every decoded video layer of one composition — plain clips, background
* fills, shared runs and GL effect inputs — asks here, so a render never mixes
* decoders between an effect clip and its non-effect neighbours.
*/
export function useRuntimeVideoDecoder(browserRenderer = false): ServerVideoDecoder {
const requested = useContext(ServerVideoDecoderContext);
const environment = useRemotionEnvironment();
return runtimeVideoDecoder(requested, environment, browserRenderer);
}
* @remotion/media shows the last frame whose timestamp is at most 1 ms after
* the requested time (the tolerance in its getFrameFromTimestamp); the FFmpeg
* compositor takes the last frame at or before it. For fractional-rate footage
* (29.97 fps in a 30 fps timeline) the two therefore disagree by one source
* frame whenever a frame lands inside that millisecond — so the compositor is
* asked 1 ms later, and a Windows export shows the frames the Player shows.
*/
export const WEBCODECS_FRAME_TOLERANCE_SECONDS = 0.001;
export function offthreadTrimBefore(trimBefore: number | undefined, fps: number): number {
return (trimBefore ?? 0) + WEBCODECS_FRAME_TOLERANCE_SECONDS * fps;
}
* <OffthreadVideo> extracts BMP frames unless `transparent` asks for (slower)
* PNG. Alpha video reaches this app's timelines as WebM — VP9-alpha MG bakes
* and the desktop importers' transparent-MOV proxies — so WebM keeps its alpha
* and ordinary camera/phone footage keeps the fast opaque extraction.
*/
export function offthreadVideoTransparent(src: string): boolean {
const path = src.split(/[?#]/, 1)[0] ?? '';
return /\.webm$/i.test(path);
}