mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-24 17:35:10 +03:00
d46e8fdf92
This makes a large number of small typo fixes. It also rewords a number of comments and JSDoc descriptions, and does some formatting standardization. This doesn't fix every single issue, but it fixes a lot. Notably, there were some formatting issues I declined to standardize due to ambivalence on what the proper standardization would be; for example, when and where empty lines should show up in JSDoc. Change-Id: I2bfa4d531a8fcdcf9578e9c181c4534ad7443b7a
359 lines
12 KiB
JavaScript
359 lines
12 KiB
JavaScript
/**
|
|
* @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');
|
|
|
|
// 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.
|
|
|
|
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 (navigator.vendor && navigator.vendor.indexOf('Apple') >= 0) {
|
|
let version = navigator.appVersion;
|
|
|
|
// 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 (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 if (version.indexOf('Version/11') >= 0) {
|
|
shaka.log.info('Patching Safari 11 MSE bugs.');
|
|
// Safari 11 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://goo.gl/rC3CLj
|
|
shaka.polyfill.MediaSource.stubAbort_();
|
|
// If you remove up to a keyframe, Safari 11 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://goo.gl/h2yDPN
|
|
shaka.polyfill.MediaSource.patchRemovalRange_();
|
|
}
|
|
} 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() {
|
|
const addSourceBuffer = MediaSource.prototype.addSourceBuffer;
|
|
MediaSource.prototype.addSourceBuffer = function() {
|
|
let sourceBuffer = addSourceBuffer.apply(this, arguments);
|
|
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 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() {
|
|
const 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.
|
|
let newDuration = 0;
|
|
for (let i = 0; i < this.sourceBuffers.length; ++i) {
|
|
let buffer = this.sourceBuffers[i];
|
|
let 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 (let i = 0; i < this.sourceBuffers.length; ++i) {
|
|
let buffer = this.sourceBuffers[i];
|
|
buffer.eventSuppressed_ = false;
|
|
}
|
|
}
|
|
|
|
return endOfStream.apply(this, arguments);
|
|
};
|
|
|
|
let cleanUpHandlerInstalled = false;
|
|
let addSourceBuffer = MediaSource.prototype.addSourceBuffer;
|
|
MediaSource.prototype.addSourceBuffer = function() {
|
|
// After adding a new source buffer, add an event listener to allow us to
|
|
// suppress events.
|
|
let sourceBuffer = addSourceBuffer.apply(this, arguments);
|
|
sourceBuffer['mediaSource_'] = this;
|
|
sourceBuffer.addEventListener('updateend',
|
|
shaka.polyfill.MediaSource.ignoreUpdateEnd_, false);
|
|
|
|
if (!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);
|
|
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) {
|
|
let sourceBuffer = event.target;
|
|
let mediaSource = sourceBuffer['mediaSource_'];
|
|
|
|
if (mediaSource.ignoreUpdateEnd_) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
event.stopImmediatePropagation();
|
|
sourceBuffer.eventSuppressed_ = true;
|
|
|
|
for (let i = 0; i < mediaSource.sourceBuffers.length; ++i) {
|
|
let 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) {
|
|
let mediaSource = /** @type {!MediaSource} */ (event.target);
|
|
for (let i = 0; i < mediaSource.sourceBuffers.length; ++i) {
|
|
let buffer = mediaSource.sourceBuffers[i];
|
|
buffer.removeEventListener('updateend',
|
|
shaka.polyfill.MediaSource.ignoreUpdateEnd_, false);
|
|
}
|
|
mediaSource.removeEventListener('sourceclose',
|
|
shaka.polyfill.MediaSource.cleanUpListeners_, false);
|
|
};
|
|
|
|
|
|
/**
|
|
* 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 == 'mp2t') {
|
|
return false;
|
|
}
|
|
|
|
return originalIsTypeSupported(mimeType);
|
|
};
|
|
};
|
|
|
|
|
|
/**
|
|
* Patch isTypeSupported() to parse for HDR-related clues and chain to a private
|
|
* API on the Chromecast which can query for support.
|
|
*
|
|
* @private
|
|
*/
|
|
shaka.polyfill.MediaSource.patchCastIsTypeSupported_ = function() {
|
|
const originalIsTypeSupported = MediaSource.isTypeSupported;
|
|
|
|
// Docs from Dolby detailing profile names: https://goo.gl/LVVXrS
|
|
let dolbyVisionRegex = /^dv(?:he|av)\./;
|
|
|
|
MediaSource.isTypeSupported = function(mimeType) {
|
|
// Parse the basic MIME type from its parameters.
|
|
let pieces = mimeType.split(/ *; */);
|
|
let basicMimeType = pieces[0];
|
|
|
|
// Parse the parameters into key-value pairs.
|
|
/** @type {Object.<string, string>} */
|
|
let parameters = {};
|
|
for (let i = 1; i < pieces.length; ++i) {
|
|
let kv = pieces[i].split('=');
|
|
let k = kv[0];
|
|
let v = kv[1].replace(/"(.*)"/, '$1');
|
|
parameters[k] = v;
|
|
}
|
|
|
|
let codecs = parameters['codecs'];
|
|
if (!codecs) {
|
|
return originalIsTypeSupported(mimeType);
|
|
}
|
|
|
|
let isHDR = false;
|
|
let isDolbyVision = false;
|
|
|
|
let codecList = codecs.split(',').filter(function(codec) {
|
|
// Remove Dolby Vision codec strings. These are not understood on
|
|
// Chromecast, even though the content can still be played.
|
|
if (dolbyVisionRegex.test(codec)) {
|
|
isDolbyVision = true;
|
|
// Return false to remove this string from the list.
|
|
return false;
|
|
}
|
|
|
|
// We take this string as a signal for HDR, but don't remove it.
|
|
if (/^(hev|hvc)1\.2/.test(codec)) {
|
|
isHDR = true;
|
|
}
|
|
|
|
// Keep all other strings in the list.
|
|
return true;
|
|
});
|
|
|
|
// If the content uses Dolby Vision, we take this as a sign that the content
|
|
// is not HDR after all.
|
|
if (isDolbyVision) isHDR = false;
|
|
|
|
// Reconstruct the "codecs" parameter from the results of the filter.
|
|
parameters['codecs'] = codecList.join(',');
|
|
|
|
// If the content is HDR, we add this additional parameter so that the Cast
|
|
// platform will check for HDR support.
|
|
if (isHDR) {
|
|
parameters['eotf'] = 'smpte2084';
|
|
}
|
|
|
|
// Reconstruct the MIME type, possibly with additional parameters.
|
|
let extendedMimeType = basicMimeType;
|
|
for (let k in parameters) {
|
|
let v = parameters[k];
|
|
extendedMimeType += '; ' + k + '="' + v + '"';
|
|
}
|
|
return cast.__platform__.canDisplayType(extendedMimeType);
|
|
};
|
|
};
|
|
|
|
|
|
shaka.polyfill.register(shaka.polyfill.MediaSource.install);
|