Skip to content

Commit abef80a

Browse files
committed
[WIP] video.js
1 parent a004002 commit abef80a

File tree

1 file changed

+128
-0
lines changed

1 file changed

+128
-0
lines changed

src/utils/video.js

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { RawImage } from "./image.js";
2+
3+
export class RawVideoFrame {
4+
5+
/**
6+
* @param {RawImage} image
7+
* @param {number} timestamp
8+
*/
9+
constructor(image, timestamp) {
10+
this.image = image;
11+
this.timestamp = timestamp;
12+
}
13+
}
14+
15+
export class RawVideo {
16+
/**
17+
* @param {RawVideoFrame[]|RawImage[]} frames
18+
* @param {number} duration
19+
*/
20+
constructor(frames, duration) {
21+
if (frames.length > 0 && frames[0] instanceof RawImage) {
22+
// Assume uniform timestamps
23+
frames = frames.map((image, i) => new RawVideoFrame(image, (i + 1) / (frames.length + 1) * duration));
24+
}
25+
this.frames = /** @type {RawVideoFrame[]} */ (frames);
26+
this.duration = duration;
27+
}
28+
29+
get width() {
30+
return this.frames[0].image.width;
31+
}
32+
get height() {
33+
return this.frames[0].image.height;
34+
}
35+
36+
get fps() {
37+
return this.frames.length / this.duration;
38+
}
39+
}
40+
41+
42+
/**
43+
* Loads a video.
44+
*
45+
* @param {string|Blob|HTMLVideoElement} url The video to process.
46+
* @param {Object} [options] Optional parameters.
47+
* @param {number} [options.num_frames=null] The number of frames to sample uniformly.
48+
* If provided, the video is seeked to the desired positions rather than processing every frame.
49+
* @param {number} [options.fps=null] The number of frames to sample per second.
50+
* If provided (and num_frames is null), the video is seeked at fixed time intervals.
51+
*
52+
* @returns {Promise<RawVideo>} The video
53+
*/
54+
export async function load_video(url, { num_frames = null, fps = null } = {}) {
55+
const frames = [];
56+
57+
const video = document.createElement('video');
58+
if (typeof url === 'string') {
59+
video.src = url;
60+
} else if (url instanceof Blob) {
61+
video.src = URL.createObjectURL(url);
62+
} else if (url instanceof HTMLVideoElement) {
63+
video.src = url.src;
64+
} else {
65+
throw new Error("Invalid URL or video element provided.");
66+
}
67+
video.crossOrigin = "anonymous";
68+
video.muted = true; // mute to allow autoplay
69+
70+
// Wait for metadata to load to obtain duration
71+
await new Promise((resolve) => {
72+
video.onloadedmetadata = resolve;
73+
});
74+
75+
if (video.seekable.start(0) === video.seekable.end(0)) {
76+
// Fallback: Download entire video if not seekable
77+
const response = await fetch(video.src);
78+
const blob = await response.blob();
79+
video.src = URL.createObjectURL(blob);
80+
81+
await new Promise((resolve) => {
82+
video.onloadedmetadata = resolve;
83+
});
84+
}
85+
86+
const duration = video.duration;
87+
88+
// Build an array of sample times based on num_frames or fps
89+
let sampleTimes = [];
90+
if (num_frames == null && fps == null) {
91+
throw new Error("Either num_frames or fps must be provided.");
92+
}
93+
94+
let count, step;
95+
if (num_frames != null) {
96+
count = num_frames;
97+
step = num_frames === 1 ? 0 : duration / (num_frames - 1);
98+
} else {
99+
step = 1 / fps;
100+
count = Math.floor(duration / step);
101+
}
102+
103+
for (let i = 0; i < count; i++) {
104+
sampleTimes.push(num_frames === 1 ? duration / 2 : i * step);
105+
}
106+
107+
const canvas = document.createElement('canvas');
108+
canvas.width = video.videoWidth;
109+
canvas.height = video.videoHeight;
110+
const ctx = canvas.getContext("2d", { willReadFrequently: true });
111+
for (const t of sampleTimes) {
112+
video.currentTime = t;
113+
await new Promise((resolve) => {
114+
video.onseeked = resolve;
115+
});
116+
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
117+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
118+
const frameData = new RawImage(imageData.data, canvas.width, canvas.height, 4);
119+
120+
const frame = new RawVideoFrame(frameData, t);
121+
frames.push(frame);
122+
}
123+
124+
// Clean up video element.
125+
video.remove();
126+
127+
return new RawVideo(frames, duration);
128+
}

0 commit comments

Comments
 (0)