From 247047a05a2ca818a8a5beeb2512804cb3082f5d Mon Sep 17 00:00:00 2001 From: mister-ben <1676039+mister-ben@users.noreply.github.com> Date: Fri, 23 Aug 2024 15:29:14 +0200 Subject: [PATCH] feat: Add experimental support for ManagedMediaSource (#1453) Adds basic support for ManagedMediaSource. Must be enabled with the `useManagedMediaSource` VHS option. Does not implement an alternate AirPlay source - this requires a more significant change, to add two source els. This means remote playback has to be disabled on the video el when using MMS. Event listeners for advanced control are not yet implemented - `startstreaming`, `endstreaming`, `qualitychange` --- README.md | 9 ++++++++ index.html | 6 ++++++ scripts/index.js | 5 ++++- src/playlist-controller.js | 12 +++++++++-- src/videojs-http-streaming.js | 12 ++++++++--- test/playlist-controller.test.js | 36 ++++++++++++++++++++++++++++++++ test/test-helpers.js | 12 +++++++++++ 7 files changed, 86 insertions(+), 6 deletions(-) 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();