javascripthtmlvideo

How to best track how long a video was played?


When the user watches a video, I would like to make 2 AJAX calls. One when the user finished watching the video and the time played is equal or more than the duration of the video (because users can rewind as well then). timePlayed>=duration && event.type=="ended". I successfully make the call for that.

Where I struggle is that I would also like to make a call when the video is watched more than 80% and the time played of the video is more than 80% as well in order to prevent the user from just fast forwarding.

In order for that to work I have to modify my videoStartedPlaying() method and this is where I come across issues as I am trying to set an interval. Now, with setting an interval, it is like an endless loop.

var video_data = document.getElementById("video");

var timeStarted = -1;
var timePlayed = 0;
var duration = 0;

// If video metadata is loaded get duration
if(video_data.readyState > 0)
    getDuration.call(video_data);
//If metadata not loaded, use event to get it
else {
    video_data.addEventListener('loadedmetadata', getDuration);
}

// remember time user started the video
function videoStartedPlaying() {
    timeStarted = new Date().getTime()/1000;
    setInterval(function(){
        playedFor = new Date().getTime()/1000 - timeStarted;
        checkpoint = playedFor / duration;
        percentComplete = video_data.currentTime/video_data.duration;

        // here I need help of how to best accomplish this
        if (percentComplete >= 0.8 && checkpoint >= 0.8) {
            // AJAX call here
        }
    }, 2000);
}

function videoStoppedPlaying(event) {
    // Start time less then zero means stop event was fired vidout start event
    if(timeStarted>0) {
        var playedFor = new Date().getTime()/1000 - timeStarted;
        timeStarted = -1;
        // add the new amount of seconds played
        timePlayed+=playedFor;
    }

    // Count as complete only if end of video was reached
    if(timePlayed>=duration && event.type=="ended") {
        // AJAX call here
    }
}

function getDuration() {
    duration = video_data.duration;
}

video_data.addEventListener("play", videoStartedPlaying);
video_data.addEventListener("playing", videoStartedPlaying);
video_data.addEventListener("ended", videoStoppedPlaying);
video_data.addEventListener("pause", videoStoppedPlaying);

I truly would appreciate any help with this as it seems like I am at my wits end.

Thanks so much!

Edit: Thanks to the comment I came up with this:

const video = document.getElementById("video");
const set = new Set();
const percent = .8;
let toWatch;

function mediaWatched (curr) {
  alert(`${curr}% of media watched`)
}

function handleMetadata(e) {
  toWatch = Math.ceil(video.duration * percent);
  console.log(toWatch, video.duration);
}

function handleTimeupdate (e) {
  set.add(Math.ceil(video.currentTime));
  let watched = Array.from(set).pop();
  if (set.has(toWatch) && watched === toWatch) {
    video.removeEventListener("timeupdate", handleTimeupdate);
    console.log(watched);
    mediaWatched(
      Math.round(watched / Math.ceil(video.duration) * 100)
    );
  }
}

video.addEventListener("loadedmetadata", handleMetadata);

video.addEventListener("timeupdate", handleTimeupdate);
<video width="400" height="300" controls="true" poster="" id="video">
    <source type="video/mp4" src="http://www.sample-videos.com/video/mp4/720/big_buck_bunny_720p_2mb.mp4" />
</video>

Now, for example, if I fast forward to around 50% length, and let it play then, it will fire whenever 80% of the movie is reached, but it shouldn't because I fast forwarded to 50% and essentially only watched 30%.

Does that make sense? How can I achieve such behavior?


Solution

  • As per discussion in the comments here's a working sample.

    It includes a couple of handlers just to make life easier in setting up the array and summing the contents so you know when you have reached the 80% mark (though you may need to change that logic if you want to force them to, say, explicit watch the first 80% not just a total of 80% throughout the video).

    There are a number of console.log(...) statements in there so you can watch what it's doing in the browser console window... you'll probably want to take them out before deploying the code.

    I've put the hook for where to make the ajax call in the timeupdate event, but you could always use a regular setInterval timer in the main loop as well to check for the 80% and make the call there, but this seemed cleaner.

    Most of it should be self explanatory, but do ask in comments if there's anything that's not clear...

    <video controls preload="auto" id="video" width="640" height="365" muted>
          <source src="http://www.sample-videos.com/video/mp4/720/big_buck_bunny_720p_2mb.mp4" type="video/mp4">
        </video>
     
    <script>
     
     // handler to let me resize the array once we know the length
     Array.prototype.resize = function(newSize, defaultValue) {
        while(newSize > this.length)
            this.push(defaultValue);
        this.length = newSize;
    }
     
    // function to round up a number
    function roundUp(num, precision) {
      return Math.ceil(num * precision) / precision
    } 
     
    var vid = document.getElementById("video")
    var duration = 0; // will hold length of the video in seconds
    var watched = new Array(0);
    var reported80percent = false;
    
    vid.addEventListener('loadedmetadata', getDuration, false);
    vid.addEventListener('timeupdate',timeupdate,false)
    
    function timeupdate() {
        currentTime = parseInt(vid.currentTime);
        // set the current second to "1" to flag it as watched
        watched[currentTime] = 1;
        
        // show the array of seconds so you can track what has been watched
        // you'll note that simply looping over the same few seconds never gets
        // the user closer to the magic 80%...
        console.log(watched);
        
        // sum the value of the array (add up the "watched" seconds)
        var sum = watched.reduce(function(acc, val) {return acc + val;}, 0);
        // take your desired action on the 80% completion
        if ((sum >= (duration * .8)) && !reported80percent) {
            // set reported80percent to true so that the action is triggered once and only once
            // could also unregister the timeupdate event to avoid calling unneeded code at this point
            // vid.removeEventListener('timeupdate',timeupdate)
            reported80percent = true;
            console.log("80% watched...")
            // your ajax call to report progress could go here...   
        }
    }
    
    function getDuration() {
        console.log("duration:" + vid.duration)
        // get the duration in seconds, rounding up, to size the array
        duration = parseInt(roundUp(vid.duration,1));
        // resize the array, defaulting entries to zero
        console.log("resizing arrary to " + duration + " seconds.");
        watched.resize(duration,0)
    }
    
    </script>