/**
* @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');
goog.require('shaka.util.MimeUtils');
goog.require('shaka.util.Platform');
/**
* @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');
// MediaSource bugs are difficult to detect without checking for the affected
// platform. SourceBuffer is not always exposed on window, for example, and
// instances are only accessible after setting up MediaSource on a video
// element. Because of this, we use UA detection and other platform detection
// tricks to decide which patches to install.
const safariVersion = shaka.util.Platform.safariVersion();
if (!window.MediaSource) {
shaka.log.info('No MSE implementation available.');
} else if (window.cast && cast.__platform__ &&
cast.__platform__.canDisplayType) {
shaka.log.info('Patching Chromecast MSE bugs.');
// Chromecast cannot make accurate determinations via isTypeSupported.
shaka.polyfill.MediaSource.patchCastIsTypeSupported_();
} else if (safariVersion) {
// TS content is broken on Safari in general.
// See https://github.com/google/shaka-player/issues/743
// and https://bugs.webkit.org/show_bug.cgi?id=165342
shaka.polyfill.MediaSource.rejectTsContent_();
if (safariVersion <= 12) {
shaka.log.info('Patching Safari 11 & 12 MSE bugs.');
// Safari 11 & 12 do not correctly implement abort() on SourceBuffer.
// Calling abort() before appending a segment causes that segment to be
// incomplete in the buffer.
// Bug filed: https://bugs.webkit.org/show_bug.cgi?id=165342
shaka.polyfill.MediaSource.stubAbort_();
// If you remove up to a keyframe, Safari 11 & 12 incorrectly will also
// remove that keyframe and the content up to the next.
// Offsetting the end of the removal range seems to help.
// Bug filed: https://bugs.webkit.org/show_bug.cgi?id=177884
shaka.polyfill.MediaSource.patchRemovalRange_();
} else {
shaka.log.info('Patching Safari 13 MSE bugs.');
// Safari 13 does not correctly implement abort() on SourceBuffer.
// Calling abort() before appending a segment causes that segment to be
// incomplete in the buffer.
// Bug filed: https://bugs.webkit.org/show_bug.cgi?id=165342
shaka.polyfill.MediaSource.stubAbort_();
}
} else if (shaka.util.Platform.isTizen2() ||
shaka.util.Platform.isTizen3() ||
shaka.util.Platform.isTizen4()) {
// Tizen's implementation of MSE does not work well with opus. To prevent
// the player from trying to play opus on Tizen, we will override media
// source to always reject opus content.
shaka.polyfill.MediaSource.rejectCodec_('opus');
} else {
shaka.log.info('Using native MSE as-is.');
}
};
/**
* Blacklist the current browser by removing media source. A side-effect of this
* will be to make |shaka.util.Platform.supportsMediaSource| return |false|.
*
* @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() {
const addSourceBuffer = MediaSource.prototype.addSourceBuffer;
MediaSource.prototype.addSourceBuffer = function(...varArgs) {
let sourceBuffer = addSourceBuffer.apply(this, varArgs);
sourceBuffer.abort = function() {}; // Stub out for buggy implementations.
return sourceBuffer;
};
};
/**
* Patch remove(). On Safari 11, if you call remove() to remove the content up
* to a keyframe, Safari will also remove the keyframe and all of the data up to
* the next one. For example, if the keyframes are at 0s, 5s, and 10s, and you
* tried to remove 0s-5s, it would instead remove 0s-10s.
*
* Offsetting the end of the range seems to be a usable workaround.
*
* @private
*/
shaka.polyfill.MediaSource.patchRemovalRange_ = function() {
const originalRemove = SourceBuffer.prototype.remove;
SourceBuffer.prototype.remove = function(startTime, endTime) {
return originalRemove.call(this, startTime, endTime - 0.001);
};
};
/**
* Patch isTypeSupported() to reject TS content. Used to avoid TS-related MSE
* bugs on Safari.
*
* @private
*/
shaka.polyfill.MediaSource.rejectTsContent_ = function() {
const originalIsTypeSupported = MediaSource.isTypeSupported;
MediaSource.isTypeSupported = function(mimeType) {
// Parse the basic MIME type from its parameters.
let pieces = mimeType.split(/ *; */);
let basicMimeType = pieces[0];
let container = basicMimeType.split('/')[1];
if (container.toLowerCase() == 'mp2t') {
return false;
}
return originalIsTypeSupported(mimeType);
};
};
/**
* Patch |MediaSource.isTypeSupported| to always reject |codec|. This is used
* when we know that we are on a platform that does not work well with a given
* codec.
*
* @param {string} codec
* @private
*/
shaka.polyfill.MediaSource.rejectCodec_ = function(codec) {
const isTypeSupported = MediaSource.isTypeSupported;
MediaSource.isTypeSupported = (mimeType) => {
const actualCodec = shaka.util.MimeUtils.getCodecBase(mimeType);
return actualCodec != codec && isTypeSupported(mimeType);
};
};
/**
* Patch isTypeSupported() to chain to a private API on the Chromecast which
* can query for support of detailed content parameters.
*
* @private
*/
shaka.polyfill.MediaSource.patchCastIsTypeSupported_ = function() {
const originalIsTypeSupported = MediaSource.isTypeSupported;
MediaSource.isTypeSupported = function(mimeType) {
// Parse the basic MIME type from its parameters.
let pieces = mimeType.split(/ *; */);
pieces.shift(); // Remove basic MIME type from pieces.
const hasCodecs = pieces.some((piece) => piece.startsWith('codecs='));
if (!hasCodecs) {
// Though the original reason for this special case was not documented,
// it is presumed to be because the platform won't accept a MIME type
// without codecs in canDisplayType. It is valid, however, in
// isTypeSupported.
return originalIsTypeSupported(mimeType);
}
// Only canDisplayType can check extended MIME type parameters on this
// platform (such as frame rate, resolution, etc).
// In previous versions of this polyfill, the MIME type parameters were
// manipulated, filtered, or extended. This is no longer true, so we pass
// the full MIME type to the platform as we received it.
return cast.__platform__.canDisplayType(mimeType);
};
};
shaka.polyfill.register(shaka.polyfill.MediaSource.install);