Is it possible to play HTML5 video in reverse?
Solution 1:
Without even going into HTML5 or Javascript, many video formats are streaming formats that are designed to be played forward. Playing it backwards would require decoding the whole stream, storing each raw frame on the disk to avoid clobbering memory, then rendering the frames backwards.
At least one person actually tried that using mplayer
, though, so it can be done, at least in principle.
Solution 2:
I managed to do it in an update method. Every frame I decrease video.currentTime to the time elapsed and so far it is the best result I managed to get.
Solution 3:
aMediaElement.playbackRate = -1;
UAs may not support this, though it is valid to set playbackRate
to a negative value.
Solution 4:
This snippet just shows, how it could be done, but it takes some time to copy each frame. Which highly depends on the hardware.
It generates a canvas of each frame it has to play. When it's on 100% the playback starts directly and plays backward, forward... and so on. The original Video is also attached after the generation, but won't play automatically due to iframe rules.
It is fine for short videos and as a proof of concept.
Update: I changed the order of the capture part so that the frame at max duration is skipped but the one at 0 is captured (forgot it before).
The frame at max duration caused a white canvas on every video i tried.
I also changed the playback to play it in reverse order as soon as the last frame is reached for an endless playback. So you easily see, that this playback is a bit CPU intensive compared to hardware accelerated videos.
fetch('https://i.imgur.com/BPQF5yy.mp4')
.then(res => res.blob())
.then(blob => {
return new Promise((res) => {
const fr = new FileReader();
fr.onload = e => res(fr.result);
fr.readAsDataURL(blob);
})
}).then(async(base64str) => {
const video = document.createElement("video");
video.src = base64str;
video.controls = true;
video.className = "w-50";
while (isNaN(video.duration))
await new Promise((r) => setTimeout(r, 50));
const FPS = 25;
const status = document.createElement("div"),
length = document.createElement("div");
length.innerText = `Generating ${Math.ceil(video.duration / (1 / FPS))} frames for a ${FPS} FPS playback (Video Duration = ${video.duration})`;
document.body.appendChild(length);
document.body.appendChild(status);
const frames = [],
copy = () => {
const c = document.createElement("canvas");
Object.assign(c, {
width: video.videoWidth,
height: video.videoHeight,
className: "w-50"
});
c.getContext("2d").drawImage(video, 0, 0);
return c;
};
// first seek outside of the loop this image won't be copied
video.currentTime = video.duration;
// this frame seems to be always white/transparent
while (video.currentTime) {
if (video.currentTime - 1 / FPS < 0)
video.currentTime = 0;
else
video.currentTime = video.currentTime - 1 / FPS;
await new Promise((next) => {
video.addEventListener('seeked', () => {
frames.push(copy());
status.innerText = (frames.length / (Math.ceil(video.duration / (1 / FPS))) * 100).toFixed(2) + '%';
next();
}, {
once: true
});
});
}
/*
* frames now contains all canvas elements created,
* I just append the first image and replace it on
* every tick with the next frame.
* using last.replaceWith(frames[currentPos]) guarantees a smooth playback.
*/
let i = 0, last = frames[0];
document.body.insertAdjacentHTML('beforeend', `<div class="w-50">Captured</div><div class="w-50">Video</div>`);
document.body.appendChild(frames[0]);
const interval = setInterval(() => {
if (frames[++i]) {
last.replaceWith(frames[i]);
last = frames[i];
} else {
frames.reverse();
i=0;
}
}, 1000 / FPS);
document.body.appendChild(video);
// won't :(
video.play();
});
/* Just for this example */
.w-50 {
width: 50%;
display: inline-block;
}
* {
margin: 0;
padding: 0;
font-family: Sans-Serif;
font-size: 12px;
}