Source: lib/polyfill/mediasource.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.polyfill.MediaSource');
  7. goog.require('shaka.log');
  8. goog.require('shaka.polyfill');
  9. goog.require('shaka.util.MimeUtils');
  10. goog.require('shaka.util.Platform');
  11. /**
  12. * @summary A polyfill to patch MSE bugs.
  13. * @export
  14. */
  15. shaka.polyfill.MediaSource = class {
  16. /**
  17. * Install the polyfill if needed.
  18. * @export
  19. */
  20. static install() {
  21. shaka.log.debug('MediaSource.install');
  22. // MediaSource bugs are difficult to detect without checking for the
  23. // affected platform. SourceBuffer is not always exposed on window, for
  24. // example, and instances are only accessible after setting up MediaSource
  25. // on a video element. Because of this, we use UA detection and other
  26. // platform detection tricks to decide which patches to install.
  27. const safariVersion = shaka.util.Platform.safariVersion();
  28. if (!window.MediaSource) {
  29. shaka.log.info('No MSE implementation available.');
  30. } else if (window.cast && cast.__platform__ &&
  31. cast.__platform__.canDisplayType) {
  32. shaka.log.info('Patching Chromecast MSE bugs.');
  33. // Chromecast cannot make accurate determinations via isTypeSupported.
  34. shaka.polyfill.MediaSource.patchCastIsTypeSupported_();
  35. } else if (safariVersion) {
  36. // NOTE: shaka.Player.isBrowserSupported() has its own restrictions on
  37. // Safari version.
  38. if (safariVersion <= 12) {
  39. shaka.log.info('Patching Safari 11 & 12 MSE bugs.');
  40. // Safari 11 & 12 do not correctly implement abort() on SourceBuffer.
  41. // Calling abort() before appending a segment causes that segment to be
  42. // incomplete in the buffer.
  43. // Bug filed: https://bugs.webkit.org/show_bug.cgi?id=165342
  44. shaka.polyfill.MediaSource.stubAbort_();
  45. // If you remove up to a keyframe, Safari 11 & 12 incorrectly will also
  46. // remove that keyframe and the content up to the next.
  47. // Offsetting the end of the removal range seems to help.
  48. // Bug filed: https://bugs.webkit.org/show_bug.cgi?id=177884
  49. shaka.polyfill.MediaSource.patchRemovalRange_();
  50. } else if (safariVersion <= 15) {
  51. shaka.log.info('Patching Safari 13 & 14 & 15 MSE bugs.');
  52. // Safari 13 does not correctly implement abort() on SourceBuffer.
  53. // Calling abort() before appending a segment causes that segment to be
  54. // incomplete in the buffer.
  55. // Bug filed: https://bugs.webkit.org/show_bug.cgi?id=165342
  56. shaka.polyfill.MediaSource.stubAbort_();
  57. }
  58. } else if (shaka.util.Platform.isEdge()) {
  59. shaka.log.info('Rejecting TS container.');
  60. // TS content is broken on Edge in general.
  61. // See https://github.com/shaka-project/shaka-player/issues/4955
  62. shaka.polyfill.MediaSource.rejectContainer_('mp2t');
  63. } else if (shaka.util.Platform.isTizen2() ||
  64. shaka.util.Platform.isTizen3() ||
  65. shaka.util.Platform.isTizen4()) {
  66. shaka.log.info('Rejecting Opus.');
  67. // Tizen's implementation of MSE does not work well with opus. To prevent
  68. // the player from trying to play opus on Tizen, we will override media
  69. // source to always reject opus content.
  70. shaka.polyfill.MediaSource.rejectCodec_('opus');
  71. } else {
  72. shaka.log.info('Using native MSE as-is.');
  73. }
  74. if (window.MediaSource &&
  75. MediaSource.isTypeSupported('video/webm; codecs="vp9"') &&
  76. !MediaSource.isTypeSupported('video/webm; codecs="vp09.00.10.08"')) {
  77. shaka.log.info('Patching vp09 support queries.');
  78. // Only the old, deprecated style of VP9 codec strings is supported.
  79. // This occurs on older smart TVs.
  80. // Patch isTypeSupported to translate the new strings into the old one.
  81. shaka.polyfill.MediaSource.patchVp09_();
  82. }
  83. }
  84. /**
  85. * Stub out abort(). On some buggy MSE implementations, calling abort()
  86. * causes various problems.
  87. *
  88. * @private
  89. */
  90. static stubAbort_() {
  91. /* eslint-disable no-restricted-syntax */
  92. const addSourceBuffer = MediaSource.prototype.addSourceBuffer;
  93. MediaSource.prototype.addSourceBuffer = function(...varArgs) {
  94. const sourceBuffer = addSourceBuffer.apply(this, varArgs);
  95. sourceBuffer.abort = function() {}; // Stub out for buggy implementations.
  96. return sourceBuffer;
  97. };
  98. /* eslint-enable no-restricted-syntax */
  99. }
  100. /**
  101. * Patch remove(). On Safari 11, if you call remove() to remove the content
  102. * up to a keyframe, Safari will also remove the keyframe and all of the data
  103. * up to the next one. For example, if the keyframes are at 0s, 5s, and 10s,
  104. * and you tried to remove 0s-5s, it would instead remove 0s-10s.
  105. *
  106. * Offsetting the end of the range seems to be a usable workaround.
  107. *
  108. * @private
  109. */
  110. static patchRemovalRange_() {
  111. // eslint-disable-next-line no-restricted-syntax
  112. const originalRemove = SourceBuffer.prototype.remove;
  113. // eslint-disable-next-line no-restricted-syntax
  114. SourceBuffer.prototype.remove = function(startTime, endTime) {
  115. // eslint-disable-next-line no-restricted-syntax
  116. return originalRemove.call(this, startTime, endTime - 0.001);
  117. };
  118. }
  119. /**
  120. * Patch |MediaSource.isTypeSupported| to always reject |container|. This is
  121. * used when we know that we are on a platform that does not work well with
  122. * a given container.
  123. *
  124. * @param {string} container
  125. * @private
  126. */
  127. static rejectContainer_(container) {
  128. const isTypeSupported = MediaSource.isTypeSupported;
  129. MediaSource.isTypeSupported = (mimeType) => {
  130. const actualContainer = shaka.util.MimeUtils.getContainerType(mimeType);
  131. return actualContainer != container && isTypeSupported(mimeType);
  132. };
  133. }
  134. /**
  135. * Patch |MediaSource.isTypeSupported| to always reject |codec|. This is used
  136. * when we know that we are on a platform that does not work well with a given
  137. * codec.
  138. *
  139. * @param {string} codec
  140. * @private
  141. */
  142. static rejectCodec_(codec) {
  143. const isTypeSupported = MediaSource.isTypeSupported;
  144. MediaSource.isTypeSupported = (mimeType) => {
  145. const actualCodec = shaka.util.MimeUtils.getCodecBase(mimeType);
  146. return actualCodec != codec && isTypeSupported(mimeType);
  147. };
  148. }
  149. /**
  150. * Patch isTypeSupported() to chain to a private API on the Chromecast which
  151. * can query for support of detailed content parameters.
  152. *
  153. * @private
  154. */
  155. static patchCastIsTypeSupported_() {
  156. const originalIsTypeSupported = MediaSource.isTypeSupported;
  157. MediaSource.isTypeSupported = (mimeType) => {
  158. // Parse the basic MIME type from its parameters.
  159. const pieces = mimeType.split(/ *; */);
  160. pieces.shift(); // Remove basic MIME type from pieces.
  161. const hasCodecs = pieces.some((piece) => piece.startsWith('codecs='));
  162. if (!hasCodecs) {
  163. // Though the original reason for this special case was not documented,
  164. // it is presumed to be because the platform won't accept a MIME type
  165. // without codecs in canDisplayType. It is valid, however, in
  166. // isTypeSupported.
  167. return originalIsTypeSupported(mimeType);
  168. }
  169. // Only canDisplayType can check extended MIME type parameters on this
  170. // platform (such as frame rate, resolution, etc).
  171. // In previous versions of this polyfill, the MIME type parameters were
  172. // manipulated, filtered, or extended. This is no longer true, so we pass
  173. // the full MIME type to the platform as we received it.
  174. return cast.__platform__.canDisplayType(mimeType);
  175. };
  176. }
  177. /**
  178. * Patch isTypeSupported() to translate vp09 codec strings into vp9, to allow
  179. * such content to play on older smart TVs.
  180. *
  181. * @private
  182. */
  183. static patchVp09_() {
  184. const originalIsTypeSupported = MediaSource.isTypeSupported;
  185. if (shaka.util.Platform.isWebOS()) {
  186. // Don't do this on LG webOS as otherwise it is unable
  187. // to play vp09 at all.
  188. return;
  189. }
  190. MediaSource.isTypeSupported = (mimeType) => {
  191. // Split the MIME type into its various parameters.
  192. const pieces = mimeType.split(/ *; */);
  193. const codecsIndex =
  194. pieces.findIndex((piece) => piece.startsWith('codecs='));
  195. if (codecsIndex < 0) {
  196. // No codec? Call the original without modifying the MIME type.
  197. return originalIsTypeSupported(mimeType);
  198. }
  199. const codecsParam = pieces[codecsIndex];
  200. const codecs = codecsParam
  201. .replace('codecs=', '').replace(/"/g, '').split(/\s*,\s*/);
  202. const vp09Index = codecs.findIndex(
  203. (codecName) => codecName.startsWith('vp09'));
  204. if (vp09Index >= 0) {
  205. // vp09? Replace it with vp9.
  206. codecs[vp09Index] = 'vp9';
  207. pieces[codecsIndex] = 'codecs="' + codecs.join(',') + '"';
  208. mimeType = pieces.join('; ');
  209. }
  210. return originalIsTypeSupported(mimeType);
  211. };
  212. }
  213. };
  214. shaka.polyfill.register(shaka.polyfill.MediaSource.install);