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.device.DeviceFactory');
  8. goog.require('shaka.device.IDevice');
  9. goog.require('shaka.log');
  10. goog.require('shaka.polyfill');
  11. goog.require('shaka.util.MimeUtils');
  12. /**
  13. * @summary A polyfill to patch MSE bugs.
  14. * @export
  15. */
  16. shaka.polyfill.MediaSource = class {
  17. /**
  18. * Install the polyfill if needed.
  19. * @export
  20. */
  21. static install() {
  22. shaka.log.debug('MediaSource.install');
  23. // MediaSource bugs are difficult to detect without checking for the
  24. // affected platform. SourceBuffer is not always exposed on window, for
  25. // example, and instances are only accessible after setting up MediaSource
  26. // on a video element. Because of this, we use UA detection and other
  27. // platform detection tricks to decide which patches to install.
  28. const BrowserEngine = shaka.device.IDevice.BrowserEngine;
  29. const device = shaka.device.DeviceFactory.getDevice();
  30. const safariVersion = device.getBrowserEngine() === BrowserEngine.WEBKIT ?
  31. device.getVersion() : null;
  32. if (!window.MediaSource && !window.ManagedMediaSource) {
  33. shaka.log.info('No MSE implementation available.');
  34. } else if (safariVersion && window.MediaSource) {
  35. // NOTE: shaka.Player.isBrowserSupported() has its own restrictions on
  36. // Safari version.
  37. if (safariVersion <= 12) {
  38. shaka.log.info('Patching Safari 8-12 MSE bugs.');
  39. // Safari 8 does not implement appendWindowEnd.
  40. // Safari 10 fires spurious 'updateend' events after endOfStream().
  41. // We do not have patches for these bugs here.
  42. // Safari 8-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 8-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 if (safariVersion <= 15) {
  53. shaka.log.info('Patching Safari 13 & 14 & 15 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 {
  61. shaka.log.info('Using native MSE as-is.');
  62. }
  63. for (const codec of device.rejectCodecs()) {
  64. shaka.log.info(`Rejecting ${codec}.`);
  65. shaka.polyfill.MediaSource.rejectCodec_(codec);
  66. }
  67. if (window.MediaSource || window.ManagedMediaSource) {
  68. // TS content is broken on all browsers in general.
  69. // See https://github.com/shaka-project/shaka-player/issues/4955
  70. // See https://github.com/shaka-project/shaka-player/issues/5278
  71. // See https://github.com/shaka-project/shaka-player/issues/6334
  72. shaka.polyfill.MediaSource.rejectContainer_('mp2t');
  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. if (window.MediaSource) {
  129. const isTypeSupported =
  130. // eslint-disable-next-line no-restricted-syntax
  131. MediaSource.isTypeSupported.bind(MediaSource);
  132. MediaSource.isTypeSupported = (mimeType) => {
  133. const actualContainer = shaka.util.MimeUtils.getContainerType(mimeType);
  134. return actualContainer != container && isTypeSupported(mimeType);
  135. };
  136. }
  137. if (window.ManagedMediaSource) {
  138. const isTypeSupportedManaged =
  139. // eslint-disable-next-line no-restricted-syntax
  140. ManagedMediaSource.isTypeSupported.bind(ManagedMediaSource);
  141. window.ManagedMediaSource.isTypeSupported = (mimeType) => {
  142. const actualContainer = shaka.util.MimeUtils.getContainerType(mimeType);
  143. return actualContainer != container && isTypeSupportedManaged(mimeType);
  144. };
  145. }
  146. }
  147. /**
  148. * Patch |MediaSource.isTypeSupported| to always reject |codec|. This is used
  149. * when we know that we are on a platform that does not work well with a given
  150. * codec.
  151. *
  152. * @param {string} codec
  153. * @private
  154. */
  155. static rejectCodec_(codec) {
  156. const isTypeSupported =
  157. // eslint-disable-next-line no-restricted-syntax
  158. MediaSource.isTypeSupported.bind(MediaSource);
  159. MediaSource.isTypeSupported = (mimeType) => {
  160. const actualCodec = shaka.util.MimeUtils.getCodecBase(mimeType);
  161. return actualCodec != codec && isTypeSupported(mimeType);
  162. };
  163. if (window.ManagedMediaSource) {
  164. const isTypeSupportedManaged =
  165. // eslint-disable-next-line no-restricted-syntax
  166. ManagedMediaSource.isTypeSupported.bind(ManagedMediaSource);
  167. window.ManagedMediaSource.isTypeSupported = (mimeType) => {
  168. const actualCodec = shaka.util.MimeUtils.getCodecBase(mimeType);
  169. return actualCodec != codec && isTypeSupportedManaged(mimeType);
  170. };
  171. }
  172. }
  173. /**
  174. * Patch isTypeSupported() to translate vp09 codec strings into vp9, to allow
  175. * such content to play on older smart TVs.
  176. *
  177. * @private
  178. */
  179. static patchVp09_() {
  180. const originalIsTypeSupported = MediaSource.isTypeSupported;
  181. if (!shaka.device.DeviceFactory.getDevice().supportStandardVP9Checking()) {
  182. // Don't do this on LG webOS as otherwise it is unable
  183. // to play vp09 at all.
  184. return;
  185. }
  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);