UNPKG

remotion

Version:

Make videos programmatically

146 lines (145 loc) 5.16 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.decodeImage = void 0; const CACHE_SIZE = 5; const getActualTime = ({ loopBehavior, durationFound, timeInSec, }) => { return loopBehavior === 'loop' ? durationFound ? timeInSec % durationFound : timeInSec : Math.min(timeInSec, durationFound || Infinity); }; const decodeImage = async ({ resolvedSrc, signal, currentTime, initialLoopBehavior, }) => { if (typeof ImageDecoder === 'undefined') { throw new Error('Your browser does not support the WebCodecs ImageDecoder API.'); } const res = await fetch(resolvedSrc, { signal }); const { body } = res; if (!body) { throw new Error('Got no body'); } const decoder = new ImageDecoder({ data: body, type: res.headers.get('Content-Type') || 'image/gif', }); await decoder.completed; const { selectedTrack } = decoder.tracks; if (!selectedTrack) { throw new Error('No selected track'); } const cache = []; let durationFound = null; const getFrameByIndex = async (frameIndex) => { const foundInCache = cache.find((c) => c.frameIndex === frameIndex); if (foundInCache && foundInCache.frame) { return foundInCache; } const frame = await decoder.decode({ frameIndex, completeFramesOnly: true, }); if (foundInCache) { foundInCache.frame = frame.image; } else { cache.push({ frame: frame.image, frameIndex, timeInSeconds: frame.image.timestamp / 1000000, }); } return { frame: frame.image, frameIndex, timeInSeconds: frame.image.timestamp / 1000000, }; }; const clearCache = (closeToTimeInSec) => { const itemsInCache = cache.filter((c) => c.frame); const sortByClosestToCurrentTime = itemsInCache.sort((a, b) => { const aDiff = Math.abs(a.timeInSeconds - closeToTimeInSec); const bDiff = Math.abs(b.timeInSeconds - closeToTimeInSec); return aDiff - bDiff; }); for (let i = 0; i < sortByClosestToCurrentTime.length; i++) { if (i < CACHE_SIZE) { continue; } const item = sortByClosestToCurrentTime[i]; item.frame = null; } }; const ensureFrameBeforeAndAfter = async ({ timeInSec, loopBehavior, }) => { const actualTimeInSec = getActualTime({ durationFound, loopBehavior, timeInSec, }); const framesBefore = cache.filter((c) => c.timeInSeconds <= actualTimeInSec); const biggestIndex = framesBefore .map((c) => c.frameIndex) .reduce((a, b) => Math.max(a, b), 0); let i = biggestIndex; while (true) { const f = await getFrameByIndex(i); i++; if (!f.frame) { throw new Error('No frame found'); } if (!f.frame.duration) { // non-animated, or AVIF in firefox break; } if (i === selectedTrack.frameCount && durationFound === null) { const duration = (f.frame.timestamp + f.frame.duration) / 1000000; durationFound = duration; } if (f.timeInSeconds > actualTimeInSec || i === selectedTrack.frameCount) { break; } } // If close to end, also cache first frame for smooth wrap around if (selectedTrack.frameCount - biggestIndex < 3 && loopBehavior === 'loop') { await getFrameByIndex(0); } clearCache(actualTimeInSec); }; // Twice because might be over total duration await ensureFrameBeforeAndAfter({ timeInSec: currentTime, loopBehavior: initialLoopBehavior, }); await ensureFrameBeforeAndAfter({ timeInSec: currentTime, loopBehavior: initialLoopBehavior, }); const getFrame = async (timeInSec, loopBehavior) => { if (durationFound !== null && timeInSec > durationFound && loopBehavior === 'clear-after-finish') { return null; } const actualTimeInSec = getActualTime({ loopBehavior, durationFound, timeInSec, }); await ensureFrameBeforeAndAfter({ timeInSec: actualTimeInSec, loopBehavior }); const itemsInCache = cache.filter((c) => c.frame); const closest = itemsInCache.reduce((a, b) => { const aDiff = Math.abs(a.timeInSeconds - actualTimeInSec); const bDiff = Math.abs(b.timeInSeconds - actualTimeInSec); return aDiff < bDiff ? a : b; }); if (!closest.frame) { throw new Error('No frame found'); } return closest; }; return { getFrame, frameCount: selectedTrack.frameCount, }; }; exports.decodeImage = decodeImage;