nickdesaulniers / netfix

Let's build a Netflix
http://nickdesaulniers.github.io/netfix
172 stars 129 forks source link

bufferWhenNeeded stops loading anything when manually jumping to a different time in video #4

Open dstaubsauger opened 8 years ago

dstaubsauger commented 8 years ago

I saw there was some code listening to the seek event, so i tried it, but:

Expected behaviour: Jumping to a certain point loads the video at that point (if not already loaded) and continues playing/loading from there

Actual behaviour: Video only plays if that point is already loaded. No subsequent parts are loaded. This also applies when seeking backwards.

IShinji commented 6 years ago

Yeah, I have found the some problem. Can anybody help? I had try to fix it, but it didn't work.

IShinji commented 6 years ago

……Author knew the issue. See here, https://hacks.mozilla.org/2015/07/streaming-media-on-demand-with-media-source-extensions/

My demo is a little naive and has a few issues:

It doesn’t show how to properly handle seeking during playback. It assumes bandwidth is constant (always fetching the next segment at 80% playback of the previous segment), which it isn’t. It starts off by loading only one segment (it might be better to fetch the first few, then wait to fetch the rest). It doesn’t switch between segments of varying resolution, instead only fetching segments of one quality. It doesn’t remove segments (part of the MSE API), although this can be helpful on memory constrained devices. Unfortunately, this requires you to re-fetch content when seeking backwards.

anuragteapot commented 4 years ago

@IShinji @dstaubsauger

I try this code

`var video = document.querySelector("video");

  var assetURL = "frag_bunny.mp4";
  var mimeCodec = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"';
  var totalSegments;
  var segmentLength = 0;
  var segmentDuration = 0;
  var seeking = false;
  var bytesFetched = 0;

  var requestedSegments = [];

  var mediaSource = null;
  if ("MediaSource" in window && MediaSource.isTypeSupported(mimeCodec)) {
    mediaSource = new MediaSource();
    video.src = URL.createObjectURL(mediaSource);
    mediaSource.addEventListener("sourceopen", sourceOpen);
  } else {
    console.error("Unsupported MIME type or codec: ", mimeCodec);
  }
  var sourceBuffer = null;

  function sourceOpen(_) {
    sourceBuffer = mediaSource.addSourceBuffer(mimeCodec);
    sourceBuffer.mode = "sequence";
    getFileLength(assetURL, function(fileLength) {
      // console.log((fileLength / 1024 / 1024).toFixed(2), "MB");
      totalSegments = (fileLength / 1024 / 1024).toFixed(0);
      for (var i = 0; i < totalSegments; ++i) requestedSegments[i] = false;

      segmentLength = Math.round(fileLength / totalSegments);

      fetchRange(assetURL, 0, segmentLength, appendSegment);
      requestedSegments[0] = true;
      video.addEventListener("timeupdate", checkBuffer);
      video.addEventListener("canplay", function() {
        segmentDuration = video.duration / totalSegments;
        const promise = video.play();
        if (promise !== undefined) {
          promise
            .then(_ => {
              console.log(_);
              // Autoplay started!
            })
            .catch(error => {
              // Autoplay was prevented.
              // Show a "Play" button so that user can start playback.
            });
        }
      });

      video.addEventListener("seeking", debounce(handleSeek, 500));
      video.addEventListener("seeked", onSeeked);
    });
  }

  function debounce(func, wait, immediate) {
    var timeout;
    return function() {
      var context = this;
      var args = arguments;
      var later = function() {
        timeout = null;
        if (!immediate) func.apply(context, args);
      };
      var callNow = immediate && !timeout;
      clearTimeout(timeout);
      timeout = setTimeout(later, wait);
      if (callNow) func.apply(context, args);
    };
  }

  function getFileLength(url, cb) {
    var xhr = new XMLHttpRequest();
    xhr.open("head", url);
    xhr.onload = function() {
      cb(xhr.getResponseHeader("content-length"));
    };
    xhr.send();
  }

  function onSeeked() {
    console.log("seeked");
  }

  function fetchRange(url, start, end, cb) {
    return new Promise((resolve, reject) => {
      var xhr = new XMLHttpRequest();
      xhr.open("get", url);
      xhr.responseType = "arraybuffer";
      xhr.setRequestHeader("Range", "bytes=" + start + "-" + end);
      xhr.onload = function() {
        console.log("fetched bytes: ", start, end);
        bytesFetched += end - start + 1;
        resolve(cb(xhr.response));
      };
      xhr.send();
    });
  }

  function appendSegment(chunk) {
    return sourceBuffer.appendBuffer(chunk);
  }

  async function checkBuffer() {
    console.log("check Buffer");
    if (seeking) {
      return true;
    }
    var nextSegment = getNextSegment();
    if (nextSegment === totalSegments && haveAllSegments()) {
      mediaSource.endOfStream();
      video.removeEventListener("timeupdate", checkBuffer);
    } else if (shouldFetchNextSegment(nextSegment)) {
      requestedSegments[nextSegment] = true;

      await fetchRange(
        assetURL,
        bytesFetched,
        bytesFetched + segmentLength,
        appendSegment
      );
    }
  }

  async function handleSeek() {
    console.log("seeking");
    seeking = true;
    var nextSegment = getNextSegment();
    if (nextSegment === totalSegments && haveAllSegments()) {
      mediaSource.endOfStream();
      video.removeEventListener("timeupdate", checkBuffer);
    } else {
      // sourceBuffer.abort();
      for (let segment = 1; segment < nextSegment; segment++) {
        if (shouldFetchNextSegment(segment)) {
          requestedSegments[segment] = true;

          // console.log({ bytesFetched, next: bytesFetched + segmentLength });

          await fetchRange(
            assetURL,
            bytesFetched,
            bytesFetched + segmentLength,
            appendSegment
          );
        }
      }
    }
    seeking = false;
  }

  function seek(e) {
    if (mediaSource.readyState === "open") {
      handleSeek();
    } else {
      console.log("seek but not open?");
      console.log(mediaSource.readyState);
    }
  }

  function getNextSegment() {
    return ((video.currentTime / segmentDuration) | 0) + 1;
  }

  function haveAllSegments() {
    return requestedSegments.every(function(val) {
      return !!val;
    });
  }

  function shouldFetchNextSegment(nextSegment) {
    // console.log(nextSegment);
    // console.log({
    //   nextSegment,
    //   currentTime: video.currentTime,
    //   val: segmentDuration * nextSegment * 0.1,
    //   bool: requestedSegments[nextSegment]
    // });
    return (
      video.currentTime > segmentDuration * nextSegment * 0.1 &&
      requestedSegments[nextSegment] == false
    );
  }`