You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, durationStr = "5") {
// Parse duration input or fallback to default
const duration = parseFloat(durationStr) || 5;
const fps = 30;
// Limit maximum dimensions for performance and encoder compatibility (e.g., 1080p limit)
let targetW = originalImg.width;
let targetH = originalImg.height;
const maxDim = 1920;
if (targetW > maxDim || targetH > maxDim) {
const ratio = Math.min(maxDim / targetW, maxDim / targetH);
targetW = Math.round(targetW * ratio);
targetH = Math.round(targetH * ratio);
}
// Video encoders generally require width and height to be even numbers
const encWidth = Math.floor(targetW / 2) * 2;
const encHeight = Math.floor(targetH / 2) * 2;
const canvas = document.createElement('canvas');
canvas.width = encWidth;
canvas.height = encHeight;
const ctx = canvas.getContext('2d');
// Base layer layout (fill background with black to handle PNG transparency)
function paintCanvas() {
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, encWidth, encHeight);
ctx.drawImage(originalImg, 0, 0, encWidth, encHeight);
}
paintCanvas();
// Helper UI function to display output visually
function createOutputElement(blob, ext) {
const container = document.createElement('div');
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.alignItems = 'center';
container.style.gap = '16px';
container.style.padding = '16px';
container.style.boxSizing = 'border-box';
container.style.fontFamily = 'sans-serif';
const video = document.createElement('video');
video.src = URL.createObjectURL(blob);
video.controls = true;
video.autoplay = true;
video.loop = true;
video.style.maxWidth = '100%';
video.style.borderRadius = '8px';
video.style.boxShadow = '0 4px 6px rgba(0,0,0,0.1)';
video.style.backgroundColor = '#000';
const downloadBtn = document.createElement('a');
downloadBtn.href = video.src;
downloadBtn.download = `converted_video.${ext}`;
downloadBtn.textContent = `Download Video (${ext.toUpperCase()})`;
downloadBtn.style.padding = '10px 20px';
downloadBtn.style.backgroundColor = '#007bff';
downloadBtn.style.color = '#fff';
downloadBtn.style.textDecoration = 'none';
downloadBtn.style.borderRadius = '4px';
downloadBtn.style.fontWeight = 'bold';
downloadBtn.style.transition = 'background-color 0.2s';
downloadBtn.onmouseover = () => downloadBtn.style.backgroundColor = '#0056b3';
downloadBtn.onmouseout = () => downloadBtn.style.backgroundColor = '#007bff';
container.appendChild(video);
container.appendChild(downloadBtn);
return container;
}
// Fallback approach using standard MediaRecorder for real-time capture
async function fallbackMediaRecorder() {
return new Promise((resolve) => {
const stream = canvas.captureStream(fps);
let mimeType = '';
const supportedTypes = [
'video/mp4',
'video/webm;codecs=h264',
'video/webm;codecs=vp9',
'video/webm;codecs=vp8',
'video/webm'
];
for (const type of supportedTypes) {
if (MediaRecorder.isTypeSupported(type)) {
mimeType = type;
break;
}
}
const recorder = new MediaRecorder(stream, { mimeType: mimeType || undefined });
const chunks = [];
recorder.ondataavailable = e => {
if (e.data.size > 0) chunks.push(e.data);
};
let finalized = false;
recorder.onstop = () => {
if (finalized) return;
finalized = true;
const ext = (mimeType && mimeType.includes('mp4')) ? 'mp4' : 'webm';
const blob = new Blob(chunks, { type: mimeType || 'video/webm' });
resolve(createOutputElement(blob, ext));
};
recorder.start();
const totalTimeMs = duration * 1000;
let start = null;
function drawFrame(now) {
if (!start) start = now;
paintCanvas();
// Extremely subtle sub-pixel change to force canvas update stream detection
ctx.fillStyle = `rgba(0,0,0,${Math.random() * 0.01})`;
ctx.fillRect(0, 0, 1, 1);
if (now - start < totalTimeMs) {
requestAnimationFrame(drawFrame);
} else {
recorder.stop();
}
}
requestAnimationFrame(drawFrame);
});
}
// Try natively muxing utilizing hardware WebCodecs for exact, genuine .mp4 output files
try {
if (!window.VideoEncoder) {
throw new Error("WebCodecs API not supported by browser environment.");
}
const muxerUrl = 'https://unpkg.com/mp4-muxer/build/mp4-muxer.mjs';
const Mp4Muxer = await import(muxerUrl);
const muxer = new Mp4Muxer.Muxer({
target: new Mp4Muxer.ArrayBufferTarget(),
video: {
codec: 'avc',
width: encWidth,
height: encHeight
},
fastStart: 'in-memory',
});
const videoEncoder = new window.VideoEncoder({
output: (chunk, meta) => muxer.addVideoChunk(chunk, meta),
error: e => { throw e; }
});
const encoderConfig = {
codec: 'avc1.42001f', // Baseline profile generic fallback configuration
width: encWidth,
height: encHeight,
bitrate: 2_000_000,
framerate: fps,
};
const support = await window.VideoEncoder.isConfigSupported(encoderConfig);
if (!support.supported) {
throw new Error("Target VideoEncoder configuration is restricted.");
}
videoEncoder.configure(encoderConfig);
const totalFrames = Math.floor(duration * fps);
for (let i = 0; i < totalFrames; i++) {
const frame = new window.VideoFrame(canvas, {
timestamp: (i * 1000000) / fps
});
const insertKeyFrame = (i % (fps * 2) === 0);
videoEncoder.encode(frame, { keyFrame: insertKeyFrame });
frame.close();
// Allow asynchronous dispatching overhead block loop iteration to bypass browser stalling
if (i % 5 === 0) await new Promise(r => setTimeout(r, 0));
}
await videoEncoder.flush();
muxer.finalize();
const buffer = muxer.target.buffer;
const blob = new Blob([buffer], { type: 'video/mp4' });
return createOutputElement(blob, 'mp4');
} catch (e) {
console.warn("Falling back to MediaRecorder setup because genuine WebCodecs mp4 encoding collapsed:", e.message);
return await fallbackMediaRecorder();
}
}
Apply Changes