{
  "version": 3,
  "sources": ["../src/index.ts"],
  "sourcesContent": ["import {\n\tOutput,\n\tBufferTarget,\n\tMp4OutputFormat,\n\tWebMOutputFormat,\n\tVideoSampleSource,\n\tVideoSample,\n\tQUALITY_HIGH,\n\tcanEncodeVideo,\n} from 'mediabunny';\nimport type { ItemId } from './types.ts';\n\n/**\n * Tracks in-progress operations so they can be cancelled at async boundaries.\n */\nconst inProgressOperations = new Set< ItemId >();\n\n/**\n * Fallback per-frame duration when ImageDecoder reports none.\n * GIF spec default 10fps = 100ms (in microseconds).\n */\nconst GIF_DEFAULT_FRAME_DURATION_US = 100_000;\n\n/**\n * Message prefix for \"unsupported but graceful\" outcomes (no WebCodecs,\n * unsupported codec). Consumers detect this prefix and fall back to uploading\n * the original GIF instead of surfacing a hard error.\n *\n * The contract is the message *prefix*, not the Error type: the worker RPC\n * layer (comctx) serializes a thrown error to its `message` string only - the\n * Error subclass, `name`, and `stack` do not survive the worker boundary.\n */\nexport const UNSUPPORTED_ERROR_PREFIX = 'Unsupported';\n\n/**\n * Message prefix for GIFs skipped because they exceed the total-pixel budget.\n *\n * Starts with UNSUPPORTED_ERROR_PREFIX so existing consumers treat the skip\n * as a graceful fallback (keep the uploaded GIF, no companion video); the\n * longer prefix lets consumers distinguish it, e.g. to log a warning. Like\n * UNSUPPORTED_ERROR_PREFIX, the contract is the message prefix because only\n * the message string survives the worker boundary.\n */\nexport const SIZE_LIMIT_ERROR_PREFIX = `${ UNSUPPORTED_ERROR_PREFIX }: GIF exceeds maximum conversion size`;\n\n/**\n * Default budget for total decoded pixels (width × height × frame count)\n * beyond which conversion is not attempted.\n *\n * Conversion cost is roughly proportional to the total number of decoded\n * pixels. 300 megapixels approximates what a mid-range machine converts\n * within the ~30s the caller is willing to wait (e.g. a 1920x1080 GIF at\n * ~145 frames); anything larger would likely be abandoned anyway, so it is\n * cheaper to not start. Pass `0` to disable the check.\n */\nexport const DEFAULT_MAX_TOTAL_PIXELS = 300_000_000;\n\n/**\n * Serializes encoder access. The upload-media concurrency limit already caps\n * this at 1, but the lock guards direct callers too.\n */\nlet operationLock: Promise< void > = Promise.resolve();\n\n/**\n * Cancels all ongoing operations for a given item ID.\n *\n * Cancellation takes effect at async boundaries (waiting for the lock,\n * encoder-support check, decoder completion, between frames).\n *\n * @param id Item ID.\n * @return Whether an operation was cancelled.\n */\nexport async function cancelOperations( id: ItemId ): Promise< boolean > {\n\treturn inProgressOperations.delete( id );\n}\n\n/**\n * Pads a dimension up to the nearest even number (encoder requirement).\n *\n * @param value Dimension value.\n * @return Even dimension value.\n */\nfunction padToEven( value: number ): number {\n\treturn value % 2 === 0 ? value : value + 1;\n}\n\n/**\n * Converts an animated GIF to a video file (MP4 or WebM).\n *\n * Decodes GIF frames via the browser ImageDecoder (honoring per-frame\n * delays) and re-encodes them with mediabunny / WebCodecs.\n *\n * Accepts the GIF as a Blob so the bytes are read once, here in the worker,\n * instead of being materialized on the main thread and transferred. An\n * ArrayBuffer is still accepted for direct callers and tests.\n *\n * @param id             Item ID.\n * @param gifSource      GIF file as a Blob/File or ArrayBuffer.\n * @param outputMimeType Output MIME type ('video/mp4' or 'video/webm').\n * @param maxDimensions  Optional maximum dimension for downscaling.\n * @param maxTotalPixels Optional budget for total decoded pixels\n *                       (width × height × frame count) beyond which the\n *                       conversion is rejected with SIZE_LIMIT_ERROR_PREFIX.\n *                       Defaults to DEFAULT_MAX_TOTAL_PIXELS; `0` disables.\n * @return Encoded video buffer.\n */\nexport async function convertGifToVideo(\n\tid: ItemId,\n\tgifSource: ArrayBuffer | Blob,\n\toutputMimeType: string,\n\tmaxDimensions?: number,\n\tmaxTotalPixels?: number\n): Promise< ArrayBuffer > {\n\tinProgressOperations.add( id );\n\n\tconst previousLock = operationLock;\n\tlet releaseLock: () => void = () => {};\n\toperationLock = new Promise< void >( ( resolve ) => {\n\t\treleaseLock = resolve;\n\t} );\n\n\ttry {\n\t\tawait previousLock;\n\n\t\tif ( ! inProgressOperations.has( id ) ) {\n\t\t\tthrow new Error( 'Operation cancelled' );\n\t\t}\n\n\t\tif (\n\t\t\ttypeof ImageDecoder === 'undefined' ||\n\t\t\ttypeof VideoEncoder === 'undefined'\n\t\t) {\n\t\t\tthrow new Error(\n\t\t\t\t`${ UNSUPPORTED_ERROR_PREFIX }: WebCodecs unavailable`\n\t\t\t);\n\t\t}\n\n\t\tconst isWebm = outputMimeType === 'video/webm';\n\t\tconst codec = isWebm ? 'vp9' : 'avc';\n\n\t\tif ( ! ( await canEncodeVideo( codec ) ) ) {\n\t\t\tthrow new Error(\n\t\t\t\t`${ UNSUPPORTED_ERROR_PREFIX }: encoder codec not supported`\n\t\t\t);\n\t\t}\n\n\t\tif ( ! inProgressOperations.has( id ) ) {\n\t\t\tthrow new Error( 'Operation cancelled' );\n\t\t}\n\n\t\t// Read the bytes here (worker thread) rather than on the main thread.\n\t\tconst data =\n\t\t\tgifSource instanceof ArrayBuffer\n\t\t\t\t? gifSource\n\t\t\t\t: await gifSource.arrayBuffer();\n\n\t\tif ( ! inProgressOperations.has( id ) ) {\n\t\t\tthrow new Error( 'Operation cancelled' );\n\t\t}\n\n\t\tconst decoder = new ImageDecoder( {\n\t\t\tdata,\n\t\t\ttype: 'image/gif',\n\t\t} );\n\n\t\ttry {\n\t\t\t// Wait for the track list to be populated, not decoder.completed.\n\t\t\t// For a fully-buffered ArrayBuffer source, `completed` resolves as\n\t\t\t// soon as the bytes are received, which can be *before* the GIF is\n\t\t\t// parsed - leaving `tracks` empty and `frameCount` at 0 (decoded as\n\t\t\t// \"GIF contains no decodable frames\"). `tracks.ready` is the\n\t\t\t// promise that resolves once track metadata is available.\n\t\t\tawait decoder.tracks.ready;\n\n\t\t\tif ( ! inProgressOperations.has( id ) ) {\n\t\t\t\tthrow new Error( 'Operation cancelled' );\n\t\t\t}\n\n\t\t\tconst track = decoder.tracks.selectedTrack;\n\t\t\tconst frameCount = track?.frameCount ?? 0;\n\t\t\tif ( frameCount === 0 ) {\n\t\t\t\tthrow new Error( 'GIF contains no decodable frames' );\n\t\t\t}\n\n\t\t\t/*\n\t\t\t * Enforce the total-pixel budget before any encoding work: an\n\t\t\t * over-budget GIF would churn the CPU for minutes only to be\n\t\t\t * abandoned. Track metadata does not expose dimensions, so decode\n\t\t\t * the first frame (cheap) to learn them.\n\t\t\t */\n\t\t\tconst pixelBudget = maxTotalPixels ?? DEFAULT_MAX_TOTAL_PIXELS;\n\t\t\tif ( pixelBudget > 0 ) {\n\t\t\t\tconst { image: probe } = await decoder.decode( {\n\t\t\t\t\tframeIndex: 0,\n\t\t\t\t} );\n\t\t\t\tconst probeWidth = probe.displayWidth;\n\t\t\t\tconst probeHeight = probe.displayHeight;\n\t\t\t\tprobe.close();\n\n\t\t\t\tif ( ! inProgressOperations.has( id ) ) {\n\t\t\t\t\tthrow new Error( 'Operation cancelled' );\n\t\t\t\t}\n\n\t\t\t\tconst totalPixels = probeWidth * probeHeight * frameCount;\n\t\t\t\tif ( totalPixels > pixelBudget ) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`${ SIZE_LIMIT_ERROR_PREFIX } (${ probeWidth }x${ probeHeight } x ${ frameCount } frames = ${ totalPixels } pixels; limit is ${ pixelBudget })`\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst source = new VideoSampleSource( {\n\t\t\t\tcodec,\n\t\t\t\tbitrate: QUALITY_HIGH,\n\t\t\t\t/*\n\t\t\t\t * A sparser key frame cadence than mediabunny's 2s default\n\t\t\t\t * roughly halves the output size for long GIFs at no\n\t\t\t\t * encode-time or quality cost. These looping, autoplaying\n\t\t\t\t * GIF replacements don't need fine seek granularity.\n\t\t\t\t */\n\t\t\t\tkeyFrameInterval: 10,\n\t\t\t} );\n\t\t\tconst target = new BufferTarget();\n\t\t\tconst output = new Output( {\n\t\t\t\tformat: isWebm ? new WebMOutputFormat() : new Mp4OutputFormat(),\n\t\t\t\ttarget,\n\t\t\t} );\n\t\t\toutput.addVideoTrack( source );\n\t\t\tawait output.start();\n\n\t\t\t// ImageDecoder durations are MICROSECONDS; mediabunny VideoSample\n\t\t\t// timestamps/durations are SECONDS. Accumulate in seconds.\n\t\t\tlet timestampSec = 0;\n\t\t\tfor ( let i = 0; i < frameCount; i++ ) {\n\t\t\t\tif ( ! inProgressOperations.has( id ) ) {\n\t\t\t\t\tthrow new Error( 'Operation cancelled' );\n\t\t\t\t}\n\n\t\t\t\tconst { image } = await decoder.decode( { frameIndex: i } );\n\t\t\t\tconst durationUs =\n\t\t\t\t\timage.duration ?? GIF_DEFAULT_FRAME_DURATION_US;\n\t\t\t\tconst durationSec = durationUs / 1_000_000;\n\n\t\t\t\tconst srcW = image.displayWidth;\n\t\t\t\tconst srcH = image.displayHeight;\n\n\t\t\t\t// Optionally downscale, then force even dimensions: the avc/vp9\n\t\t\t\t// encoders reject odd width/height (e.g. a 600x385 GIF). This\n\t\t\t\t// runs even when no downscaling is requested, so odd-sized GIFs\n\t\t\t\t// are not rejected outright.\n\t\t\t\tlet targetW = srcW;\n\t\t\t\tlet targetH = srcH;\n\t\t\t\tif (\n\t\t\t\t\tmaxDimensions &&\n\t\t\t\t\t( srcW > maxDimensions || srcH > maxDimensions )\n\t\t\t\t) {\n\t\t\t\t\tconst scale = Math.min(\n\t\t\t\t\t\tmaxDimensions / srcW,\n\t\t\t\t\t\tmaxDimensions / srcH\n\t\t\t\t\t);\n\t\t\t\t\ttargetW = Math.round( srcW * scale );\n\t\t\t\t\ttargetH = Math.round( srcH * scale );\n\t\t\t\t}\n\t\t\t\ttargetW = padToEven( targetW );\n\t\t\t\ttargetH = padToEven( targetH );\n\n\t\t\t\tlet frameForEncode: VideoFrame = image;\n\t\t\t\tif ( targetW !== srcW || targetH !== srcH ) {\n\t\t\t\t\tconst canvas = new OffscreenCanvas( targetW, targetH );\n\t\t\t\t\tconst ctx = canvas.getContext( '2d' );\n\t\t\t\t\tif ( ! ctx ) {\n\t\t\t\t\t\tthrow new Error( 'Failed to create 2D canvas context' );\n\t\t\t\t\t}\n\t\t\t\t\tctx.drawImage( image, 0, 0, targetW, targetH );\n\t\t\t\t\t// This replacement VideoFrame's timestamp is in\n\t\t\t\t\t// microseconds.\n\t\t\t\t\tframeForEncode = new VideoFrame( canvas, {\n\t\t\t\t\t\ttimestamp: Math.round( timestampSec * 1_000_000 ),\n\t\t\t\t\t\tduration: durationUs,\n\t\t\t\t\t} );\n\t\t\t\t\timage.close();\n\t\t\t\t}\n\n\t\t\t\tconst sample = new VideoSample( frameForEncode, {\n\t\t\t\t\ttimestamp: timestampSec,\n\t\t\t\t\tduration: durationSec,\n\t\t\t\t} );\n\t\t\t\ttry {\n\t\t\t\t\tawait source.add( sample );\n\t\t\t\t} finally {\n\t\t\t\t\t// Close both the sample wrapper and the underlying frame;\n\t\t\t\t\t// leaking either pressures memory across a long GIF.\n\t\t\t\t\tsample.close();\n\t\t\t\t\tframeForEncode.close();\n\t\t\t\t}\n\t\t\t\ttimestampSec += durationSec;\n\t\t\t}\n\n\t\t\tawait output.finalize();\n\n\t\t\tconst out = target.buffer;\n\t\t\tif ( ! out || out.byteLength === 0 ) {\n\t\t\t\tthrow new Error( 'Encoder produced empty output' );\n\t\t\t}\n\t\t\treturn out;\n\t\t} finally {\n\t\t\tdecoder.close();\n\t\t}\n\t} finally {\n\t\tinProgressOperations.delete( id );\n\t\treleaseLock();\n\t}\n}\n"],
  "mappings": "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",
  "names": []
}
