43

Can HTML5 <video> tag be played in reverse, or do I have to download 2 videos (forward and backward play)?

I'm looking for a solution that avoids a user from downloading 2 videos.

cokeman19
  • 2,405
  • 1
  • 25
  • 40
Ish
  • 28,486
  • 11
  • 60
  • 77
  • 3
    I'm strongly leaning towards **NO**, as most compression schemes assume you're playing Forward. – drudge Mar 11 '11 at 19:20

6 Answers6

34

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.

Frédéric Hamidi
  • 258,201
  • 41
  • 486
  • 479
  • 2
    Thanks @Frédéric, Now i know having 2 separate video's is better solution to my problem. – Ish Mar 11 '11 at 19:49
  • But Adding two separate Videos required to load multiple players or changing current player video source. changing current player video source will cost loading time. – Arham Ali Qureshi Apr 22 '15 at 20:00
  • @Arham, but buffering the whole input stream locally in order to reverse it on the client is not an option either (yet), I'm afraid. – Frédéric Hamidi Apr 22 '15 at 21:00
  • @FrédéricHamidi. you can set initially current time equal to video duration and use setInterval to subtract current time on every interval but you would need to fine tune that interval duration and subtraction of current time. – Arham Ali Qureshi May 05 '15 at 20:04
  • 1
    @Arham, that would be murder for forward-streaming formats. Every time you track back to a point that does not exactly match a keyframe, the previous keyframe and all the relative frames between it and your track point would have to be rendered. Even four years later, I doubt most machines have the raw power to do that with acceptable performance. Oh, and you would still have to stream the whole content before you can play it. – Frédéric Hamidi May 05 '15 at 20:08
14

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.

BaptisteB
  • 1,168
  • 2
  • 9
  • 16
  • 10
    It would be nice to see a jsfiddle of this. – Isaac Lubow Feb 09 '17 at 17:40
  • 2
    Here's a very rough one I got to work, it's a little choppy but you can set the rate by changing `0.1` and `100` (i.e. 0.1 seconds being equal to 100 ms): ```js var v = document.getElementById('video'); var reverse = setInterval(function() { v.currentTime = v.currentTime - 0.1; }, 100); ``` – jywarren Apr 30 '20 at 19:45
  • Sorry and to stop it playing in reverse, you run `clearInterval(reverse);` – jywarren Apr 30 '20 at 19:52
11
aMediaElement.playbackRate = -1;

UAs may not support this, though it is valid to set playbackRate to a negative value.

Eli Grey
  • 35,104
  • 14
  • 75
  • 93
  • 3
    Worked with MP4 in Safari, but was very clunky (video was not smooth at all). – Luke Stevenson Dec 20 '11 at 00:58
  • 10
    Note that Chrome doesn't have plans to implement negative playbackRate at this time. https://code.google.com/p/chromium/issues/detail?id=33099 – Garen Checkley Oct 25 '13 at 04:48
  • 7
    Not supported in Firefox at this time, either – commonpike Jan 03 '14 at 20:15
  • From my testing it's supported only in Safari. MDN says "Negative values will not cause the media to play in reverse." (https://developer.mozilla.org/en-US/Apps/Fundamentals/Audio_and_video_delivery/WebAudio_playbackRate_explained) however the specification allows negative values (https://dev.w3.org/html5/spec-preview/media-elements.html#effective-playback-rate) – Adam Chwedyk Sep 08 '16 at 14:13
11

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;
}
Christopher
  • 3,124
  • 2
  • 12
  • 29
1

I tried request animation frame, calculated the diff and updated currentTime. This does not work, the video tag doesn't repaint fast enough.

Mikeec3
  • 649
  • 3
  • 9
0

Get the HTMLMediaElement's duration then set an Interval that would run every second and playing the media by setting the .currentTime and decrease the value every second by 1. The media element must be fully downloaded for this to work. I've only tested this on 30-second clips and unsure if faster (lesser than 1sec.) intervals are possible. Note that this method still plays the media forward. Try increasing media playback rate to make it feel more seamless.