diff --git a/README.md b/README.md index 00d9bd0f9..11e5922a0 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ Video.js Compatibility: 7.x, 8.x - [useCueTags](#usecuetags) - [parse708captions](#parse708captions) - [overrideNative](#overridenative) + - [experimentalUseMMS](#experimentalusemms) - [playlistExclusionDuration](#playlistexclusionduration) - [maxPlaylistRetries](#maxplaylistretries) - [bandwidth](#bandwidth) @@ -349,6 +350,14 @@ var player = videojs('playerId', { Since MSE playback may be desirable on all browsers with some native support other than Safari, `overrideNative: !videojs.browser.IS_SAFARI` could be used. +##### experimentalUseMMS +* Type: `boolean` +* can be used as an initialization option + +Use ManagedMediaSource when available. If both ManagedMediaSource and MediaSource are present, ManagedMediaSource would be used. This will only be effective if `ovrerideNative` is true, because currently the only browsers that implement ManagedMediaSource also have native support. Safari on iPhone 17.1 has ManagedMediaSource, as does Safari 17 on desktop and iPad. + +Currently, using this option will disable AirPlay. + ##### playlistExclusionDuration * Type: `number` * can be used as an initialization option diff --git a/index.html b/index.html index 06b937626..d2940c3d9 100644 --- a/index.html +++ b/index.html @@ -177,6 +177,11 @@ +
+ + +
+
@@ -274,6 +279,7 @@
+ diff --git a/scripts/index.js b/scripts/index.js index e10483711..4b66316d6 100644 --- a/scripts/index.js +++ b/scripts/index.js @@ -471,6 +471,7 @@ 'network-info', 'dts-offset', 'override-native', + 'use-mms', 'preload', 'mirror-source', 'forced-subtitles' @@ -521,6 +522,7 @@ 'llhls', 'buffer-water', 'override-native', + 'use-mms', 'liveui', 'pixel-diff-selector', 'network-info', @@ -587,6 +589,7 @@ var videoEl = document.createElement('video-js'); videoEl.setAttribute('controls', ''); + videoEl.setAttribute('playsInline', ''); videoEl.setAttribute('preload', stateEls.preload.options[stateEls.preload.selectedIndex].value || 'auto'); videoEl.className = 'vjs-default-skin'; fixture.appendChild(videoEl); @@ -602,6 +605,7 @@ html5: { vhs: { overrideNative: getInputValue(stateEls['override-native']), + experimentalUseMMS: getInputValue(stateEls['use-mms']), bufferBasedABR: getInputValue(stateEls['buffer-water']), llhls: getInputValue(stateEls.llhls), exactManifestTimings: getInputValue(stateEls['exact-manifest-timings']), @@ -612,7 +616,6 @@ } } }); - setupPlayerStats(player); setupSegmentMetadata(player); setupContentSteeringData(player); diff --git a/src/playlist-controller.js b/src/playlist-controller.js index 0a61ad8bc..76106be1a 100644 --- a/src/playlist-controller.js +++ b/src/playlist-controller.js @@ -165,7 +165,8 @@ export class PlaylistController extends videojs.EventTarget { cacheEncryptionKeys, bufferBasedABR, leastPixelDiffSelector, - captionServices + captionServices, + experimentalUseMMS } = options; if (!src) { @@ -210,7 +211,14 @@ export class PlaylistController extends videojs.EventTarget { this.mediaTypes_ = createMediaTypes(); - this.mediaSource = new window.MediaSource(); + if (experimentalUseMMS && window.ManagedMediaSource) { + // Airplay source not yet implemented. Remote playback must be disabled. + this.tech_.el_.disableRemotePlayback = true; + this.mediaSource = new window.ManagedMediaSource(); + videojs.log('Using ManagedMediaSource'); + } else if (window.MediaSource) { + this.mediaSource = new window.MediaSource(); + } this.handleDurationChange_ = this.handleDurationChange_.bind(this); this.handleSourceOpen_ = this.handleSourceOpen_.bind(this); diff --git a/src/videojs-http-streaming.js b/src/videojs-http-streaming.js index 2ad41e134..1a6b75306 100644 --- a/src/videojs-http-streaming.js +++ b/src/videojs-http-streaming.js @@ -1369,6 +1369,11 @@ const VhsSourceHandler = { canHandleSource(srcObj, options = {}) { const localOptions = merge(videojs.options, options); + // If not opting to experimentalUseMMS, and playback is only supported with MediaSource, cannot handle source + if (!localOptions.vhs.experimentalUseMMS && !browserSupportsCodec('avc1.4d400d,mp4a.40.2', false)) { + return false; + } + return VhsSourceHandler.canPlayType(srcObj.type, localOptions); }, handleSource(source, tech, options = {}) { @@ -1403,13 +1408,14 @@ const VhsSourceHandler = { }; /** - * Check to see if the native MediaSource object exists and supports - * an MP4 container with both H.264 video and AAC-LC audio. + * Check to see if either the native MediaSource or ManagedMediaSource + * objectx exist and support an MP4 container with both H.264 video + * and AAC-LC audio. * * @return {boolean} if native media sources are supported */ const supportsNativeMediaSources = () => { - return browserSupportsCodec('avc1.4d400d,mp4a.40.2'); + return browserSupportsCodec('avc1.4d400d,mp4a.40.2', true); }; // register source handlers with the appropriate techs diff --git a/test/playlist-controller.test.js b/test/playlist-controller.test.js index f50b8eb67..9cbe9391c 100644 --- a/test/playlist-controller.test.js +++ b/test/playlist-controller.test.js @@ -5,6 +5,7 @@ import window from 'global/window'; import { useFakeEnvironment, useFakeMediaSource, + useFakeManagedMediaSource, createPlayer, standardXHRResponse, openMediaSource, @@ -7657,3 +7658,38 @@ QUnit.test('Pathway cloning - do nothing when next and past clones are the same' assert.deepEqual(pc.contentSteeringController_.currentPathwayClones, clonesMap); }); + +QUnit.test('uses ManagedMediaSource only when opted in', function(assert) { + const mms = useFakeManagedMediaSource(); + + const options = { + src: 'test', + tech: this.player.tech_, + player_: this.player + }; + + const msSpy = sinon.spy(window, 'MediaSource'); + const mmsSpy = sinon.spy(window, 'ManagedMediaSource'); + + const controller1 = new PlaylistController(options); + + assert.equal(true, window.MediaSource.called, 'by default, MediaSource used'); + assert.equal(false, window.ManagedMediaSource.called, 'by default, ManagedMediaSource not used'); + + controller1.dispose(); + window.MediaSource.resetHistory(); + window.ManagedMediaSource.resetHistory(); + + options.experimentalUseMMS = true; + + const controller2 = new PlaylistController(options); + + assert.equal(false, window.MediaSource.called, 'when opted in, MediaSource not used'); + assert.equal(true, window.ManagedMediaSource.called, 'whne opted in, ManagedMediaSource used'); + + controller2.dispose(); + + msSpy.restore(); + mmsSpy.restore(); + mms.restore(); +}); diff --git a/test/test-helpers.js b/test/test-helpers.js index 6a16cd48f..667796bda 100644 --- a/test/test-helpers.js +++ b/test/test-helpers.js @@ -166,6 +166,18 @@ export const useFakeMediaSource = function() { }; }; +export const useFakeManagedMediaSource = function() { + window.ManagedMediaSource = MockMediaSource; + window.URL.createObjectURL = (object) => realCreateObjectURL(object instanceof MockMediaSource ? object.nativeMediaSource_ : object); + + return { + restore() { + window.MediaSource = RealMediaSource; + window.URL.createObjectURL = realCreateObjectURL; + } + }; +}; + export const downloadProgress = (xhr, rawEventData) => { const text = rawEventData.toString();