Skip to content

Commit 247047a

Browse files
authored
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`
1 parent dba1b79 commit 247047a

File tree

7 files changed

+86
-6
lines changed

7 files changed

+86
-6
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ Video.js Compatibility: 7.x, 8.x
4141
- [useCueTags](#usecuetags)
4242
- [parse708captions](#parse708captions)
4343
- [overrideNative](#overridenative)
44+
- [experimentalUseMMS](#experimentalusemms)
4445
- [playlistExclusionDuration](#playlistexclusionduration)
4546
- [maxPlaylistRetries](#maxplaylistretries)
4647
- [bandwidth](#bandwidth)
@@ -349,6 +350,14 @@ var player = videojs('playerId', {
349350

350351
Since MSE playback may be desirable on all browsers with some native support other than Safari, `overrideNative: !videojs.browser.IS_SAFARI` could be used.
351352

353+
##### experimentalUseMMS
354+
* Type: `boolean`
355+
* can be used as an initialization option
356+
357+
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.
358+
359+
Currently, using this option will disable AirPlay.
360+
352361
##### playlistExclusionDuration
353362
* Type: `number`
354363
* can be used as an initialization option

index.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,11 @@
177177
<label class="form-check-label" for="override-native">Override Native (reloads player)</label>
178178
</div>
179179

180+
<div class="form-check">
181+
<input id=use-mms type="checkbox" class="form-check-input" checked>
182+
<label class="form-check-label" for="use-mms">[EXPERIMENTAL] Use ManagedMediaSource if available. Use in combination with override native (reloads player)</label>
183+
</div>
184+
180185
<div class="form-check">
181186
<input id=mirror-source type="checkbox" class="form-check-input" checked>
182187
<label class="form-check-label" for="mirror-source">Mirror sources from player.src (reloads player, uses EXPERIMENTAL sourceset option)</label>
@@ -274,6 +279,7 @@
274279
</div>
275280
</div>
276281

282+
277283
<footer class="text-center p-3" id=unit-test-link>
278284
<a href="test/debug.html">Run unit tests</a>
279285
</footer>

scripts/index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,7 @@
471471
'network-info',
472472
'dts-offset',
473473
'override-native',
474+
'use-mms',
474475
'preload',
475476
'mirror-source',
476477
'forced-subtitles'
@@ -521,6 +522,7 @@
521522
'llhls',
522523
'buffer-water',
523524
'override-native',
525+
'use-mms',
524526
'liveui',
525527
'pixel-diff-selector',
526528
'network-info',
@@ -587,6 +589,7 @@
587589
var videoEl = document.createElement('video-js');
588590

589591
videoEl.setAttribute('controls', '');
592+
videoEl.setAttribute('playsInline', '');
590593
videoEl.setAttribute('preload', stateEls.preload.options[stateEls.preload.selectedIndex].value || 'auto');
591594
videoEl.className = 'vjs-default-skin';
592595
fixture.appendChild(videoEl);
@@ -602,6 +605,7 @@
602605
html5: {
603606
vhs: {
604607
overrideNative: getInputValue(stateEls['override-native']),
608+
experimentalUseMMS: getInputValue(stateEls['use-mms']),
605609
bufferBasedABR: getInputValue(stateEls['buffer-water']),
606610
llhls: getInputValue(stateEls.llhls),
607611
exactManifestTimings: getInputValue(stateEls['exact-manifest-timings']),
@@ -612,7 +616,6 @@
612616
}
613617
}
614618
});
615-
616619
setupPlayerStats(player);
617620
setupSegmentMetadata(player);
618621
setupContentSteeringData(player);

src/playlist-controller.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,8 @@ export class PlaylistController extends videojs.EventTarget {
165165
cacheEncryptionKeys,
166166
bufferBasedABR,
167167
leastPixelDiffSelector,
168-
captionServices
168+
captionServices,
169+
experimentalUseMMS
169170
} = options;
170171

171172
if (!src) {
@@ -210,7 +211,14 @@ export class PlaylistController extends videojs.EventTarget {
210211

211212
this.mediaTypes_ = createMediaTypes();
212213

213-
this.mediaSource = new window.MediaSource();
214+
if (experimentalUseMMS && window.ManagedMediaSource) {
215+
// Airplay source not yet implemented. Remote playback must be disabled.
216+
this.tech_.el_.disableRemotePlayback = true;
217+
this.mediaSource = new window.ManagedMediaSource();
218+
videojs.log('Using ManagedMediaSource');
219+
} else if (window.MediaSource) {
220+
this.mediaSource = new window.MediaSource();
221+
}
214222

215223
this.handleDurationChange_ = this.handleDurationChange_.bind(this);
216224
this.handleSourceOpen_ = this.handleSourceOpen_.bind(this);

src/videojs-http-streaming.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1369,6 +1369,11 @@ const VhsSourceHandler = {
13691369
canHandleSource(srcObj, options = {}) {
13701370
const localOptions = merge(videojs.options, options);
13711371

1372+
// If not opting to experimentalUseMMS, and playback is only supported with MediaSource, cannot handle source
1373+
if (!localOptions.vhs.experimentalUseMMS && !browserSupportsCodec('avc1.4d400d,mp4a.40.2', false)) {
1374+
return false;
1375+
}
1376+
13721377
return VhsSourceHandler.canPlayType(srcObj.type, localOptions);
13731378
},
13741379
handleSource(source, tech, options = {}) {
@@ -1403,13 +1408,14 @@ const VhsSourceHandler = {
14031408
};
14041409

14051410
/**
1406-
* Check to see if the native MediaSource object exists and supports
1407-
* an MP4 container with both H.264 video and AAC-LC audio.
1411+
* Check to see if either the native MediaSource or ManagedMediaSource
1412+
* objectx exist and support an MP4 container with both H.264 video
1413+
* and AAC-LC audio.
14081414
*
14091415
* @return {boolean} if native media sources are supported
14101416
*/
14111417
const supportsNativeMediaSources = () => {
1412-
return browserSupportsCodec('avc1.4d400d,mp4a.40.2');
1418+
return browserSupportsCodec('avc1.4d400d,mp4a.40.2', true);
14131419
};
14141420

14151421
// register source handlers with the appropriate techs

test/playlist-controller.test.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import window from 'global/window';
55
import {
66
useFakeEnvironment,
77
useFakeMediaSource,
8+
useFakeManagedMediaSource,
89
createPlayer,
910
standardXHRResponse,
1011
openMediaSource,
@@ -7657,3 +7658,38 @@ QUnit.test('Pathway cloning - do nothing when next and past clones are the same'
76577658

76587659
assert.deepEqual(pc.contentSteeringController_.currentPathwayClones, clonesMap);
76597660
});
7661+
7662+
QUnit.test('uses ManagedMediaSource only when opted in', function(assert) {
7663+
const mms = useFakeManagedMediaSource();
7664+
7665+
const options = {
7666+
src: 'test',
7667+
tech: this.player.tech_,
7668+
player_: this.player
7669+
};
7670+
7671+
const msSpy = sinon.spy(window, 'MediaSource');
7672+
const mmsSpy = sinon.spy(window, 'ManagedMediaSource');
7673+
7674+
const controller1 = new PlaylistController(options);
7675+
7676+
assert.equal(true, window.MediaSource.called, 'by default, MediaSource used');
7677+
assert.equal(false, window.ManagedMediaSource.called, 'by default, ManagedMediaSource not used');
7678+
7679+
controller1.dispose();
7680+
window.MediaSource.resetHistory();
7681+
window.ManagedMediaSource.resetHistory();
7682+
7683+
options.experimentalUseMMS = true;
7684+
7685+
const controller2 = new PlaylistController(options);
7686+
7687+
assert.equal(false, window.MediaSource.called, 'when opted in, MediaSource not used');
7688+
assert.equal(true, window.ManagedMediaSource.called, 'whne opted in, ManagedMediaSource used');
7689+
7690+
controller2.dispose();
7691+
7692+
msSpy.restore();
7693+
mmsSpy.restore();
7694+
mms.restore();
7695+
});

test/test-helpers.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,18 @@ export const useFakeMediaSource = function() {
166166
};
167167
};
168168

169+
export const useFakeManagedMediaSource = function() {
170+
window.ManagedMediaSource = MockMediaSource;
171+
window.URL.createObjectURL = (object) => realCreateObjectURL(object instanceof MockMediaSource ? object.nativeMediaSource_ : object);
172+
173+
return {
174+
restore() {
175+
window.MediaSource = RealMediaSource;
176+
window.URL.createObjectURL = realCreateObjectURL;
177+
}
178+
};
179+
};
180+
169181
export const downloadProgress = (xhr, rawEventData) => {
170182
const text = rawEventData.toString();
171183

0 commit comments

Comments
 (0)