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