play video in loop using setTimeout

Viewed 1516

I want to play videos in a loop. For some reason I don't want change video src on ended event. So I created video elements for each video in a loop. Also I have video src and durations in array.

here is my idea: Only current playing video tag can be visible. Others will be hided. Instead of using ended event, I want to use setTimeout function. Video's duration will be delay parameter.

But they all play together. I couldn't make them play in order.

Here is what I done so far:

videoArray = [
    {"video":video1.mp4, "duration": 5},
    {"video":video2.mp4, "duration": 7},
    {"video":video3.mp4, "duration": 9},
    {"video":video4.mp4, "duration": 10},
]

for (var j = 0; j < videoArray.length; j++){
    var video = document.createElement("video");
    video.src=videoArray[j];
    video.id="video-"+j;
    video.preload = "metadata";
    video.type="video/mp4";
    video.autoplay = true; 
    video.style.display="none";
    document.body.appendChild(video); 
}

for (var count = 0; count < videoArray.length; count++) {
    (function(num){
        setTimeout(function() {
            videoArray[num].video.style.display="block";
            videoArray[num].video.play();
        }, 1000 * videoArray[num].duration);
        videoArray[num].video.style.display="none";
    })(count);
}
3 Answers

Disclaimer

I know that the question was asked without the ended event, but I do not think that set time out is the way to go.
Think of the scenario where you have video buffering, or slowing down for any reason, your setTimeout will be out of sync.

At the bottom I've added another solution that answers the requirement of not using the ended event, but again, I do not recommend using it.

Solution

The idea is to have an event listener to the end of the video, in that case, even if you run the video on a different speed you are still going to run the next video regardless of the duration.

Another benefit is that you do not need to know the duration of the videos in the first place.

PS. the event listener that you need to listen to is video.addEventListener("ended", callback);

You are more than welcome to run the code or to have a look at a working example I've created for you

Working Example

    const videoUrls = [
        'https://videos-play-loop.netlify.com/video1.mp4',
        'https://videos-play-loop.netlify.com//video2.mp4',
        'https://videos-play-loop.netlify.com//video3.mp4',
    ];

    const createVideo = ({id, src, width, cls = 'video', display = 'block', playbackRate = 1, muted = true, type = 'video/mp4', autoplay = false, controls = true}) => {
        const videoElement = document.createElement("video");
        videoElement.id = id;
        videoElement.src = src;
        videoElement.classList.add(src);
        videoElement.type = type;
        videoElement.autoplay = autoplay;
        videoElement.controls = controls;
        videoElement.style.display = display;
        videoElement.muted = muted;
        videoElement.playbackRate = playbackRate;
        return videoElement;
    };


    const addVideos = (container, videoUrls) => {
        const videos = videoUrls.map((url, index) => {
            const first = index === 0;
            const display = first ? 'block' : 'none';
            return createVideo({id: `video-${index}`, src: url,display, width: 640, autoplay: first, playbackRate: 3});
        });
        videos.forEach((video, index) => {
            const last = index === videos.length - 1;
            const playNext = (element) => {
                element.target.style.display = "none";
                const nextElementIndex = last ? 0 : index + 1;
                const nextElement = videos[nextElementIndex];
                nextElement.autoplay = true;
                nextElement.style.display="block";
                nextElement.load();
            };
            video.addEventListener("ended", playNext);
            container.appendChild(video)
        });
    };
    const videoWrapper = document.getElementById('video-wrapper');
    addVideos(videoWrapper, videoUrls);
#video-wrapper video {
    max-width: 600px;
}
<div id="video-wrapper"></div>

Working solution with setTimeout (please use the solution above)

const videoUrls = [{
    url: `https://videos-play-loop.netlify.com/video3.mp4`,
    duration: 3,
  },
  {
    url: `https://videos-play-loop.netlify.com/video2.mp4`,
    duration: 4
  },
  {
    url: `https://videos-play-loop.netlify.com/video1.mp4`,
    duration: 5
  }
];


const createVideo = ({
  id,
  src,
  width,
  cls = 'video',
  display = 'block',
  duration,
  playbackRate = 1,
  muted = true,
  type = 'video/mp4',
  autoplay = false,
  controls = true
}) => {
  const videoElement = document.createElement("video");
  videoElement.id = id;
  videoElement.src = src;
  videoElement.classList.add(src);
  videoElement.type = type;
  videoElement.autoplay = autoplay;
  videoElement.controls = controls;
  videoElement.style.display = display;
  videoElement.muted = muted;
  videoElement.playbackRate = playbackRate;
  videoElement.setAttribute('data-duration', duration);
  return videoElement;
};

const playNext = (videos, index) => {
  const current = videos[index];
  const activeVideoDuration = parseInt(current.dataset.duration) * 1000;
  setTimeout(() => {
    const last = index === videos.length - 1;
    current.style.display = "none";
    current.pause();
    const activeVideoIndex = last ? 0 : index + 1;
    const next = videos[activeVideoIndex];
    next.autoplay = true;
    next.style.display = "block";
    next.load();
    next.play();
    playNext(videos, activeVideoIndex);
  }, activeVideoDuration);
};


const addVideos = (container, videoUrls) => {
  const videos = videoUrls.map((video, index) => {
    const {
      url,
      duration
    } = video;
    const first = index === 0;
    const display = first ? 'block' : 'none';
    return createVideo({
      id: `video-${index}`,
      src: url,
      duration,
      display,
      width: 640,
      autoplay: first,
    });
  });

  videos.forEach(video => container.appendChild(video));
  playNext(videos, 0);
};

const videoWrapper = document.getElementById('video-wrapper');
addVideos(videoWrapper, videoUrls);
#video-wrapper video {
  max-width: 600px;
}
<div id="video-wrapper"></div>

You could hold the duration of the videos in a variable and the accumulate this variable with the previous video duration and set this a the setTimeOut duration.

Note that the time of the videos is in seconds. And for the first video to play user has to interact otherwise the video will not play.

Working Example:

function startVideos(event) {
 event.target.style.display= "none";
        (function() {
          videoArray = [
            {
              video:
                "http://techslides.com/demos/sample-videos/small.mp4",
              duration: 5
            },
            {
              video:
                "http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4",
              duration: 60
            },
            {
              video:
                "https://mobamotion.mobatek.net/samples/sample-mp4-video.mp4",
              duration: 120
            },
            {
              video:
                "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
              duration: 600
            }
          ];

          let videArrayElem = [];

          for (var j = 0; j < videoArray.length; j++) {
            var video = document.createElement("video");
            video.src = videoArray[j].video;
            video.id = "video-" + j;
            video.preload = "metadata";
            video.type = "video/mp4";
            video.autoplay = false;
            video.controls= true;
            video.style.display = "none";
            videArrayElem.push(video);
            document.body.appendChild(video);
          }

          let prviousVideoDuration = 0;
          for (var count = 0; count < videoArray.length; count++) {
            (function(num) {
              setTimeout(function() {
                videArrayElem[num].style.display = "block";
                videArrayElem[num].play();
              }, prviousVideoDuration);
              prviousVideoDuration += 1000 * videoArray[num].duration;
              videArrayElem[num].style.display = "none";
            })(count);
          }
        })();
      }
video {
        height: 100px;
        width: 100px;
        display: inline-block;
        margin: 4px;
        float: left;
      }
<button type="button" onclick="startVideos(event)">Start Video Demo</button>

Aside from the facts that you are trying to achieve something strange, and that your code example will not compile. As I see it, you've got it. The only missing thing is pause() and display = "none". With minimum edits you have what you need.

const videoArray = [
    {"video":"video1.mp4", "duration": 5}, // of cause you need "" 
    {"video":"video2.mp4", "duration": 7},
    {"video":"video3.mp4", "duration": 9},
    {"video":"video4.mp4", "duration": 10},
]

for (let j = 0; j < videoArray.length; j++){
    const video = document.createElement("video");
    // you need to save DOM nodes somewhere to use them in the second loop
    videoArray[j].video_el = video
    video.src=videoArray[j].video; // `.video` is added
    video.id="video-"+j;
    video.preload = "metadata";
    video.type="video/mp4";
    video.autoplay = true; 
    video.style.display="none";
    document.body.appendChild(video); 
}

for (var count = 0; count < videoArray.length; count++) {
    (function(num){
        setTimeout(function() {
            // add this to hide previous video node and stop video from playing 
            if( num ) {
                videoArray[num-1].video_el.style.display="none";
                videoArray[num-1].video_el.pause()
            }
            // videoArray[num].video - is a string, not a DOM node
            // so, you need to change this:
            // videoArray[num].video.style.display="block";
            // for this:
            videoArray[num].video_el.style.display="block";
            // no need. `autoplay` is set to `true` in the first loop
            // videoArray[num].video_el.play();
        }, 1000 * videoArray[num].duration);
        // no need. already done in the first loop
        // videoArray[num].video_el.style.display="none";
    })(count);
}

But there is a lot of flaws:

  1. setTimeout does not care about network delays and other time related issues, so most probably your video sequence will not play seamlessly.
  2. Because of the first flow and because I doubt that duration values are exact you should use pause().
  3. As @IslamElshobokshy noted in comments to your question using play/pause is not so simple.
  4. If user opens the page and does nothing more, you'll get:

    Uncaught (in promise) DOMException: play() failed because the user didn't interact with the document first.

    in Chrome. And

    Autoplay is only allowed when approved by the user, the site is activated by the user, or media is muted.
    NotAllowedError: The play method is not allowed by the user agent or the platform in the current context, possibly because the user denied permission.

    in Firefox.

So you'd probably be better off with the ended event after all, and with muted attribute (to mitigate the last issue):

for (let j = 0; j < videoArray.length; j++){
    const video = document.createElement("video")
    videoArray[j].video_el = video

    video.src      = videoArray[j].video
    video.id       = "video-"+j
    video.preload  = "metadata"
    video.type     = "video/mp4"
    video.autoplay = true

    // show the first video right away
    if( j !== 0 ) video.style.display = "none"
    // set muted attribute
    video.muted = "muted"
    // play next video after the previous had ended
    video.addEventListener('ended', (num => function() {
        this.style.display = "none";
        if( num !== videoArray.length-1 ) {
            videoArray[num+1].video_el.style.display="block";
            // only needed if `muted` is not set 
            videoArray[num+1].video_el.play();
        }
    })(j), false);

    document.body.appendChild(video); 
}

If muted attribute is not the option, you can add some event listeners on the body to call play() on the first video as soon as the user starts interaction with the page. And maybe you would like to read more about autoplay here: https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide

Related