Browse Source

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`
pull/1537/head
mister-ben 11 months ago
committed by GitHub
parent
commit
247047a05a
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 9
      README.md
  2. 6
      index.html
  3. 5
      scripts/index.js
  4. 12
      src/playlist-controller.js
  5. 12
      src/videojs-http-streaming.js
  6. 36
      test/playlist-controller.test.js
  7. 12
      test/test-helpers.js

9
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

6
index.html

@ -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>
@ -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>

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

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

12
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

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

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

Loading…
Cancel
Save