Public
Edited
Sep 28, 2023
2 forks
Insert cell
Insert cell
viewof videoFile = html`<input type="file" accept=".mp4,.mov,.webm" />`
Insert cell
viewof inputFPS = html`<input type=number value=15 />`
Insert cell
maxFrameCountGuess = Math.round(duration * inputFPS)
Insert cell
outputWidth = Math.floor(options.outputScale * videoWidth)
Insert cell
outputHeight = Math.floor(options.outputScale * videoHeight)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
previewImage = html`<div>${previewScrub}</div><img src="${frames[previewScrub].data}">`
Insert cell
Insert cell
videoEl = {
const videoEl = html`<video src="${URL.createObjectURL(
videoFile
)}" controls style="max-width: 100%;" />`;

videoEl.addEventListener('loadedmetadata', function(event) {
mutable videoWidth = videoEl.videoWidth;
mutable videoHeight = videoEl.videoHeight;
});

videoEl.addEventListener('durationchange', function(event) {
mutable duration = videoEl.duration;
});

function setCurrentTime() {
mutable currentTime = videoEl.currentTime;
}
videoEl.addEventListener('seeked', setCurrentTime);
videoEl.addEventListener('timeupdate', setCurrentTime);

return videoEl;
}
Insert cell
mutable videoWidth = 0
Insert cell
mutable videoHeight = 0
Insert cell
mutable duration = 0
Insert cell
mutable currentTime = 0
Insert cell
mutable busy = false
Insert cell
mutable progress = 0
Insert cell
outputFrameRate = Math.max(1, Math.floor(options.numFrames / duration))
Insert cell
outputFrames = options.stopAtEnd ? 5000 : options.numFrames
Insert cell
async function seekTo(videoEl, time) {
return new Promise((resolve, reject) => {
const handleSeeked = function() {
videoEl.removeEventListener('seeked', handleSeeked);
resolve(videoEl.currentTime);
};
videoEl.addEventListener('seeked', handleSeeked);
videoEl.currentTime = time;
});
}
Insert cell
async function extractFrames({
videoEl,
clipStart,
clipEnd,
numFrames,
quality = 0.8,
outputScale = 1
}) {
// if (busy) {
// throw new Error('Already encoding, wait a moment...');
// }
// mutable busy = true;
videoEl.pause();

const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
const outputWidth = outputScale * videoEl.videoWidth;
const outputHeight = outputScale * videoEl.videoHeight;
canvas.width = outputWidth;
canvas.height = outputHeight;

const frames = [];

for (let i = 0; i < numFrames; i++) {
const duration = clipEnd - clipStart;
if (duration <= 0) {
throw new Error('clipEnd is before clipStart');
}
const time = i * (duration / (Math.max(2, numFrames) - 1)) + clipStart;
// In threory, waits until seeked time is in view
const seekedTime = await seekTo(videoEl, time);
context.drawImage(videoEl, 0, 0, outputWidth, outputHeight);
frames.push({
time,
seekedTime,
data: canvas.toDataURL('image/jpeg', quality)
});
mutable progress = (i + 1) / numFrames;
}

return frames;
}
Insert cell
frames = extractFrames({
videoEl,
...options
})
Insert cell
preview = {
const previewImages = frames.map(
({ data }) => html`<img src="${data}" style="max-width: 128px">`
);
return html`<div>${previewImages}</div>`;
}
Insert cell
lottieJson = frames.reduce(
function(lottie, frame, index) {
const id = "fr_" + index;
const w = outputWidth;
const w2 = Math.floor(w / 2);
const h = outputHeight;
const h2 = Math.floor(h / 2);
const op = (index + 1) === options.numFrames ? outputFrames : (index + 1);

lottie.assets.push({
id,
w,
h,
u: "",
p: frame.data,
e: 1
});

lottie.layers.push({
ddd: 0,
ind: index + 1,
ty: 2,
nm: id + ".jpg",
cl: "jpg",
refId: id,
sr: 1,
ks: {
o: { a: 0, k: 100, ix: 11 },
r: { a: 0, k: 0, ix: 10 },
p: { a: 0, k: [w2, h2, 0], ix: 2 },
a: { a: 0, k: [w2, h2, 0], ix: 1 },
s: { a: 0, k: [100, 100, 100], ix: 6 }
},
ao: 0,
ip: index,
op: op,
st: index,
bm: 0
});

return lottie;
},
{
v: "5.5.2",
fr: outputFrameRate,
ip: 0,
op: outputFrames,
w: outputWidth,
h: outputHeight,
nm: "@mbieniek-ws/video-to-lottie",
ddd: 0,
assets: [],
layers: [],
markers: []
}
)
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more