46

I'm trying to test whether a video is choppy. I have noticed that the pause event is not triggered when the video pauses for buffering. What is the best way to detect whether the video has paused for buffering?

Supreet Totagi
  • 822
  • 3
  • 9
  • 12

5 Answers5

54

I did this by inspecting the player progress every x milliseconds, e.g. 50. If the player hasn't advanced as much as it was expected to, then we are buffering. This is quite reliable, since I've found that other events such as waiting or stalled are not fired in all cases of the video buffering.

Note that the interval must be larger than the expected inter-frame difference, but I'm sure that you won't want to be that precise anyway. An estimation of buffering time within ±300ms would still be fine, given that humans most likely cannot perceive differences in that region.

It is important to check whether the user hasn't actively paused the playback though.

var checkInterval  = 50.0 // check every 50 ms (do not use lower values)
var lastPlayPos    = 0
var currentPlayPos = 0
var bufferingDetected = false
var player = document.getElementById('videoPlayer')

setInterval(checkBuffering, checkInterval)
function checkBuffering() {
    currentPlayPos = player.currentTime

    // checking offset should be at most the check interval
    // but allow for some margin
    var offset = (checkInterval - 20) / 1000

    // if no buffering is currently detected,
    // and the position does not seem to increase
    // and the player isn't manually paused...
    if (
            !bufferingDetected 
            && currentPlayPos < (lastPlayPos + offset)
            && !player.paused
        ) {
        console.log("buffering")
        bufferingDetected = true
    }

    // if we were buffering but the player has advanced,
    // then there is no buffering
    if (
        bufferingDetected 
        && currentPlayPos > (lastPlayPos + offset)
        && !player.paused
        ) {
        console.log("not buffering anymore")
        bufferingDetected = false
    }
    lastPlayPos = currentPlayPos
}
slhck
  • 36,575
  • 28
  • 148
  • 201
  • 1
    Should note that the `checkInterval` needs to be larger than the currentTime update interval. I tried using `30.0` and never got the "not buffering anymore" state because the `offset` was too small. – potench Jul 21 '14 at 23:43
  • 1
    @Elmo I'm quite sure. Does it not work for you? At least the HTML spec hasn't changed – to me it seems that this is still the most reliable method. You could check if the `waiting` event (as proposed in brianchirls's answer) is more reliable now in different browsers. – slhck Dec 01 '16 at 10:13
  • @slhck I just didn't try it because it seemed like something better is available now. I wish there was something like Google Analytics or some open source library for tracking video performance. – Elmo Dec 01 '16 at 16:48
  • @Elmo True, I'm not aware of anything like this. There are of course lots of providers that offer their own streaming solutions (with DASH or other techniques) and corresponding analytics, but I don't have a good overview of these either. – slhck Dec 02 '16 at 11:27
  • 1
    Am I mistaken or is there a bug in the code when calculating offset? when changing `checkInterval` to e.g. `500`, `var offset = 1 / checkInterval` becomes 1/500. This would be an incredibly small offset to check every half second. Shouldn't the offset be calculated differently if `checkInterval` is supposed to be any interval you want? I would suggest something like: `(checkInterval - x) / 1000;`. So the size of the interval in seconds, minus some margin `x < checkInterval` to account for inaccuracies – David Schumann Jun 27 '17 at 09:22
  • 1
    @DavidNathan Yes, you're absolutely right. Thank you for spotting that. I never caught that bug since I never changed the value of the offset. Not sure what value of `x` should be ... probably even `(checkInterval / 2) / 1000` would work. – slhck Jun 27 '17 at 10:32
  • I simply went with `checkInterval = 500` and `offset = 200`. That feels responsive enough, and the cases where it would not catch a buffering event are highly unlikely. But your `(checkInterval / 2) /1000 ` is good too. I would advise against using small values for checkInterval though. – David Schumann Jun 27 '17 at 11:44
26

The event you're looking for is waiting.

From the spec:

A waiting DOM event can be fired as a result of an element that is potentially playing stopping playback due to its readyState attribute changing to a value lower than HAVE_FUTURE_DATA.

The paused state does not change because the video is still "potentially playing" (i.e. "trying" to play). So the waiting event fires. When enough data has been loaded, playing fires.

You can also check the state at any time by looking at two properties, networkState and readyState

if (video.networkState === video.NETWORK_LOADING) {
    // The user agent is actively trying to download data.
}

if (video.readyState < video.HAVE_FUTURE_DATA) {
    // There is not enough data to keep playing from this point
}
zhiyelee
  • 429
  • 4
  • 14
brianchirls
  • 7,661
  • 1
  • 32
  • 32
  • 3
    As far as I've observed, the `waiting` event will **not** be fired in all cases. I simulated a slow connection and the video would buffer every few seconds, without seeing a `waiting` event fired (and neither `stalled`). The best way to check this would be in conjunction with monitoring playback progress every couple of milliseconds. – slhck May 23 '14 at 11:34
  • Which browser and what kind of video file was it? Can you seek anywhere you want in the video? – brianchirls May 23 '14 at 17:56
  • 1
    Chrome stable in Linix with an HTML5-compatible MP4 file. I was able to seek, but when the bandwidth is limited and the buffer cannot be filled, the player stalls but neither event is fired. This might as well be a bug with the API but I haven't done any cross browser checks. – slhck May 23 '14 at 18:49
  • 2
    @slhck is correct. I had the exact same issue and I solved it by checking playback progress every second (I found lower values to be overkill). I have tested this on the latest versions of FF, Chrome and IE on Windows 7. On both Chrome and IE the `readyState` property is useless as it reports `HAVE_FUTURE_DATA` while the playback is actually frozen due to lack of data. So far, out of the three browsers I tested, FF seems to be the only one that triggers the `waiting` event. – Valentin Flachsel May 26 '14 at 11:26
  • 3
    Looks like Chrome's support for `readyState` and related events is sketchy. See this bug https://code.google.com/p/chromium/issues/detail?id=144683 and https://code.google.com/p/chromium/issues/detail?id=73609 Maybe it's worth filing another one for `waiting` – brianchirls May 27 '14 at 00:20
  • 3
    "waiting" event DOES fire reliably as of 2019. I noticed that a lot of these issues are from 2014 so things appear to have moved on since then. – Michael Giovanni Pumo May 16 '19 at 11:31
4

As per MDN docs 'waiting' event is -

Sent when the requested operation (such as playback) is delayed pending the completion of another operation (such as a seek).

So seek or network request will trigger 'waiting'. Michael in the comments did point out that 'waiting' is reliable as of 2019 so I gave this a try and it worked!

let slowInternetTimeout = null;

let threshold = 3000; //ms after which user perceives buffering

video.addEventListener('waiting', () => {
    slowInternetTimeout = setTimeout(() => {
        //show buffering
    }, threshold);
});
video.addEventListener('playing', () => {
    if(slowInternetTimeout != null){
        clearTimeout(slowInternetTimeout);
        slowInternetTimeout = null;
    }
});
1

You can just check the buffered video content length and if it is less than the current playing part then just fire the pause event.Using following code you can check the buffered video length.

$vid = $("#video_id");

$vid.on('progress', function(e) {

    percentVidLoaded = null;
    // FF4+, Chrome
    if ($vid[0] && $vid[0].buffered && $vid[0].buffered.length > 0 && $vid[0].buffered.end && $vid[0].duration) {
        percentVidLoaded = $vid[0].buffered.end(0) / $vid[0].duration;
    }
    /* Some browsers (e.g., FF3.6 and Safari 5) cannot calculate target.bufferered.end()
     *  to be anything other than 0. If the byte count is available we use this instead.
     *  Browsers that support the else if do not seem to have the bufferedBytes value and
     *  should skip to there.
     */
    else if ($vid[0] && $vid[0].bytesTotal != undefined && $vid[0].bytesTotal > 0 && $vid[0].bufferedBytes != undefined) {
        percentVidLoaded = $vid[0].bufferedBytes / $vid[0].bytesTotal;
    }
    if (percentVidLoaded !== null) {
        percentVidLoaded = 100 * Math.min(1, Math.max(0, percentVidLoaded));
    }
});
jedierikb
  • 12,752
  • 22
  • 95
  • 166
Arjun Thakur
  • 635
  • 8
  • 21
  • 1
    use `typeof !== "undefined"` – dude Dec 07 '15 at 10:18
  • 2
    This will only read the buffer and convert it into percentage. This will not check the buffer against the video.currentTime. – dude Dec 07 '15 at 13:52
  • Anyway the value of video.currentTime is not the currentTime that the video should be at, unfortunately is the real currentTime, so no way to check against the buffered percentage. – Splact Jun 20 '16 at 11:16
0

You need to check if the buffer is less than the current video time. If so, then the video is buffering. However, you should check this with a small tolerance to make sure you detect it before it is acuatally necessary to buffer.

Example:

var video = document.getElementById("myVideo");
var prevBuffer = {
    "buffer": null,
    "time": null
};
var isBuffering = function(){

    if(video && video.buffered && video.buffered.end && video.buffered.length > 0){
        var buffer = video.buffered.end(0);
        var time   = video.currentTime;

        // Check if the video hangs because of issues with e.g. performance
        if(prevBuffer.buffer === buffer && prevBuffer.time === time && !video.paused){
            return true;
        }
        prevBuffer = {
            "buffer": buffer,
            "time": time
        };
        // Check if video buffer is less
        // than current time (tolerance 3 sec)
        if((buffer - 3) < time){
            return true;
        }
    }
    return false;

};
video.addEventListener("play", function(e){
    // Make sure this handler is only called once
    e.target.removeEventListener(e.type, arguments.callee);
    // Give browsers 3secs time to buffer
    setTimeout(function(){
        // As "progress", "stalled" or "waiting" aren't fired
        // reliable, we need to use an interval
        var interval = setInterval(function(){
            if(isBuffering()){
                clearInterval(interval);
                console.log("Buffering");
            }
        }, 500);
    }, 3000);
});
dude
  • 5,678
  • 11
  • 54
  • 81
  • The stream may stall even if the buffer end is greater than the current time. I observed that on some browsers we will have network state NETWORK_LOADING whether or not we are paused due to underflow as well as readyState HAVE_ENOUGH_DATA and where buffered.end() > currentTime + tolerance. The tolerance value is the one that causes the issue here. Can we read a tolerance value from the video element such that we always know whether or not it is buffering? – angie Jan 05 '16 at 01:47
  • As far as I know there is not specific method to observe the buffer state. There is nothing more than what you just mentioned ("networkState") and the "readyState" property. However, they were built for a different approach. In which browser did this happen? – dude Jan 05 '16 at 07:14
  • @angie I just edited my answer. This may fix your use case too. – dude Feb 10 '16 at 12:51
  • Thanks for editing. Was the 3 second value based on observation? A 3 second tolerance may do the trick (although if the player is intelligent it may vary based on bandwidth, I am not sure). – angie May 08 '16 at 01:24