Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add experimental support for ManagedMediaSource #1453

Merged
merged 14 commits into from
Aug 23, 2024
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">Use ManagedMediaSource if available. Use in combination with override native (reloads player)</label>
mister-ben marked this conversation as resolved.
Show resolved Hide resolved
</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 @@
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;

Check warning on line 1374 in src/videojs-http-streaming.js

View check run for this annotation

Codecov / codecov/patch

src/videojs-http-streaming.js#L1374

Added line #L1374 was not covered by tests
}

return VhsSourceHandler.canPlayType(srcObj.type, localOptions);
},
handleSource(source, tech, options = {}) {
Expand Down Expand Up @@ -1403,13 +1408,14 @@
};

/**
* 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
Loading