Skip to content

Commit

Permalink
feat: Add experimental support for ManagedMediaSource (#1453)
Browse files Browse the repository at this point in the history
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`
  • Loading branch information
mister-ben authored Aug 23, 2024
1 parent dba1b79 commit 247047a
Show file tree
Hide file tree
Showing 7 changed files with 86 additions and 6 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,11 @@
<label class="form-check-label" for="override-native">Override Native (reloads player)</label>
</div>

<div class="form-check">
<input id=use-mms type="checkbox" class="form-check-input" checked>
<label class="form-check-label" for="use-mms">[EXPERIMENTAL] Use ManagedMediaSource if available. Use in combination with override native (reloads player)</label>
</div>

<div class="form-check">
<input id=mirror-source type="checkbox" class="form-check-input" checked>
<label class="form-check-label" for="mirror-source">Mirror sources from player.src (reloads player, uses EXPERIMENTAL sourceset option)</label>
Expand Down Expand Up @@ -274,6 +279,7 @@
</div>
</div>


<footer class="text-center p-3" id=unit-test-link>
<a href="test/debug.html">Run unit tests</a>
</footer>
Expand Down
5 changes: 4 additions & 1 deletion scripts/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,7 @@
'network-info',
'dts-offset',
'override-native',
'use-mms',
'preload',
'mirror-source',
'forced-subtitles'
Expand Down Expand Up @@ -521,6 +522,7 @@
'llhls',
'buffer-water',
'override-native',
'use-mms',
'liveui',
'pixel-diff-selector',
'network-info',
Expand Down Expand Up @@ -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);
Expand All @@ -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']),
Expand All @@ -612,7 +616,6 @@
}
}
});

setupPlayerStats(player);
setupSegmentMetadata(player);
setupContentSteeringData(player);
Expand Down
12 changes: 10 additions & 2 deletions src/playlist-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@ export class PlaylistController extends videojs.EventTarget {
cacheEncryptionKeys,
bufferBasedABR,
leastPixelDiffSelector,
captionServices
captionServices,
experimentalUseMMS
} = options;

if (!src) {
Expand Down Expand Up @@ -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);
Expand Down
12 changes: 9 additions & 3 deletions src/videojs-http-streaming.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}) {
Expand Down Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions test/playlist-controller.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import window from 'global/window';
import {
useFakeEnvironment,
useFakeMediaSource,
useFakeManagedMediaSource,
createPlayer,
standardXHRResponse,
openMediaSource,
Expand Down Expand Up @@ -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();
});
12 changes: 12 additions & 0 deletions test/test-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down

0 comments on commit 247047a

Please sign in to comment.