Skip to content

Commit

Permalink
always read full file into memory
Browse files Browse the repository at this point in the history
  • Loading branch information
lyonbot committed Apr 22, 2024
1 parent f443635 commit d8d90c7
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 54 deletions.
19 changes: 14 additions & 5 deletions src/components/Processing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,8 @@ export function ProcessingBar() {

async function grabFramesAndCombine() {
const { sourceStart, sourceEnd, frameCount: estimatedFrameCount } = outputTimeRange()
const { file } = store
if (!file) throw new Error('no file')
const { file, fileContent } = store
if (!file || !fileContent) throw new Error('no file')

setProgress('Allocating RAM')

Expand All @@ -181,8 +181,12 @@ export function ProcessingBar() {

const progressSyncTimer = setInterval(() => { setPercentage(frameCount / estimatedFrameCount * 100) }, 150)
try {

await grabFrames1({
file,
fileContent,
fileExtname: store.fileInfo.extname,
fileURL: store.fileInfo.url,

resizeWidth: outputSize().width,
resizeHeight: outputSize().height,
frameTimestamps: Array.from({ length: estimatedFrameCount }, (_, i) => sourceStart + i * (sourceEnd - sourceStart) / estimatedFrameCount),
Expand Down Expand Up @@ -211,6 +215,11 @@ export function ProcessingBar() {
}

function startProcess() {
if (outputSize().width <= 0 || outputSize().height <= 0) {
alert('Invalid output size');
return;
}

setIsRunning(true)
setErrorMessage('')
console.time('process')
Expand Down Expand Up @@ -270,8 +279,8 @@ export function ProcessingBar() {
fix={() => updateStore('options', 'height', 600)}
/>
<WarningMsg
when={outputTimeRange().frameCount > 200}
message="too many frames yield large GIF. please trim the video, or lowing framerate."
when={outputTimeRange().frameCount > 250}
message={`frame count ${outputTimeRange().frameCount} can be decreased with framerate, trimming and speed-up`}
/>

<Show when={errorMessage()}>
Expand Down
89 changes: 43 additions & 46 deletions src/processors/frameGrabber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import { reportError } from '../report';
import type { FFmpeg } from '@ffmpeg/ffmpeg';

interface GrabFrameOptions {
file: File;
fileContent: Uint8Array;
fileURL: string;
fileExtname: string;

resizeWidth: number;
resizeHeight: number;
frameTimestamps: number[]; // in sec
Expand All @@ -18,7 +21,7 @@ interface GrabbedFrame {
time: number;
}

export async function grabFramesWithMP4Box({ file, resizeWidth, resizeHeight, frameTimestamps, reportProgress }: GrabFrameOptions) {
export async function grabFramesWithMP4Box({ fileContent, resizeWidth, resizeHeight, frameTimestamps, reportProgress }: GrabFrameOptions) {
const startTS = frameTimestamps[0] * 1000000
const endTS = frameTimestamps[frameTimestamps.length - 1] * 1000000
const outputFrames = [] as GrabbedFrame[]
Expand All @@ -42,11 +45,10 @@ export async function grabFramesWithMP4Box({ file, resizeWidth, resizeHeight, fr

const mp4InfoPromise = new Promise<any>((resolve, reject) => {
mp4boxInputFile.onReady = resolve
file.arrayBuffer().then((b) => {
const buffer = b as MP4ArrayBuffer
buffer.fileStart = 0
mp4boxInputFile.appendBuffer(buffer)
}).catch(reject)

const buffer = fileContent.buffer.slice(0) as MP4ArrayBuffer
buffer.fileStart = 0
mp4boxInputFile.appendBuffer(buffer)
});

// reference: https://github.com/vjeux/mp4-h264-re-encode/tree/main
Expand Down Expand Up @@ -180,49 +182,44 @@ export async function grabFramesWithMP4Box({ file, resizeWidth, resizeHeight, fr
return outputFrames
}

export async function grabFramesWithVideoTag({ file, resizeWidth, resizeHeight, frameTimestamps, reportProgress }: GrabFrameOptions) {
const fileURL = URL.createObjectURL(file)
export async function grabFramesWithVideoTag({ fileURL, resizeWidth, resizeHeight, frameTimestamps, reportProgress }: GrabFrameOptions) {
const start = frameTimestamps[0]
const outputFrames = [] as GrabbedFrame[]

try {
const video = document.createElement('video')
const videoReady = new Promise(r => video.onloadedmetadata = r)
video.muted = true
video.src = fileURL

await videoReady
const videoPlaying = new Promise(r => video.onplaying = r)
video.currentTime = start
video.play()
await videoPlaying
video.pause()
video.currentTime = start
video.onplaying = null

// grab frames

const waitForFrameReady = () => new Promise(r => video.onseeked = r)
for (let i = 0; i < frameTimestamps.length; i++) {
const frameTime = frameTimestamps[i]
const seekEnd = waitForFrameReady()
video.currentTime = frameTime
await seekEnd

outputFrames.push({
image: await createImageBitmap(video, { resizeWidth, resizeHeight }),
time: frameTime
})
if (!reportProgress(i + 1, outputFrames.at(-1)!)) break
}

return outputFrames
} finally {
URL.revokeObjectURL(fileURL)
const video = document.createElement('video')
const videoReady = new Promise(r => video.onloadedmetadata = r)
video.muted = true
video.src = fileURL

await videoReady
const videoPlaying = new Promise(r => video.onplaying = r)
video.currentTime = start
video.play()
await videoPlaying
video.pause()
video.currentTime = start
video.onplaying = null

// grab frames

const waitForFrameReady = () => new Promise(r => video.onseeked = r)
for (let i = 0; i < frameTimestamps.length; i++) {
const frameTime = frameTimestamps[i]
const seekEnd = waitForFrameReady()
video.currentTime = frameTime
await seekEnd

outputFrames.push({
image: await createImageBitmap(video, { resizeWidth, resizeHeight }),
time: frameTime
})
if (!reportProgress(i + 1, outputFrames.at(-1)!)) break
}

return outputFrames
}

export async function grabFramesWithFFMpeg({ file, resizeWidth, resizeHeight, frameTimestamps, reportProgress, ffmpeg }: GrabFrameOptions) {
export async function grabFramesWithFFMpeg({ fileContent, fileExtname, resizeWidth, resizeHeight, frameTimestamps, reportProgress, ffmpeg }: GrabFrameOptions) {
if (!ffmpeg) throw new Error('FFMpeg not loaded');

const start = frameTimestamps[0]
Expand All @@ -231,8 +228,8 @@ export async function grabFramesWithFFMpeg({ file, resizeWidth, resizeHeight, fr

// const mountPoint = "/mounted/"
// await ffmpeg.mount('WORKERFS' as any, { files: [file] }, mountPoint) // see https://emscripten.org/docs/api_reference/Filesystem-API.html#workerfs
const inputFileName = 'input__' + file.name
await ffmpeg.writeFile(inputFileName, new Uint8Array(await file.arrayBuffer()))
const inputFileName = `input1.${fileExtname}`
await ffmpeg.writeFile(inputFileName, fileContent.slice())

const abortController = new AbortController()
let aborted = false
Expand Down
23 changes: 20 additions & 3 deletions src/store.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ export const getDefaultWatermark = (): WatermarkConfig => ({ ...defaultWatermark

export const [store, updateStore] = createStore({
file: null as null | File,
fileContent: null as null | Uint8Array,
fileInfo: {
name: '',
extname: '', // without dot
url: '',
width: 0,
height: 0,
Expand Down Expand Up @@ -107,14 +110,24 @@ export const outputTimeRange = createRoot(() => {

createRoot(() => {
createEffect(() => {
updateStore('fileInfo', { url: '' })

const file = store.file;
if (!file) return

updateStore('fileInfo', {
name: file.name,
extname: file.name.split('.').pop() ?? '',
})

file.arrayBuffer().then(data => {
if (file !== store.file) return
updateStore('fileContent', new Uint8Array(data))
})

const url = URL.createObjectURL(file);
const video = document.createElement('video');
video.onloadedmetadata = () => {
if (file !== store.file) return;

updateStore('fileInfo', {
url,
width: video.videoWidth,
Expand All @@ -126,7 +139,11 @@ createRoot(() => {
video.src = url;
video.muted = true;

onCleanup(() => { URL.revokeObjectURL(url) })
onCleanup(() => {
URL.revokeObjectURL(url)
updateStore('fileInfo', { url: '' });
updateStore('fileContent', null);
})
})

createEffect(() => {
Expand Down

0 comments on commit d8d90c7

Please sign in to comment.