6

I have a basic HTML5 video set up from which I load one of four videos. The problem I'm having is that when I load the next video, it continues playing from the previous time position. Efforts to set the currentTime property seem to be either short lived or ignored entirely.

I have added listeners to a collection of events and have something like this in each one;

myPlayer.addEventListener("loadeddata", function() {
        console.log(" loadeddata: before = " + myPlayer.currentTime);
        myPlayer.currentTime = 0.1;
        console.log(" loadeddata: after = " + myPlayer.currentTime);
    }, false);

Sometimes I see the time change for one event but not persist correctly;

durationchange: before = 19.773332595825195
durationchange: after = 0.10000000149011612

loadedmetadata: before = 0.10000000149011612
loadedmetadata: after = 19.773332595825195

loadeddata: before = 19.773332595825195
loadeddata: after = 0.10000000149011612

canplay: before = 0.10000000149011612
canplay: after = 19.773332595825195

And sometimes it never even seems to set at all;

durationchange: before = 50.66666793823242
durationchange: after = 50.66666793823242

loadedmetadata: before = 50.66666793823242
loadedmetadata: after = 50.66666793823242

loadeddata: before = 50.66666793823242
loadeddata: after = 50.66666793823242

canplay: before = 50.66666793823242
canplay: after = 50.66666793823242

This seems similar to the issue here but there didn't seem to be any resolution. Has anyone encountered this issue on iPhone before?

Quinnland23
  • 486
  • 1
  • 3
  • 10

8 Answers8

13

TL;DR: change currentTime on the loadeddata event. This works for audio too.

It looks like Safari (and the problem is still appearing for me on Safari 11.1) is Safari will not allow currentTime to be changed when a video is first loaded IF it hasn't loaded a frame for that currentTime yet. The bigger problem: the wrong solution can break Chrome (and likely other browsers too).

Fortunately, we have a lot of events we can listen for while media is loading:

During the loading process of an audio/video, the following events occur, in this order:

  1. loadstart
  2. durationchange
  3. loadedmetadata
  4. loadeddata
  5. progress
  6. canplay
  7. canplaythrough

-W3Schools (I know it's not a preferred source, but I couldn't find the same info on MDN)

I tried adjusting currentTime on different events, but on the first 3 events, Safari would move the time back to 0; and on 5 and 6 it seemed to prevent the video from playing in Chrome, because it would get stuck at currentTime (which I could've worked around, but I thought there was a better solution).

(I didn't want to have to load the whole file to go to the right spot, because I want to support hefty videos. So canplaythrough wasn't an option for me.)

The description for the loadeddata event reads:

The loadeddata event is fired when the first frame of the media has finished loading.

-MDN

When I changed currentTime on loadeddata, Safari could tell that the frame was loaded and available, and would update correctly. It also wouldn't cause Chrome to freeze in a single spot while playing.

The problem and solution are identical for audio.

Josh Powlison
  • 723
  • 9
  • 15
12

From my findings the issue seems to be that on iPhone only (iPad works fine) the currentTime property will not be set correctly until the "canplaythrough" event, however changing the currentTime at that point will cause a noticeable hiccup. The solution for that would be to intentionally pause the video after calling load...

myVideo.load();
myVideo.pause();        

...and then call play in the event when the time has reset.

The second problem however is when the duration of the new movie is shorter then the currentTime position. In this case not only does currentTime fail to set but "canplaythrough" is never called, and QT just sits at the end of the video doing nothing.

I discovered the solution to both problems was to force a secondary load if the currentTime was not reset in the event BEFORE "canplaythrough". Its a bit round about with the timer callback but it seems to do the trick;

var myVideo = document.getElementById("video1"); 

myVideo.addEventListener("canplay", function() {
    console.log(" canplay: before = " + myVideo.currentTime);
    myVideo.currentTime = 0.1;
    console.log(" canplay: after = " + myVideo.currentTime);

    if( myVideo.currentTime < 1 ) {
        myVideo.play();
    }
    else {
        myVideo.load();
        myVideo.pause();
        setTimeout(checkStarted, 500);
    }
}, false);

function checkStarted()
{ 
    console.log(" checkStarted called");
    myVideo.play();
}
Quinnland23
  • 486
  • 1
  • 3
  • 10
  • 1
    This is mostly right except that you can listen to the `loadedmetadata` event instead of `canplay` since it's fired earlier. – fregante May 25 '17 at 07:40
7

I had to set the preload attribute to metadata (on the HTML of the main video element) and set the currentTime of the video element within the loadedmetadata event listener.

 <video id="myVideo" preload="metadata">
    <source src="/path/to/video" type="video/mp4">
 </video>

JS

VideoEl.addEventListener('loadedmetadata', VideoMetaDataLoaded);

function VideoMetaDataLoaded() {
  VideoEl.currentTime = newTime;
}
ccates
  • 71
  • 1
  • 3
  • 1
    Hello ccates. I don't know anything about iOS development, but I'm not sure that this answer gives any information additional to that which is provided by existing answers to this question. It's also not a very detailed or reproducible answer. Please could you review the 7 existing answers and check that this answer gives additional help? If you feel that it does, please add some detail and make the new information clear. If not, please delete this answer. – Captain Hat Jan 23 '21 at 02:02
  • 1
    I had the same issue as the one described above. None of the answers here fixed my issue so I had to come up with my own unique solution. What I commented worked for me and fixed the video not loading on mobile safari. When I realized that, I took the time to come back here and post what I found. I definitely don’t feel it should be down voted though. I’m not deleting my answer because it will help someone in the future since none of these answers fixed my problem. – ccates Jan 24 '21 at 11:19
  • 1
    I am unclear about your position on this thread. Are you currently having this issue or are you just going around on this site and down voting comments whenever you feel like it? – ccates Jan 24 '21 at 11:35
  • Sorry, it sounds like I misunderstood your answer - I've removed the downvote. I got to this question via the 'first posts' review queue, where S.O users review first posts from across the site and edit/vote/comment on them to help improve site quality. – Captain Hat Jan 24 '21 at 11:44
  • Does this answer stand alone, or is it an addition to another answer for this question? If the latter, you might consider adding a comment or even suggesting an edit to that answer. – Captain Hat Jan 24 '21 at 11:44
  • 1
    I would say it can be used as a single solution to the problem. I added some example code for context to the answer.. hope it helps :) – ccates Jan 28 '21 at 14:30
  • Thanks ccates - code examples are always helpful – Captain Hat Jan 28 '21 at 14:33
  • Thanks for your answer. For anyone coming here, this fixed an issue on Safari (iPad OS + iPhone iOS). You may also want to read `video.duration` after `"loadedmetadata"` have been called, as it is no available before. – TotomInc Jan 24 '22 at 13:57
2

Below is my Angular/Ionic solution to restore the video position. It works on IOS, Android, Chrome and Safari.

HTML:

<video         preload="metadata"
               poster="{{ resource.thumbnail_file }}"
               playsinline webkit-playsinline
               #videos>
...
</video>

Typescript:

@ViewChildren('videos') videos: QueryList<any>;
videoCache: Map<number, number> = new Map<number, number>();

private restoreVideoPositions() {
    var context = this;
    setTimeout(() => {
      this.videos.forEach(function (item, idx) {
        var video = item.nativeElement;
        var currentTime = context.videoCache.get(video.id);

        if (currentTime != null && currentTime > 0) {
          console.log('add listener', currentTime);

          video.addEventListener("loadeddata", function () {
            if (video.readyState >= 3) {
              video.currentTime = currentTime;
              // video.play();
            }
          });
          video.load();
         }
      });
    }, 0);
}

private storeVideoPositions() {
    var context = this;
    this.videoCache.clear()
    this.videos.forEach(function (item, idx) {
      var video = item.nativeElement;
      video.pause();
      context.videoCache.set(video.id, video.currentTime)
    });
  }
Billal Begueradj
  • 20,717
  • 43
  • 112
  • 130
Tobias Ernst
  • 4,214
  • 1
  • 32
  • 30
  • OP is asking for a solution using vanilla javascript. In his example he is using vanilla javascript and the problem is related to vanilla HTML elements. Thus, there is no need to introduce additional code for frameworks (Angular) or additional languages (Typescript). – Wilter Monteiro Nov 01 '21 at 21:13
1

I used a combination of the last two answers here, but had to reduce the ready state limit to 2 (HTMLVideoElement.prototype.HAVE_CURRENT_DATA) as the loadeddata event on iOS would often never come in higher than 2.

Russ
  • 623
  • 8
  • 14
1

With help from the other answers here I came up with the next solution:

HTMLVideoElement.prototype.playFromTime = function(currentTime){
    let that = this;
    that.load();
    that.pause();
    that.currentTime = currentTime;
    
    let loadedMetadata;
    loadedMetadata = function(event){
        that.currentTime = currentTime;
        that.removeEventListener("loadedmetadata", loadedMetadata);
    }
    if(that.currentTime !== currentTime){
        that.addEventListener("loadedmetadata", loadedMetadata);
    }
    that.play();
}
//usage example:
myVidElement.playFromTime(20);

Which in my situation works on an iPhone on Safari, Safari on the desktop, Firefox on macOS, Chrome on macOS. That's all I have tested for now.

An Hv
  • 897
  • 1
  • 6
  • 7
Johan Velthuis
  • 280
  • 3
  • 13
0

Had the same problem with a play video on scroll with React. The following solved it for me.

 useEffect(() => {
    videoRef.current.load();
  }, []);

const { render } = ReactDOM;
const { useState, useEffect, useRef } = React;

const Video = ({ src, height, length }) => {
  const [currentTime, setCurrentTime] = useState(0);
  const videoRef = useRef(null);

  useEffect(() => {
    // This is needed for IOS
    videoRef.current.load();
    window.addEventListener("scroll", handleScroll);
    return () => window.removeEventListener("scroll", handleScroll);
  }, []);

  const getCurrentTime = () => {
    const percentScrolled = window.scrollY / (height - window.innerHeight);
    return length * percentScrolled;
  };

  const handleScroll = (e) => {
    const time = getCurrentTime();
    videoRef.current.currentTime = time;
    setCurrentTime(time);
  };

  return (
    <div style={{ height }}>
      <p>Time: {currentTime.toFixed(2)}s</p>
      <video ref={videoRef} muted playsInline>
        <source src={src} type="video/mp4" />
        Your browser does not support the video tag.
      </video>
    </div>
  );
};

const App = () => {
  return (
    <Video
      length={5}
      height={3000}
      src="https://lqez.github.io/js/airpodsvf/video.mp4"
    />
  );
};

render(<App />, document.getElementById("root"));
body {
  background: black;
}

p {
  color: slategrey;
  top: 0;
  position: fixed;
  z-index: 1;
  padding: 20px;
}

video {
  top: 60px;
  position: fixed;
  width: 100%;
  display: block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.9.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.9.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Ali Klein
  • 1,811
  • 13
  • 13
-1

this is a problem with chrome on local assets. happened with me. when i was serving video from local assets and setting time it resets the video to 0. so the solution that worked for me was serving the video from s3 bucket.

  • This answer does not address OP's question properly. The problem is not related to where the video is served from and OP is/was having trouble with iPhone(Safari) not chrome. – Wilter Monteiro Nov 01 '21 at 21:17