From b41624edea14840f80a3e926e7cacb29c00837db Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 8 Jul 2026 16:21:16 +0300 Subject: [PATCH] Lazy-load mediabunny on web (#11094) Co-authored-by: Claude Fable 5 --- src/lib/media/video/compress.web.ts | 28 ++++++++++++-------- src/lib/media/video/mediabunny.ts | 20 ++++++++++++++ src/view/com/composer/videos/metadata.web.ts | 3 ++- 3 files changed, 39 insertions(+), 12 deletions(-) create mode 100644 src/lib/media/video/mediabunny.ts diff --git a/src/lib/media/video/compress.web.ts b/src/lib/media/video/compress.web.ts index 7f96141b74..46fff5b058 100644 --- a/src/lib/media/video/compress.web.ts +++ b/src/lib/media/video/compress.web.ts @@ -1,17 +1,8 @@ import {type ImagePickerAsset} from 'expo-image-picker' import { - ALL_FORMATS, type AudioCodec, - BlobSource, - BufferTarget, - canEncodeAudio, - canEncodeVideo, - Conversion, - Input, - Mp4OutputFormat, - Output, + type Input as MediaBunnyInput, type VideoCodec, - WebMOutputFormat, } from 'mediabunny' import {VIDEO_MAX_SIZE} from '#/lib/constants' @@ -23,6 +14,7 @@ import { COMPRESSION_MIN_SIZE_BYTES, COMPRESSION_TARGET_BITRATE, } from './constants' +import {loadMediaBunny} from './mediabunny' import {type CompressedVideo, type ProbedMetadata} from './types' // Codecs to try in order of preference @@ -112,6 +104,7 @@ export async function compressVideo( } async function probeWithMediaBunny(blob: Blob): Promise { + const {ALL_FORMATS, BlobSource, Input} = await loadMediaBunny() const input = new Input({source: new BlobSource(blob), formats: ALL_FORMATS}) try { const videoTrack = await input.getPrimaryVideoTrack() @@ -153,6 +146,7 @@ async function findEncodableVideoCodec( width: number, height: number, ): Promise<{codec: VideoCodec; useWebM: boolean} | null> { + const {canEncodeVideo} = await loadMediaBunny() for (const codec of VIDEO_CODECS) { const canEncode = await canEncodeVideo(codec, { width, @@ -179,9 +173,10 @@ const AUDIO_CODECS_MP4: AudioCodec[] = ['aac'] const AUDIO_CODECS_WEBM: AudioCodec[] = ['opus', 'vorbis'] async function findEncodableAudioCodec( - audioTrack: Awaited>, + audioTrack: Awaited>, useWebM: boolean, ): Promise<{codec: AudioCodec} | null> { + const {canEncodeAudio} = await loadMediaBunny() if (!audioTrack) { return null } @@ -228,6 +223,17 @@ async function doCompression( ): Promise { const {onProgress, signal} = opts + const { + ALL_FORMATS, + BlobSource, + BufferTarget, + Conversion, + Input, + Mp4OutputFormat, + Output, + WebMOutputFormat, + } = await loadMediaBunny() + const input = new Input({ source: new BlobSource(blob), formats: ALL_FORMATS, diff --git a/src/lib/media/video/mediabunny.ts b/src/lib/media/video/mediabunny.ts new file mode 100644 index 0000000000..edcbc805c1 --- /dev/null +++ b/src/lib/media/video/mediabunny.ts @@ -0,0 +1,20 @@ +type MediaBunnyModule = typeof import('mediabunny') + +let promise: Promise | undefined + +/** + * Lazily loads mediabunny into its own webpack chunk and caches the resulting + * promise so the module is fetched at most once. If the import fails (e.g. a + * transient chunk-load error), the cached promise is reset so a later call can + * retry the load. + */ +export function loadMediaBunny(): Promise { + promise ??= import(/* webpackChunkName: "mediabunny" */ 'mediabunny').catch( + e => { + // reset so a later attempt can retry after a transient chunk-load failure + promise = undefined + throw e + }, + ) + return promise +} diff --git a/src/view/com/composer/videos/metadata.web.ts b/src/view/com/composer/videos/metadata.web.ts index 62383bce7b..920dcdb6af 100644 --- a/src/view/com/composer/videos/metadata.web.ts +++ b/src/view/com/composer/videos/metadata.web.ts @@ -1,6 +1,6 @@ import {type ImagePickerAsset} from 'expo-image-picker' -import {ALL_FORMATS, BlobSource, Input} from 'mediabunny' +import {loadMediaBunny} from '#/lib/media/video/mediabunny' import {logger} from '#/logger' export function hasWebCodecs(): boolean { @@ -55,6 +55,7 @@ async function getMetadataWithWebCodecs( file: File, blobUrl: string, ): Promise { + const {ALL_FORMATS, BlobSource, Input} = await loadMediaBunny() const input = new Input({ source: new BlobSource(file), formats: ALL_FORMATS,