3

I've written a VideoBuffer class for loading segments of webm video. Each segment works separately when opened as a blob in the video element or directly in a separate browser tab, but I can't seem to get it to work when appending them to a source buffer.

There's no error or exception being thrown, the ready state of the MediaSource gets closed and the SourceBufferList gets emptied. I've based much on my code on this working example, where the main difference is that I am appending each xhr response to the buffer instead of appending the results from a FileReader.

Any ideas of why this isn't working or how I should debug it?

var EventEmitter = require("events").EventEmitter;

function VideoBuffer(options) {
  this.video = options.video;
  this.source = new MediaSource();
  this.video.src = URL.createObjectURL(this.source);
  this.url = options.url;
  this.segment = 0;
  this.time = 0;
  this.duration = options.duration;
  this.segments = Math.ceil(this.duration/10);
  this.preset = options.preset;
  this.source.addEventListener("sourceopen", this.onSourceOpen.bind(this));
  this.source.addEventListener("sourceended", this.onSourceEnded.bind(this));
  this.video.addEventListener("progress", this.onVideoProgress.bind(this));

  this.on("error", function(err){
    try {
      throw err;
    } catch (e) {
      console.error(e.stack);
    }
  });
}

VideoBuffer.prototype = Object.create(EventEmitter.prototype);

VideoBuffer.prototype.onSourceOpen = function (e) {
  this.debug("Source opened:", this.source);
  var buffer = this.source.addSourceBuffer('video/webm; codecs="vorbis,vp8"');
  buffer.addEventListener("updatestart", this.onBufferUpdateStart.bind(this));
  buffer.addEventListener("update", this.onBufferUpdate.bind(this));
  buffer.addEventListener("updateend", this.onBufferUpdateEnd.bind(this));
  this.debug("Added source buffer:", buffer);
  this.emit("source:open", e);
};

VideoBuffer.prototype.onSourceEnded = function (e) {
  this.debug("Source ended:", this.mediaSource);
  this.emit("source:ended", e);
};

VideoBuffer.prototype.onBufferUpdateStart = function (e) {
  this.emit("buffer:updatestart", e);
};

VideoBuffer.prototype.onBufferUpdate = function (e) {
  this.emit("buffer:update", e);
};

VideoBuffer.prototype.onBufferUpdateEnd = function (e) {
  this.emit("buffer:updateend", e);
  if (this.segment === 0) {
    this.debug("Got first segment, starting playback");
    this.debug(this);
    this.video.play();
  }
};

VideoBuffer.prototype.onVideoProgress = function (e) {
  this.debug("Video progress:", e);
  this.segment++;
  this.loadNextSegment();
}

VideoBuffer.prototype.loadNextSegment = function () {
  var url = this.url
    .replace("%segment", this.segment)
    .replace("%time", this.time)
    .replace("%preset", this.preset);

  this.debug("Loading segment:", this.segment, url);

  this.request = new XMLHttpRequest();
  this.request.responseType = "arraybuffer";
  this.request.open("GET", url, true);
  this.request.onload = this.onRequestLoad.bind(this);
  this.request.send(null);
}

VideoBuffer.prototype.onRequestLoad = function(e) {
  if (this.request.status != 200) {
    this.emit("error", new Error("Unexpected status code"));
    return false;
  }
  var ms = this.source;
  var sb = ms.sourceBuffers.length ? ms.sourceBuffers[0] : false;
  var buffer = new Uint8Array(this.request.response);

  this.debug("Got segment:", buffer.length, "bytes");

  if (buffer.length && sb) {
    try {
      this.debug("Appending segment to source buffer");
      sb.appendBuffer(buffer);
    } catch (e) {
      this.emit("error", new Error(e));
      return false;
    }
    if (this.segment === this.segments) {
      ms.endOfStream();
      return;
    }
  } else {
    this.emit("error", new Error("Empty response or missing source buffer"));
  }
}

VideoBuffer.prototype.play = function (time) {
  this.time = 0;
  this.segment = Math.floor(this.time/10);
  this.loadNextSegment();
};

VideoBuffer.prototype.debug = (function () {
  return console.log.bind(console);
}());

module.exports = VideoBuffer;

Note: The code above is loaded with browserify, which is why it's a CommonJS module.

Gist for those who prefer and/or wish to experiment.

mekwall
  • 28,614
  • 6
  • 75
  • 77
  • Found the [answer in another quesiton](http://stackoverflow.com/questions/14037112/media-source-api-not-working-for-a-custom-webm-file-chrome-version-23-0-1271-97)! – mekwall Jan 28 '14 at 16:34

1 Answers1

1

You're sure there's no error? Try getting rid of the try/catch in onRequestLoad.

I suspect what's happening is that you're trying to append to your SourceBuffer instance while its updating attribute is still true.

Every time you append to a SourceBuffer, it has to do a bit of work, and temporarily becomes unable to accept new chunks. Best way is either to use some kind of queueing mechanism, or just wait until the updateend event fires before you send the next XHR.

Kevin Ennis
  • 14,226
  • 2
  • 43
  • 44
  • Yep. I have tried without the try/catch, and it is failing on the first segment. After `updateend` the MediaSource closes and the SourceBuffer is no more. It self-destructs for some reason... – mekwall Jan 27 '14 at 15:55
  • Hmm. Any chance there's something weird with the source? I don't know a lot about WebM -- but with mp4 you need to load an initialization segment first. Maybe that's the problem? – Kevin Ennis Jan 28 '14 at 00:31
  • 1
    The source is fine (I've tried with numerous ones, even test videos that hasn't been split or manipulated with), but you might be on to something about the initialization segment. However, there's nothing specific about that in the example. – mekwall Jan 28 '14 at 13:11