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();