Skip to content

Instantly share code, notes, and snippets.

  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save davo/091a82a3d3193ad128c0b05ef5cdc9f2 to your computer and use it in GitHub Desktop.
Video iPhone Frame Overlay Web App Prototype
import React, { Component, useCallback, useState, useRef } from "react";
import "./App.css";
const imgUrl = require("./assets/iPhone-XS-Portrait-Space-Gray.png");
const fullSize = {
width: 1325,
height: 2616
};
const rect = {
x: 100,
y: 89,
width: 1125,
height: 2436
};
const frameTime = 1 / 60;
function downloadBlob(data, fileName, mimeType) {
const blob = new Blob([data], { type: mimeType });
const url = window.URL.createObjectURL(blob);
downloadURL(url, fileName);
setTimeout(() => {
return window.URL.revokeObjectURL(url);
}, 1000);
}
function downloadURL(data, fileName) {
const a = document.createElement("a");
a.href = data;
a.download = fileName;
a.style.display = "none";
document.body.appendChild(a);
a.click();
a.remove();
}
function encode(frames, callback) {
const worker = new Worker("ffmpeg-worker-mp4.js");
const imagesToRender = frames.map((frame, i) => ({
name: `frame_${i}.jpeg`,
data: frame
}));
worker.onmessage = function(e) {
const msg = e.data;
switch (msg.type) {
case "ready": {
worker.postMessage({
type: "run",
MEMFS: imagesToRender,
// TOTAL_MEMORY: 536870912,
arguments: [
"-framerate",
"60",
"-i",
`frame_%d.jpeg`,
"-c:v",
"libx264",
"-pix_fmt",
"yuv420p",
// "-vf",
// "scale=-2:1280",
"-analyzeduration",
"2147483647",
"-probesize",
"2147483647",
"output.mp4"
]
});
break;
}
case "stdout": {
console.log(msg.data);
break;
}
case "stderr": {
console.warn(msg.data);
break;
}
case "done": {
worker.terminate();
const { name, data } = msg.data.MEMFS[0];
callback(data, name);
break;
}
case "exit": {
console.log("Process exited with code " + msg.data);
break;
}
}
};
}
function render(
canvas,
ctx,
resizeCanvas,
resizeCtx,
img,
video,
callback,
frames = [],
currentTime = frameTime
) {
const videoDuration = video.duration;
ctx.clearRect(0, 0, fullSize.width, fullSize.height);
ctx.fillStyle = "#FFFFFF";
ctx.fillRect(0, 0, fullSize.width, fullSize.height);
ctx.drawImage(video, rect.x, rect.y, rect.width, rect.height);
ctx.drawImage(img, 0, 0, fullSize.width, fullSize.height);
resizeCtx.drawImage(canvas, 0, 0, resizeCanvas.width, resizeCanvas.height);
const dataUrl = resizeCanvas.toDataURL("image/jpeg");
const byteString = atob(dataUrl.replace(/^data:image\/jpeg;base64,/, ""));
const frameData = new Uint8Array(byteString.length);
for (let i = 0; i < byteString.length; i++) {
frameData[i] = byteString.charCodeAt(i);
}
frames.push(frameData);
if (currentTime >= videoDuration) {
return callback(frames);
}
video.addEventListener(
"canplay",
() => {
video.addEventListener(
"canplaythrough",
() => {
requestAnimationFrame(() => {
render(
canvas,
ctx,
resizeCanvas,
resizeCtx,
img,
video,
callback,
frames,
currentTime + frameTime
);
});
},
{ once: true }
);
video.pause();
},
{ once: true }
);
video.currentTime = currentTime;
video.play();
}
const App = () => {
const imageRef = useRef(null);
const videoRef = useRef(null);
const canvasRef = useRef(null);
const resizeCanvasRef = useRef(null);
const [videoUrl, setVideoUrl] = useState(null);
const handleVideoLoad = useCallback(() => {
const video = videoRef.current;
const canvas = canvasRef.current;
const resizeCanvas = resizeCanvasRef.current;
const img = imageRef.current;
if (img && video && canvas && resizeCanvas) {
const ctx = canvas.getContext("2d");
const resizeCtx = resizeCanvas.getContext("2d");
canvas.width = fullSize.width;
canvas.height = fullSize.height;
resizeCanvas.width = Math.floor(fullSize.width / 2);
resizeCanvas.height = Math.floor(fullSize.height / 2);
const startTime = window.performance.now();
render(canvas, ctx, resizeCanvas, resizeCtx, img, video, frames => {
encode(frames, (data, name) => {
downloadBlob(data, name, "video/mp4");
console.log(`Total Time: ${window.performance.now() - startTime}`);
});
});
}
});
const handleUpload = useCallback(e => {
const curFiles = e.target.files;
if (curFiles.length === 1) {
const file = curFiles[0];
setVideoUrl(URL.createObjectURL(file));
if (videoRef.current) {
videoRef.current.load();
}
}
});
const dimensionAdjust = 500 / fullSize.height;
const width = fullSize.width * dimensionAdjust;
const height = fullSize.height * dimensionAdjust;
return (
<div
style={{
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center"
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
marginBottom: "1rem"
}}
>
<label style={{ display: "block", marginBottom: "1rem" }}>
Upload Video
</label>
<input
style={{ display: "block" }}
type="file"
accept="video/*"
onChange={handleUpload}
/>
</div>
<img ref={imageRef} style={{ width: 0, height: 0 }} src={imgUrl} />
<canvas style={{ width: 0, height: 0 }} ref={resizeCanvasRef} />
<canvas style={{ width, height, marginBottom: "1rem" }} ref={canvasRef} />
<video
style={{ width: 0, height: 0 }}
ref={videoRef}
muted
onLoadedData={handleVideoLoad}
>
<source src={videoUrl} />
</video>
</div>
);
};
export default App;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment