very hacky mp4 export
This commit is contained in:
parent
a9af6028c0
commit
87a3e0abc3
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
367
src/main.js
367
src/main.js
|
|
@ -4732,7 +4732,374 @@ async function about() {
|
|||
);
|
||||
}
|
||||
|
||||
// Export stuff that's all crammed in here and needs refactored
|
||||
function createProgressModal() {
|
||||
// Check if the modal already exists
|
||||
const existingModal = document.getElementById('progressModal');
|
||||
if (existingModal) {
|
||||
existingModal.style.display = 'flex';
|
||||
return; // If the modal already exists, do nothing
|
||||
}
|
||||
|
||||
// Create modal container with a unique ID
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'progressModal'; // Give the modal a unique ID
|
||||
modal.style.position = 'fixed';
|
||||
modal.style.top = '0';
|
||||
modal.style.left = '0';
|
||||
modal.style.width = '100%';
|
||||
modal.style.height = '100%';
|
||||
modal.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
|
||||
modal.style.display = 'flex';
|
||||
modal.style.justifyContent = 'center';
|
||||
modal.style.alignItems = 'center';
|
||||
modal.style.zIndex = '9999';
|
||||
|
||||
// Create inner modal box
|
||||
const modalContent = document.createElement('div');
|
||||
modalContent.style.backgroundColor = backgroundColor;
|
||||
modalContent.style.padding = '20px';
|
||||
modalContent.style.borderRadius = '8px';
|
||||
modalContent.style.textAlign = 'center';
|
||||
modalContent.style.minWidth = '300px';
|
||||
|
||||
// Create progress bar
|
||||
const progressBar = document.createElement('progress');
|
||||
progressBar.id = 'progressBar';
|
||||
progressBar.value = 0;
|
||||
progressBar.max = 100;
|
||||
progressBar.style.width = '100%';
|
||||
|
||||
// Create text to show the current frame info
|
||||
const progressText = document.createElement('p');
|
||||
progressText.id = 'progressText';
|
||||
progressText.innerText = 'Rendering frame 0 of 0';
|
||||
|
||||
// Append elements to modalContent
|
||||
modalContent.appendChild(progressBar);
|
||||
modalContent.appendChild(progressText);
|
||||
|
||||
// Append modalContent to modal
|
||||
modal.appendChild(modalContent);
|
||||
|
||||
// Append modal to body
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
function done(output) {
|
||||
const url = URL.createObjectURL(output);
|
||||
downloadObjectURL(url, "test.mp4")
|
||||
const modal = document.getElementById('progressModal');
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
|
||||
async function exportMp4() {
|
||||
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);
|
||||
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);
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
// exportMp4()
|
||||
|
||||
async function render() {
|
||||
exportMp4()
|
||||
return
|
||||
document.querySelector("body").style.cursor = "wait";
|
||||
const path = await saveFileDialog({
|
||||
filters: [
|
||||
|
|
|
|||
Loading…
Reference in New Issue