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 && !window.ManagedMediaSource) {
  29. shaka.log.info('No MSE implementation available.');
  30. } else if (safariVersion && window.MediaSource) {
  31. // NOTE: shaka.Player.isBrowserSupported() has its own restrictions on
  32. // Safari version.
  33. if (safariVersion <= 10) {
  34. // Safari 8 does not implement appendWindowEnd.
  35. // Safari 9 & 10 do not correctly implement abort() on SourceBuffer.
  36. // Bug filed: https://bugs.webkit.org/show_bug.cgi?id=160316
  37. // Blacklist these very outdated versions.
  38. // Bug filed: https://bugs.webkit.org/show_bug.cgi?id=165342
  39. // Safari 10 fires spurious 'updateend' events after endOfStream().
  40. // Bug filed: https://bugs.webkit.org/show_bug.cgi?id=165336
  41. shaka.log.info('Blacklisting MSE on Safari <= 10.');
  42. shaka.polyfill.MediaSource.blacklist_();
  43. } else if (safariVersion <= 12) {
  44. shaka.log.info('Patching Safari 11 & 12 MSE bugs.');
  45. // Safari 11 & 12 do not correctly implement abort() on SourceBuffer.
  46. // Calling abort() before appending a segment causes that segment to be
  47. // incomplete in the buffer.
  48. // Bug filed: https://bugs.webkit.org/show_bug.cgi?id=165342
  49. shaka.polyfill.MediaSource.stubAbort_();
  50. // If you remove up to a keyframe, Safari 11 & 12 incorrectly will also
  51. // remove that keyframe and the content up to the next.
  52. // Offsetting the end of the removal range seems to help.
  53. // Bug filed: https://bugs.webkit.org/show_bug.cgi?id=177884
  54. shaka.polyfill.MediaSource.patchRemovalRange_();
  55. } else if (safariVersion <= 15) {
  56. shaka.log.info('Patching Safari 13 & 14 & 15 MSE bugs.');
  57. // Safari 13 does not correctly implement abort() on SourceBuffer.
  58. // Calling abort() before appending a segment causes that segment to be
  59. // incomplete in the buffer.
  60. // Bug filed: https://bugs.webkit.org/show_bug.cgi?id=165342
  61. shaka.polyfill.MediaSource.stubAbort_();
  62. }
  63. } else if (shaka.util.Platform.isZenterio()) {
  64. // Zenterio uses WPE based on Webkit 607.x.x which do not correctly
  65. // implement abort() on SourceBuffer.
  66. // Calling abort() before appending a segment causes that segment to be
  67. // incomplete in the buffer.
  68. // Bug filed: https://bugs.webkit.org/show_bug.cgi?id=165342
  69. shaka.polyfill.MediaSource.stubAbort_();
  70. // If you remove up to a keyframe, Webkit 607.x.x incorrectly will also
  71. // remove that keyframe and the content up to the next.
  72. // Offsetting the end of the removal range seems to help.
  73. // Bug filed: https://bugs.webkit.org/show_bug.cgi?id=177884
  74. shaka.polyfill.MediaSource.patchRemovalRange_();
  75. } else if (shaka.util.Platform.isTizen2() ||
  76. shaka.util.Platform.isTizen3() ||
  77. shaka.util.Platform.isTizen4()) {
  78. shaka.log.info('Rejecting Opus.');
  79. // Tizen's implementation of MSE does not work well with opus. To prevent
  80. // the player from trying to play opus on Tizen, we will override media
  81. // source to always reject opus content.
  82. shaka.polyfill.MediaSource.rejectCodec_('opus');
  83. } else if (shaka.util.Platform.isComcastX1()) {
  84. // XOne look to be like safari 8 which do not correctly
  85. // implement abort() on SourceBuffer.
  86. // Calling abort() before appending a segment causes that segment to be
  87. // incomplete in the buffer.
  88. // Bug filed: https://bugs.webkit.org/show_bug.cgi?id=165342
  89. shaka.polyfill.MediaSource.stubAbort_();
  90. // If you remove up to a keyframe, Webkit 601.x.x incorrectly will also
  91. // remove that keyframe and the content up to the next.
  92. // Offsetting the end of the removal range seems to help.
  93. // Bug filed: https://bugs.webkit.org/show_bug.cgi?id=177884
  94. shaka.polyfill.MediaSource.patchRemovalRange_();
  95. } else {
  96. shaka.log.info('Using native MSE as-is.');
  97. }
  98. if (window.MediaSource || window.ManagedMediaSource) {
  99. // TS content is broken on all browsers in general.
  100. // See https://github.com/shaka-project/shaka-player/issues/4955
  101. // See https://github.com/shaka-project/shaka-player/issues/5278
  102. // See https://github.com/shaka-project/shaka-player/issues/6334
  103. shaka.polyfill.MediaSource.rejectContainer_('mp2t');
  104. }
  105. if (window.MediaSource &&
  106. MediaSource.isTypeSupported('video/webm; codecs="vp9"') &&
  107. !MediaSource.isTypeSupported('video/webm; codecs="vp09.00.10.08"')) {
  108. shaka.log.info('Patching vp09 support queries.');
  109. // Only the old, deprecated style of VP9 codec strings is supported.
  110. // This occurs on older smart TVs.
  111. // Patch isTypeSupported to translate the new strings into the old one.
  112. shaka.polyfill.MediaSource.patchVp09_();
  113. }
  114. }
  115. /**
  116. * Blacklist the current browser by making
  117. * MediaSourceEngine.isBrowserSupported fail later.
  118. *
  119. * @private
  120. */
  121. static blacklist_() {
  122. window['MediaSource'] = null;
  123. }
  124. /**
  125. * Stub out abort(). On some buggy MSE implementations, calling abort()
  126. * causes various problems.
  127. *
  128. * @private
  129. */
  130. static stubAbort_() {
  131. /* eslint-disable no-restricted-syntax */
  132. const addSourceBuffer = MediaSource.prototype.addSourceBuffer;
  133. MediaSource.prototype.addSourceBuffer = function(...varArgs) {
  134. const sourceBuffer = addSourceBuffer.apply(this, varArgs);
  135. sourceBuffer.abort = function() {}; // Stub out for buggy implementations.
  136. return sourceBuffer;
  137. };
  138. /* eslint-enable no-restricted-syntax */
  139. }
  140. /**
  141. * Patch remove(). On Safari 11, if you call remove() to remove the content
  142. * up to a keyframe, Safari will also remove the keyframe and all of the data
  143. * up to the next one. For example, if the keyframes are at 0s, 5s, and 10s,
  144. * and you tried to remove 0s-5s, it would instead remove 0s-10s.
  145. *
  146. * Offsetting the end of the range seems to be a usable workaround.
  147. *
  148. * @private
  149. */
  150. static patchRemovalRange_() {
  151. // eslint-disable-next-line no-restricted-syntax
  152. const originalRemove = SourceBuffer.prototype.remove;
  153. // eslint-disable-next-line no-restricted-syntax
  154. SourceBuffer.prototype.remove = function(startTime, endTime) {
  155. // eslint-disable-next-line no-restricted-syntax
  156. return originalRemove.call(this, startTime, endTime - 0.001);
  157. };
  158. }
  159. /**
  160. * Patch |MediaSource.isTypeSupported| to always reject |container|. This is
  161. * used when we know that we are on a platform that does not work well with
  162. * a given container.
  163. *
  164. * @param {string} container
  165. * @private
  166. */
  167. static rejectContainer_(container) {
  168. if (window.MediaSource) {
  169. const isTypeSupported =
  170. // eslint-disable-next-line no-restricted-syntax
  171. MediaSource.isTypeSupported.bind(MediaSource);
  172. MediaSource.isTypeSupported = (mimeType) => {
  173. const actualContainer = shaka.util.MimeUtils.getContainerType(mimeType);
  174. return actualContainer != container && isTypeSupported(mimeType);
  175. };
  176. }
  177. if (window.ManagedMediaSource) {
  178. const isTypeSupportedManaged =
  179. // eslint-disable-next-line no-restricted-syntax
  180. ManagedMediaSource.isTypeSupported.bind(ManagedMediaSource);
  181. window.ManagedMediaSource.isTypeSupported = (mimeType) => {
  182. const actualContainer = shaka.util.MimeUtils.getContainerType(mimeType);
  183. return actualContainer != container && isTypeSupportedManaged(mimeType);
  184. };
  185. }
  186. }
  187. /**
  188. * Patch |MediaSource.isTypeSupported| to always reject |codec|. This is used
  189. * when we know that we are on a platform that does not work well with a given
  190. * codec.
  191. *
  192. * @param {string} codec
  193. * @private
  194. */
  195. static rejectCodec_(codec) {
  196. const isTypeSupported =
  197. // eslint-disable-next-line no-restricted-syntax
  198. MediaSource.isTypeSupported.bind(MediaSource);
  199. MediaSource.isTypeSupported = (mimeType) => {
  200. const actualCodec = shaka.util.MimeUtils.getCodecBase(mimeType);
  201. return actualCodec != codec && isTypeSupported(mimeType);
  202. };
  203. if (window.ManagedMediaSource) {
  204. const isTypeSupportedManaged =
  205. // eslint-disable-next-line no-restricted-syntax
  206. ManagedMediaSource.isTypeSupported.bind(ManagedMediaSource);
  207. window.ManagedMediaSource.isTypeSupported = (mimeType) => {
  208. const actualCodec = shaka.util.MimeUtils.getCodecBase(mimeType);
  209. return actualCodec != codec && isTypeSupportedManaged(mimeType);
  210. };
  211. }
  212. }
  213. /**
  214. * Patch isTypeSupported() to translate vp09 codec strings into vp9, to allow
  215. * such content to play on older smart TVs.
  216. *
  217. * @private
  218. */
  219. static patchVp09_() {
  220. const originalIsTypeSupported = MediaSource.isTypeSupported;
  221. if (shaka.util.Platform.isWebOS()) {
  222. // Don't do this on LG webOS as otherwise it is unable
  223. // to play vp09 at all.
  224. return;
  225. }
  226. MediaSource.isTypeSupported = (mimeType) => {
  227. // Split the MIME type into its various parameters.
  228. const pieces = mimeType.split(/ *; */);
  229. const codecsIndex =
  230. pieces.findIndex((piece) => piece.startsWith('codecs='));
  231. if (codecsIndex < 0) {
  232. // No codec? Call the original without modifying the MIME type.
  233. return originalIsTypeSupported(mimeType);
  234. }
  235. const codecsParam = pieces[codecsIndex];
  236. const codecs = codecsParam
  237. .replace('codecs=', '').replace(/"/g, '').split(/\s*,\s*/);
  238. const vp09Index = codecs.findIndex(
  239. (codecName) => codecName.startsWith('vp09'));
  240. if (vp09Index >= 0) {
  241. // vp09? Replace it with vp9.
  242. codecs[vp09Index] = 'vp9';
  243. pieces[codecsIndex] = 'codecs="' + codecs.join(',') + '"';
  244. mimeType = pieces.join('; ');
  245. }
  246. return originalIsTypeSupported(mimeType);
  247. };
  248. }
  249. };
  250. shaka.polyfill.register(shaka.polyfill.MediaSource.install);