diff --git a/src/main.js b/src/main.js index ed6eee7..745e9b1 100644 --- a/src/main.js +++ b/src/main.js @@ -4846,320 +4846,6 @@ function createProgressModal() { } -// https://semisignal.com/tag/ffmpeg-js/ -function convertDataURIToBinary(dataURI) { - const base64 = dataURI.replace(/^data[^,]+,/,''); - const raw = window.atob(base64); - const rawLength = raw.length; - - const array = new Uint8Array(new ArrayBuffer(rawLength)); - for (let i = 0; i < rawLength; i++) { - array[i] = raw.charCodeAt(i); - } - return array; -}; - -//**blob to dataURL** -function blobToDataURL(blob, callback) { - const a = new FileReader(); - a.onload = function(e) {callback(e.target.result);} - a.readAsDataURL(blob); -} - -function pad(n, width, z) { - z = z || '0'; - n = n + ''; - return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n; -} - -function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -function downloadObjectURL(url, filename) { - const a = document.createElement('a'); - a.href = url; - a.download = filename; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); -} - -async function done(output, path) { - await writeFile( - path, // The destination file path for saving - new Uint8Array(await output.arrayBuffer()), - ); - // const url = URL.createObjectURL(output); - // downloadObjectURL(url, "test.mp4") - const modal = document.getElementById('progressModal'); - modal.style.display = 'none'; - document.querySelector("body").style.cursor = "default"; -} - -async function exportMp4(path) { - const worker = new Worker("/ffmpeg-worker-mp4.js") - // const worker = new Worker("/ffmpeg-worker-webm.js") - - const canvas = document.createElement("canvas"); - canvas.width = config.fileWidth; // Set desired width - canvas.height = config.fileHeight; // Set desired height - const ctx = canvas.getContext("2d") - - const images = [] - const videoChunks = [] - // const chunkSize = 10; - const chunkSize = root.maxFrame+1; - let currentFrame = 0; - - createProgressModal(); - - function processChunk(worker, chunkStart, chunkEnd) { - - const chunkFrames = []; - - // Prepare the frames for the current chunk - // for (let i = chunkStart; i < chunkEnd; i++) { - function processFrame(i) { - - // Update progress bar - const progressText = document.getElementById('progressText'); - progressText.innerText = `Rendering frame ${i + 1} of ${root.maxFrame}`; - const progressBar = document.getElementById('progressBar'); - const progress = Math.round(((i + 1) / root.maxFrame) * 100); - progressBar.value = progress; - - ctx.resetTransform(); - ctx.beginPath(); - ctx.fillStyle = "white"; - ctx.fillRect(0, 0, config.fileWidth, config.fileHeight); - root.setFrameNum(i); - root.draw(ctx); - const img = new Image() - const mimeType = 'image/jpeg' - const imgString = canvas.toDataURL(mimeType,1) - const data = convertDataURIToBinary( imgString ) - chunkFrames.push({ - name: `img${ pad( chunkFrames.length, 3 ) }.jpeg`, - data - }) - img.src = imgString - if (i+1 < chunkEnd) { - setTimeout(() => processFrame(i+1), 4) - } else { - // Post the chunk to the worker - setTimeout(() => worker.postMessage({ - type: 'run', - // TOTAL_MEMORY: 268435456, - TOTAL_MEMORY: 1073741824, - arguments: [ - '-r', config.framerate.toString(), - '-i', 'img%03d.jpeg', - '-c:v', 'libx264', - '-crf', '23', - '-vf', `scale=${parseInt(config.fileWidth/8)*2}:${parseInt(config.fileHeight/8)*2}`, - // '-vf', `scale=${config.fileWidth}:${config.fileHeight}`, - '-pix_fmt', 'yuv420p', - '-vb', '20M', - 'out.mp4' - ], - // arguments: [ - // '-r', '20', - // '-i', 'img%03d.jpeg', - // 'out.webm' - // ], - MEMFS: chunkFrames // Supply the chunk frames - }), 1000); - - currentFrame += chunkSize - } - } - - processFrame(chunkStart) - - - - worker.onmessage = function(e) { - const msg = e.data; - - if (msg.type === 'done') { - // Add chunk to the videoChunks array - videoChunks.push(msg.data.MEMFS[0].data) - - // Check if we need to process the next chunk or finalize - if (currentFrame < root.maxFrame) { - const nextChunkStart = currentFrame; - const nextChunkEnd = Math.min(currentFrame + chunkSize, root.maxFrame); - - // Process next chunk after the current one finishes - processChunk(worker, nextChunkStart, nextChunkEnd); // Recurse with updated chunk indices - } else { - // If all chunks are processed, call finalization - const progressText = document.getElementById('progressText'); - progressText.innerText = 'Finalizing...'; - const progressBar = document.getElementById('progressBar'); - progressBar.value = 100; - setTimeout(() => concatenateChunks(videoChunks), 1000); // Finalize once all chunks are processed - } - } else if (msg.type === "stdout") { - console.error(msg.data); - } else if (msg.type === "stderr") { - console.log(msg.data); - } - }; - } - - // worker.onmessage = function(e) { - // const msg = e.data; - - // switch (msg.type) { - // // Handle stdout and stderr from FFmpeg - // case "stdout": - // console.error(msg.data); - // break; - // case "stderr": - // console.log(msg.data); - // break; - // case "exit": - // console.log("Process exited with code " + msg.data); - // break; - - // // Handle completion of each chunk - // case 'done': - // // Save the chunk's video data as a blob - // const chunkBlob = new Blob([msg.data.MEMFS[0].data], { - // type: "video/mp4" - // }); - // videoChunks.push(chunkBlob); // Add chunk to the videoChunks array - - // // If we've processed all chunks, concatenate them - // if (currentFrame >= root.maxFrame) { - // concatenateChunks(videoChunks); - // } - // break; - // } - // }; - - // Concatenate all video chunks into one MP4 file - async function concatenateChunks(chunks) { - if (chunks.length==1) { - // No need to concatenate, send directly to done - const finalBlob = new Blob([chunks[0]], { - type: "video/mp4" - }); - // Trigger the done callback with the final video blob - done(finalBlob, path); - return; - } - - const chunkNames = chunks.map((_, index) => `chunk${index + 1}.mp4`); - - // Create a file list in MEMFS - const memfsChunks = chunks.map((chunk, index) => ({ - name: chunkNames[index], - data: chunk - })); - - const concatList = chunkNames.map((chunkName) => `file '${chunkName}'`).join('\n'); - // Create a file in MEMFS for the concat list - const concatListFile = { - name: 'concat_list.txt', - data: new TextEncoder().encode(concatList) - }; - - // Add the concat list to the MEMFS files - memfsChunks.push(concatListFile); - - console.log(chunkNames) - console.log(concatList) - // Prepare FFmpeg command to concatenate video chunks - worker.postMessage({ - type: 'run', - TOTAL_MEMORY: 268435456, - arguments: [ - '-f', 'concat', - '-safe', '0', // Allow using file paths - '-i', 'concat_list.txt', // Use the concat list file - // '-c:v', 'copy', - '-c:v', 'libx264', - '-crf', '23', - '-pix_fmt', 'yuv420p', - 'final_video.mp4' - ], - // arguments: [ - // '-f', 'concat', - // '-safe', '0', // Allow using file paths - // '-i', 'concat_list.txt', - // 'final_video.webm' - // ], - MEMFS: memfsChunks // Provide the chunks and concat list as input to FFmpeg - }); - - // Listen for the final output - worker.onmessage = function(e) { - const msg = e.data; - - switch (msg.type) { - case 'done': - console.log('done') - // Combine the blobs into the final video file - const finalBlob = new Blob([msg.data.MEMFS[0].data], { - type: "video/mp4" - }); - // Trigger the done callback with the final video blob - done(finalBlob, path); - break; - case 'stderr': - console.log(msg.data); - break; - case 'stdout': - console.error(msg.data); - break; - case 'exit': - console.log('FFmpeg worker exit code:', msg.data); - break; - } - }; - } - - processChunk(worker, 0, Math.min(chunkSize, root.maxFrame)) - - function finalize() { - - worker.onmessage = function(e) { - var msg = e.data; - switch (msg.type) { - // Ffmpeg seems to have stdout and stderr swapped - case "stdout": - console.error(msg.data); - break; - case "stderr": - console.log(msg.data); - break; - case "exit": - console.log("Process exited with code " + msg.data); - break; - - case 'done': - const blob = new Blob([msg.data.MEMFS[0].data], { - type: "video/mp4" - }); - done( blob ) - - break; - } - }; - - worker.postMessage({ - type: 'run', - TOTAL_MEMORY: 268435456, - arguments: ["-r", "20", "-i", "img%03d.jpeg", "-c:v", "libx264", "-crf", "1", "-vf", `scale=1000:1000`, "-pix_fmt", "yuv420p", "-vb", "20M", "out.mp4"], - MEMFS: images - }); - } -} - async function setupVideoExport(ext, path, canvas, exportContext) { createProgressModal();