Source: lib/polyfill/mediasource.js

/**
 * @license
 * Copyright 2016 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

goog.provide('shaka.polyfill.MediaSource');

goog.require('shaka.log');
goog.require('shaka.polyfill.register');


/**
 * @namespace shaka.polyfill.MediaSource
 *
 * @summary A polyfill to patch MSE bugs.
 */


/**
 * Install the polyfill if needed.
 */
shaka.polyfill.MediaSource.install = function() {
  shaka.log.debug('MediaSource.install');

  if (!window.MediaSource) {
    shaka.log.info('No MSE implementation available.');
    return;
  }

  // Detection is complicated by the fact that Safari does not expose
  // SourceBuffer on window.  So we can't detect missing features by accessing
  // SourceBuffer.prototype.  That is why we use navigator to detect Safari and
  // particular versions of it.
  var vendor = navigator.vendor;
  var version = navigator.appVersion;
  if (!vendor || !version || vendor.indexOf('Apple') < 0) {
    shaka.log.info('Using native MSE as-is.');
    return;
  }

  if (version.indexOf('Version/8') >= 0) {
    // Safari 8 does not implement appendWindowEnd.  If we ignore the
    // incomplete MSE implementation, some content (especially multi-period)
    // will fail to play correctly.  The best we can do is blacklist Safari 8.
    shaka.log.info('Blacklisting Safari 8 MSE.');
    shaka.polyfill.MediaSource.blacklist_();
  } else if (version.indexOf('Version/9') >= 0) {
    shaka.log.info('Patching Safari 9 MSE bugs.');
    // Safari 9 does not correctly implement abort() on SourceBuffer.
    // Calling abort() causes a decoder failure, rather than resetting the
    // decode timestamp as called for by the spec.
    // Bug filed: http://goo.gl/UZ2rPp
    shaka.polyfill.MediaSource.stubAbort_();
  } else if (version.indexOf('Version/10') >= 0) {
    shaka.log.info('Patching Safari 10 MSE bugs.');
    // Safari 10 does not correctly implement abort() on SourceBuffer.
    // Calling abort() before appending a segment causes that segment to be
    // incomplete in buffer.
    // Bug filed: https://goo.gl/rC3CLj
    shaka.polyfill.MediaSource.stubAbort_();
    // Safari 10 fires spurious 'updateend' events after endOfStream().
    // Bug filed: https://goo.gl/qCeTZr
    shaka.polyfill.MediaSource.patchEndOfStreamEvents_();
  } else {
    shaka.log.info('Using native MSE as-is.');
  }
};


/**
 * Blacklist the current browser by making MediaSourceEngine.isBrowserSupported
 * fail later.
 *
 * @private
 */
shaka.polyfill.MediaSource.blacklist_ = function() {
  window['MediaSource'] = null;
};


/**
 * Stub out abort().  On some buggy MSE implementations, calling abort() causes
 * various problems.
 *
 * @private
 */
shaka.polyfill.MediaSource.stubAbort_ = function() {
  var addSourceBuffer = MediaSource.prototype.addSourceBuffer;
  MediaSource.prototype.addSourceBuffer = function() {
    var sourceBuffer = addSourceBuffer.apply(this, arguments);
    sourceBuffer.abort = function() {};  // Stub out for buggy implementations.
    return sourceBuffer;
  };
};


/**
 * Patch endOfStream() to get rid of 'updateend' events that should not fire.
 * These extra events confuse MediaSourceEngine, which relies on correct events
 * to manage SourceBuffer state.
 *
 * @private
 */
shaka.polyfill.MediaSource.patchEndOfStreamEvents_ = function() {
  var endOfStream = MediaSource.prototype.endOfStream;
  MediaSource.prototype.endOfStream = function() {
    // This bug manifests only when endOfStream() results in the truncation
    // of the MediaSource's duration.  So first we must calculate what the
    // new duration will be.
    var newDuration = 0;
    for (var i = 0; i < this.sourceBuffers.length; ++i) {
      var buffer = this.sourceBuffers[i];
      var bufferEnd = buffer.buffered.end(buffer.buffered.length - 1);
      newDuration = Math.max(newDuration, bufferEnd);
    }

    // If the duration is going to be reduced, suppress the next 'updateend'
    // event on each SourceBuffer.
    if (!isNaN(this.duration) &&
        newDuration < this.duration) {
      this.ignoreUpdateEnd_ = true;
      for (var i = 0; i < this.sourceBuffers.length; ++i) {
        var buffer = this.sourceBuffers[i];
        buffer.eventSuppressed_ = false;
      }
    }

    return endOfStream.apply(this, arguments);
  };

  var addSourceBuffer = MediaSource.prototype.addSourceBuffer;
  MediaSource.prototype.addSourceBuffer = function() {
    // After adding a new source buffer, add an event listener to allow us to
    // suppress events.
    var sourceBuffer = addSourceBuffer.apply(this, arguments);
    sourceBuffer.mediaSource_ = this;
    sourceBuffer.addEventListener('updateend',
        shaka.polyfill.MediaSource.ignoreUpdateEnd_, false);

    if (!this.cleanUpHandlerInstalled_) {
      // If we haven't already, install an event listener to allow us to clean
      // up listeners when MediaSource is torn down.
      this.addEventListener('sourceclose',
          shaka.polyfill.MediaSource.cleanUpListeners_, false);
      this.cleanUpHandlerInstalled_ = true;
    }
    return sourceBuffer;
  };
};


/**
 * An event listener for 'updateend' which selectively suppresses the events.
 *
 * @see shaka.polyfill.MediaSource.patchEndOfStreamEvents_
 *
 * @param {Event} event
 * @private
 */
shaka.polyfill.MediaSource.ignoreUpdateEnd_ = function(event) {
  var sourceBuffer = event.target;
  var mediaSource = sourceBuffer.mediaSource_;

  if (mediaSource.ignoreUpdateEnd_) {
    event.preventDefault();
    event.stopPropagation();
    event.stopImmediatePropagation();
    sourceBuffer.eventSuppressed_ = true;

    for (var i = 0; i < mediaSource.sourceBuffers.length; ++i) {
      var buffer = mediaSource.sourceBuffers[i];
      if (buffer.eventSuppressed_ == false) {
        // More events need to be suppressed.
        return;
      }
    }

    // All events have been suppressed, all buffers are out of 'updating'
    // mode.  Stop suppressing events.
    mediaSource.ignoreUpdateEnd_ = false;
  }
};


/**
 * An event listener for 'sourceclose' which cleans up listeners for 'updateend'
 * to avoid memory leaks.
 *
 * @see shaka.polyfill.MediaSource.patchEndOfStreamEvents_
 * @see shaka.polyfill.MediaSource.ignoreUpdateEnd_
 *
 * @param {Event} event
 * @private
 */
shaka.polyfill.MediaSource.cleanUpListeners_ = function(event) {
  var mediaSource = event.target;
  for (var i = 0; i < mediaSource.sourceBuffers.length; ++i) {
    var buffer = mediaSource.sourceBuffers[i];
    buffer.removeEventListener('updateend',
        shaka.polyfill.MediaSource.ignoreUpdateEnd_, false);
  }
  mediaSource.removeEventListener('sourceclose',
      shaka.polyfill.MediaSource.cleanUpListeners_, false);
};


shaka.polyfill.register(shaka.polyfill.MediaSource.install);