Source: lib/polyfill/mediasource.js

  1. /**
  2. * @license
  3. * Copyright 2016 Google Inc.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. goog.provide('shaka.polyfill.MediaSource');
  18. goog.require('shaka.log');
  19. goog.require('shaka.polyfill.register');
  20. /**
  21. * @namespace shaka.polyfill.MediaSource
  22. *
  23. * @summary A polyfill to patch MSE bugs.
  24. */
  25. /**
  26. * Install the polyfill if needed.
  27. */
  28. shaka.polyfill.MediaSource.install = function() {
  29. shaka.log.debug('MediaSource.install');
  30. // MediaSource bugs are difficult to detect without checking for the affected
  31. // platform. SourceBuffer is not always exposed on window, for example, and
  32. // instances are only accessible after setting up MediaSource on a video
  33. // element. Because of this, we use UA detection and other platform detection
  34. // tricks to decide which patches to install.
  35. if (!window.MediaSource) {
  36. shaka.log.info('No MSE implementation available.');
  37. } else if (window.cast && cast.__platform__ &&
  38. cast.__platform__.canDisplayType) {
  39. shaka.log.info('Patching Chromecast MSE bugs.');
  40. // Chromecast cannot make accurate determinations via isTypeSupported.
  41. shaka.polyfill.MediaSource.patchCastIsTypeSupported_();
  42. } else if (navigator.vendor && navigator.vendor.indexOf('Apple') >= 0) {
  43. var version = navigator.appVersion;
  44. if (version.indexOf('Version/8') >= 0) {
  45. // Safari 8 does not implement appendWindowEnd. If we ignore the
  46. // incomplete MSE implementation, some content (especially multi-period)
  47. // will fail to play correctly. The best we can do is blacklist Safari 8.
  48. shaka.log.info('Blacklisting Safari 8 MSE.');
  49. shaka.polyfill.MediaSource.blacklist_();
  50. } else if (version.indexOf('Version/9') >= 0) {
  51. shaka.log.info('Patching Safari 9 MSE bugs.');
  52. // Safari 9 does not correctly implement abort() on SourceBuffer.
  53. // Calling abort() causes a decoder failure, rather than resetting the
  54. // decode timestamp as called for by the spec.
  55. // Bug filed: http://goo.gl/UZ2rPp
  56. shaka.polyfill.MediaSource.stubAbort_();
  57. } else if (version.indexOf('Version/10') >= 0) {
  58. shaka.log.info('Patching Safari 10 MSE bugs.');
  59. // Safari 10 does not correctly implement abort() on SourceBuffer.
  60. // Calling abort() before appending a segment causes that segment to be
  61. // incomplete in buffer.
  62. // Bug filed: https://goo.gl/rC3CLj
  63. shaka.polyfill.MediaSource.stubAbort_();
  64. // Safari 10 fires spurious 'updateend' events after endOfStream().
  65. // Bug filed: https://goo.gl/qCeTZr
  66. shaka.polyfill.MediaSource.patchEndOfStreamEvents_();
  67. }
  68. } else {
  69. shaka.log.info('Using native MSE as-is.');
  70. }
  71. };
  72. /**
  73. * Blacklist the current browser by making MediaSourceEngine.isBrowserSupported
  74. * fail later.
  75. *
  76. * @private
  77. */
  78. shaka.polyfill.MediaSource.blacklist_ = function() {
  79. window['MediaSource'] = null;
  80. };
  81. /**
  82. * Stub out abort(). On some buggy MSE implementations, calling abort() causes
  83. * various problems.
  84. *
  85. * @private
  86. */
  87. shaka.polyfill.MediaSource.stubAbort_ = function() {
  88. var addSourceBuffer = MediaSource.prototype.addSourceBuffer;
  89. MediaSource.prototype.addSourceBuffer = function() {
  90. var sourceBuffer = addSourceBuffer.apply(this, arguments);
  91. sourceBuffer.abort = function() {}; // Stub out for buggy implementations.
  92. return sourceBuffer;
  93. };
  94. };
  95. /**
  96. * Patch endOfStream() to get rid of 'updateend' events that should not fire.
  97. * These extra events confuse MediaSourceEngine, which relies on correct events
  98. * to manage SourceBuffer state.
  99. *
  100. * @private
  101. */
  102. shaka.polyfill.MediaSource.patchEndOfStreamEvents_ = function() {
  103. var endOfStream = MediaSource.prototype.endOfStream;
  104. MediaSource.prototype.endOfStream = function() {
  105. // This bug manifests only when endOfStream() results in the truncation
  106. // of the MediaSource's duration. So first we must calculate what the
  107. // new duration will be.
  108. var newDuration = 0;
  109. for (var i = 0; i < this.sourceBuffers.length; ++i) {
  110. var buffer = this.sourceBuffers[i];
  111. var bufferEnd = buffer.buffered.end(buffer.buffered.length - 1);
  112. newDuration = Math.max(newDuration, bufferEnd);
  113. }
  114. // If the duration is going to be reduced, suppress the next 'updateend'
  115. // event on each SourceBuffer.
  116. if (!isNaN(this.duration) &&
  117. newDuration < this.duration) {
  118. this.ignoreUpdateEnd_ = true;
  119. for (var i = 0; i < this.sourceBuffers.length; ++i) {
  120. var buffer = this.sourceBuffers[i];
  121. buffer.eventSuppressed_ = false;
  122. }
  123. }
  124. return endOfStream.apply(this, arguments);
  125. };
  126. var cleanUpHandlerInstalled = false;
  127. var addSourceBuffer = MediaSource.prototype.addSourceBuffer;
  128. MediaSource.prototype.addSourceBuffer = function() {
  129. // After adding a new source buffer, add an event listener to allow us to
  130. // suppress events.
  131. var sourceBuffer = addSourceBuffer.apply(this, arguments);
  132. sourceBuffer['mediaSource_'] = this;
  133. sourceBuffer.addEventListener('updateend',
  134. shaka.polyfill.MediaSource.ignoreUpdateEnd_, false);
  135. if (!cleanUpHandlerInstalled) {
  136. // If we haven't already, install an event listener to allow us to clean
  137. // up listeners when MediaSource is torn down.
  138. this.addEventListener('sourceclose',
  139. shaka.polyfill.MediaSource.cleanUpListeners_, false);
  140. cleanUpHandlerInstalled = true;
  141. }
  142. return sourceBuffer;
  143. };
  144. };
  145. /**
  146. * An event listener for 'updateend' which selectively suppresses the events.
  147. *
  148. * @see shaka.polyfill.MediaSource.patchEndOfStreamEvents_
  149. *
  150. * @param {Event} event
  151. * @private
  152. */
  153. shaka.polyfill.MediaSource.ignoreUpdateEnd_ = function(event) {
  154. var sourceBuffer = event.target;
  155. var mediaSource = sourceBuffer['mediaSource_'];
  156. if (mediaSource.ignoreUpdateEnd_) {
  157. event.preventDefault();
  158. event.stopPropagation();
  159. event.stopImmediatePropagation();
  160. sourceBuffer.eventSuppressed_ = true;
  161. for (var i = 0; i < mediaSource.sourceBuffers.length; ++i) {
  162. var buffer = mediaSource.sourceBuffers[i];
  163. if (buffer.eventSuppressed_ == false) {
  164. // More events need to be suppressed.
  165. return;
  166. }
  167. }
  168. // All events have been suppressed, all buffers are out of 'updating'
  169. // mode. Stop suppressing events.
  170. mediaSource.ignoreUpdateEnd_ = false;
  171. }
  172. };
  173. /**
  174. * An event listener for 'sourceclose' which cleans up listeners for 'updateend'
  175. * to avoid memory leaks.
  176. *
  177. * @see shaka.polyfill.MediaSource.patchEndOfStreamEvents_
  178. * @see shaka.polyfill.MediaSource.ignoreUpdateEnd_
  179. *
  180. * @param {Event} event
  181. * @private
  182. */
  183. shaka.polyfill.MediaSource.cleanUpListeners_ = function(event) {
  184. var mediaSource = /** @type {!MediaSource} */ (event.target);
  185. for (var i = 0; i < mediaSource.sourceBuffers.length; ++i) {
  186. var buffer = mediaSource.sourceBuffers[i];
  187. buffer.removeEventListener('updateend',
  188. shaka.polyfill.MediaSource.ignoreUpdateEnd_, false);
  189. }
  190. mediaSource.removeEventListener('sourceclose',
  191. shaka.polyfill.MediaSource.cleanUpListeners_, false);
  192. };
  193. /**
  194. * Patch isTypeSupported() to parse for HDR-related clues and chain to a private
  195. * API on the Chromecast which can query for support.
  196. *
  197. * @private
  198. */
  199. shaka.polyfill.MediaSource.patchCastIsTypeSupported_ = function() {
  200. var originalIsTypeSupported = MediaSource.isTypeSupported;
  201. // Docs from Dolby detailing profile names: https://goo.gl/LVVXrS
  202. var dolbyVisionRegex = /^dv(?:he|av)\./;
  203. MediaSource.isTypeSupported = function(mimeType) {
  204. // Parse the basic MIME type from its parameters.
  205. var pieces = mimeType.split(/ *; */);
  206. var basicMimeType = pieces[0];
  207. // Parse the parameters into key-value pairs.
  208. /** @type {Object.<string, string>} */
  209. var parameters = {};
  210. for (var i = 1; i < pieces.length; ++i) {
  211. var kv = pieces[i].split('=');
  212. var k = kv[0];
  213. var v = kv[1].replace(/"(.*)"/, '$1');
  214. parameters[k] = v;
  215. }
  216. var codecs = parameters['codecs'];
  217. if (!codecs) {
  218. return originalIsTypeSupported(mimeType);
  219. }
  220. var isHDR = false;
  221. var isDolbyVision = false;
  222. var codecList = codecs.split(',').filter(function(codec) {
  223. // Remove Dolby Vision codec strings. These are not understood on
  224. // Chromecast, even though the content can still be played.
  225. if (dolbyVisionRegex.test(codec)) {
  226. isDolbyVision = true;
  227. // Return false to remove this string from the list.
  228. return false;
  229. }
  230. // We take this string as a signal for HDR, but don't remove it.
  231. if (/^(hev|hvc)1\.2/.test(codec)) {
  232. isHDR = true;
  233. }
  234. // Keep all other strings in the list.
  235. return true;
  236. });
  237. // If the content uses Dolby Vision, we take this as a sign that the content
  238. // is not HDR after all.
  239. if (isDolbyVision) isHDR = false;
  240. // Reconstruct the "codecs" parameter from the results of the filter.
  241. parameters['codecs'] = codecList.join(',');
  242. // If the content is HDR, we add this additional parameter so that the Cast
  243. // platform will check for HDR support.
  244. if (isHDR) {
  245. parameters['eotf'] = 'smpte2084';
  246. }
  247. // Reconstruct the MIME type, possibly with additional parameters.
  248. var extendedMimeType = basicMimeType;
  249. for (var k in parameters) {
  250. var v = parameters[k];
  251. extendedMimeType += '; ' + k + '="' + v + '"';
  252. }
  253. return cast.__platform__.canDisplayType(extendedMimeType);
  254. };
  255. };
  256. shaka.polyfill.register(shaka.polyfill.MediaSource.install);