Skip to content

Commit

Permalink
feat: useNetworkInfo API by default + exclude audio only renditions w…
Browse files Browse the repository at this point in the history
…hen we have video renditions alongside (#1565)

* chore: filter audio-only playlists when we have playlists with video-only or muxed

* chore: set use network info api to true by default

* chore: simplify filter logic

* chore: use infinity exclude

* chore: default to true only if unset

* chore: fix tests

* feat: use default as true for networkInformation api
  • Loading branch information
dzianis-dashkevich authored Feb 5, 2025
1 parent 14ac65a commit 1289dd4
Show file tree
Hide file tree
Showing 10 changed files with 83 additions and 52 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ This option defaults to `false`.

##### useNetworkInformationApi
* Type: `boolean`,
* Default: `false`
* Default: `true`
* Use [window.networkInformation.downlink](https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/downlink) to estimate the network's bandwidth. Per mdn, _The value is never greater than 10 Mbps, as a non-standard anti-fingerprinting measure_. Given this, if bandwidth estimates from both the player and networkInfo are >= 10 Mbps, the player will use the larger of the two values as its bandwidth estimate.

##### useDtsForTimestampOffset
Expand Down
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@
</div>

<div class="form-check">
<input id=network-info type="checkbox" class="form-check-input">
<input id=network-info type="checkbox" class="form-check-input" checked>
<label class="form-check-label" for="network-info">Use networkInfo API for bandwidth estimations (reloads player)</label>
</div>

Expand Down
43 changes: 42 additions & 1 deletion src/playlist-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {getKnownPartCount} from './playlist.js';
import {merge} from './util/vjs-compat';
import DateRangesStorage from './util/date-ranges';
import { getStreamingNetworkErrorMetadata } from './error-codes.js';
import {getCodecs, unwrapCodecList} from './util/codecs';

const { EventTarget } = videojs;

Expand Down Expand Up @@ -523,14 +524,27 @@ export default class PlaylistLoader extends EventTarget {

parseManifest_({url, manifestString}) {
try {
return parseManifest({
const parsed = parseManifest({
onwarn: ({message}) => this.logger_(`m3u8-parser warn for ${url}: ${message}`),
oninfo: ({message}) => this.logger_(`m3u8-parser info for ${url}: ${message}`),
manifestString,
customTagParsers: this.customTagParsers,
customTagMappers: this.customTagMappers,
llhls: this.llhls
});

/**
* VHS does not support switching between variants with and without audio and video
* so we want to filter out audio-only variants when variants with video and(or) audio are also detected.
*/

if (!parsed.playlists || !parsed.playlists.length) {
return parsed;
}

this.excludeAudioOnlyVariants(parsed.playlists);

return parsed;
} catch (error) {
this.error = error;
this.error.metadata = {
Expand All @@ -540,6 +554,33 @@ export default class PlaylistLoader extends EventTarget {
}
}

excludeAudioOnlyVariants(playlists) {
// helper function
const hasVideo = (playlist) => {
const attributes = playlist.attributes || {};
const { width, height } = attributes.RESOLUTION || {};

if (width && height) {
return true;
}

// parse codecs string from playlist attributes
const codecsList = getCodecs(playlist) || [];
// unwrap list
const codecsInfo = unwrapCodecList(codecsList);

return Boolean(codecsInfo.video);
};

if (playlists.some(hasVideo)) {
playlists.forEach((playlist) => {
if (!hasVideo(playlist)) {
playlist.excludeUntil = Infinity;
}
});
}
}

/**
* Update the playlist loader's state in response to a new or updated playlist.
*
Expand Down
2 changes: 1 addition & 1 deletion src/util/codecs.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const logFn = logger('CodecUtils');
* @param {Playlist} media the current media playlist
* @return {Object} an object with the video and audio codecs
*/
const getCodecs = function(media) {
export const getCodecs = function(media) {
// if the codecs were explicitly specified, use them instead of the
// defaults
const mediaAttributes = media.attributes || {};
Expand Down
3 changes: 2 additions & 1 deletion src/videojs-http-streaming.js
Original file line number Diff line number Diff line change
Expand Up @@ -696,7 +696,8 @@ class VhsHandler extends Component {
this.source_.useBandwidthFromLocalStorage :
this.options_.useBandwidthFromLocalStorage || false;
this.options_.useForcedSubtitles = this.options_.useForcedSubtitles || false;
this.options_.useNetworkInformationApi = this.options_.useNetworkInformationApi || false;
this.options_.useNetworkInformationApi = typeof this.options_.useNetworkInformationApi !== 'undefined' ?
this.options_.useNetworkInformationApi : true;
this.options_.useDtsForTimestampOffset = this.options_.useDtsForTimestampOffset || false;
this.options_.customTagParsers = this.options_.customTagParsers || [];
this.options_.customTagMappers = this.options_.customTagMappers || [];
Expand Down
2 changes: 1 addition & 1 deletion test/manifests/multipleAudioGroupsCombinedMain.m3u8
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-hi",LANGUAGE="fre",NAME="Français",AUTOSELECT=YES, DEFAULT=NO,URI="fre/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-hi",LANGUAGE="sp",NAME="Espanol",AUTOSELECT=YES, DEFAULT=NO,URI="sp/prog_index.m3u8"

#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=195023,CODECS="mp4a.40.5", AUDIO="audio-lo"
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=195023,CODECS="avc1.42e01e,mp4a.40.5", AUDIO="audio-lo"
lo/prog_index.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=260000,CODECS="avc1.42e01e,mp4a.40.2", AUDIO="audio-lo"
lo2/prog_index.m3u8
Expand Down
2 changes: 1 addition & 1 deletion test/manifests/two-renditions.m3u8
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=240000,RESOLUTION=396x224
media.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=40000
#EXT-X-STREAM-INF:BANDWIDTH=40000,RESOLUTION=396x224
media1.m3u8
29 changes: 4 additions & 25 deletions test/playlist-controller.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1478,11 +1478,7 @@ QUnit.test('excludes switching from video+audio playlists to audio only', functi
this.standardXHRResponse(this.requests.shift());

const pc = this.playlistController;
let debugLogs = [];

pc.logger_ = (...logs) => {
debugLogs = debugLogs.concat(logs);
};
// segment must be appended before the exclusion logic runs
return requestAndAppendSegment({
request: this.requests.shift(),
Expand All @@ -1498,11 +1494,6 @@ QUnit.test('excludes switching from video+audio playlists to audio only', functi
const audioPlaylist = pc.mainPlaylistLoader_.main.playlists[0];

assert.equal(audioPlaylist.excludeUntil, Infinity, 'excluded incompatible playlist');
assert.notEqual(
debugLogs.indexOf('excluding 0-media.m3u8: codec count "1" !== "2"'),
-1,
'debug logs about codec count'
);
});
});

Expand All @@ -1525,11 +1516,6 @@ QUnit.test('excludes switching from audio-only playlists to video+audio', functi
// media1
this.standardXHRResponse(this.requests.shift());

let debugLogs = [];

pc.logger_ = (...logs) => {
debugLogs = debugLogs.concat(logs);
};
// segment must be appended before the exclusion logic runs
return requestAndAppendSegment({
request: this.requests.shift(),
Expand All @@ -1540,23 +1526,17 @@ QUnit.test('excludes switching from audio-only playlists to video+audio', functi
}).then(() => {
assert.equal(
pc.mainPlaylistLoader_.media(),
pc.mainPlaylistLoader_.main.playlists[0],
'selected audio only'
pc.mainPlaylistLoader_.main.playlists[1],
'selected audio+video'
);

const videoAudioPlaylist = pc.mainPlaylistLoader_.main.playlists[1];
const audioOnly = pc.mainPlaylistLoader_.main.playlists[0];

assert.equal(
videoAudioPlaylist.excludeUntil,
audioOnly.excludeUntil,
Infinity,
'excluded incompatible playlist'
);

assert.notEqual(
debugLogs.indexOf('excluding 1-media1.m3u8: codec count "2" !== "1"'),
-1,
'debug logs about codec count'
);
});
});

Expand Down Expand Up @@ -1682,7 +1662,6 @@ QUnit.test('excludes switching between playlists with different codecs', functio
'excluding 1-media1.m3u8: video codec "hvc1" !== "avc1"',
'excluding 2-media2.m3u8: audio codec "ac-3" !== "mp4a"',
'excluding 3-media3.m3u8: video codec "hvc1" !== "avc1" && audio codec "ac-3" !== "mp4a"',
'excluding 5-media5.m3u8: codec count "1" !== "2" && audio codec "ac-3" !== "mp4a"',
'excluding 6-media6.m3u8: codec count "1" !== "2" && video codec "hvc1" !== "avc1"'
].forEach(function(message) {
assert.notEqual(
Expand Down
12 changes: 11 additions & 1 deletion test/test-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,17 @@ export const createPlayer = function(options, src, clock) {
}
}
document.querySelector('#qunit-fixture').appendChild(video);
const player = videojs(video, options || {});

options = options || {};
options.html5 = options.html5 || {};
options.html5.vhs = options.html5.vhs || {};

// we should disable useNetworkInformationApi for tests, unless it is explicitly set to some value
if (typeof options.html5.vhs.useNetworkInformationApi === 'undefined') {
options.html5.vhs.useNetworkInformationApi = false;
}

const player = videojs(video, options);

player.buffered = function() {
return createTimeRanges(0, 0);
Expand Down
38 changes: 19 additions & 19 deletions test/videojs-http-streaming.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1248,7 +1248,7 @@ QUnit.test('buffer checks are noops when only the main is ready', function(asser
assert.strictEqual(this.requests.length, 1, 'one request was made');
assert.strictEqual(
this.requests[0].url,
absoluteUrl('manifest/media1.m3u8'),
absoluteUrl('manifest/media.m3u8'),
'media playlist requested'
);

Expand All @@ -1269,16 +1269,16 @@ QUnit.test('selects a playlist below the current bandwidth', function(assert) {

// the default playlist has a really high bitrate
this.player.tech_.vhs.playlists.main.playlists[0].attributes.BANDWIDTH = 9e10;
// playlist 1 has a very low bitrate
this.player.tech_.vhs.playlists.main.playlists[1].attributes.BANDWIDTH = 1;
// playlist 2 has a very low bitrate
this.player.tech_.vhs.playlists.main.playlists[2].attributes.BANDWIDTH = 1;
// but the detected client bandwidth is really low
this.player.tech_.vhs.bandwidth = 10;

const playlist = this.player.tech_.vhs.selectPlaylist();

assert.strictEqual(
playlist,
this.player.tech_.vhs.playlists.main.playlists[1],
this.player.tech_.vhs.playlists.main.playlists[2],
'the low bitrate stream is selected'
);

Expand Down Expand Up @@ -1383,12 +1383,12 @@ QUnit.test('raises the minimum bitrate for a stream proportionially', function(a
this.player.tech_.vhs.bandwidth = 11;

// 9.9 * 1.1 < 11
this.player.tech_.vhs.playlists.main.playlists[1].attributes.BANDWIDTH = 9.9;
this.player.tech_.vhs.playlists.main.playlists[2].attributes.BANDWIDTH = 9.9;
const playlist = this.player.tech_.vhs.selectPlaylist();

assert.strictEqual(
playlist,
this.player.tech_.vhs.playlists.main.playlists[1],
this.player.tech_.vhs.playlists.main.playlists[2],
'a lower bitrate stream is selected'
);

Expand Down Expand Up @@ -1416,7 +1416,7 @@ QUnit.test('uses the lowest bitrate if no other is suitable', function(assert) {
// playlist 1 has the lowest advertised bitrate
assert.strictEqual(
playlist,
this.player.tech_.vhs.playlists.main.playlists[1],
this.player.tech_.vhs.playlists.main.playlists[0],
'the lowest bitrate stream is selected'
);

Expand Down Expand Up @@ -2892,7 +2892,7 @@ QUnit.test('resets the switching algorithm if a request times out', function(ass

assert.strictEqual(
this.player.tech_.vhs.playlists.media(),
this.player.tech_.vhs.playlists.main.playlists[1],
this.player.tech_.vhs.playlists.main.playlists[0],
'reset to the lowest bitrate playlist'
);

Expand Down Expand Up @@ -4693,7 +4693,7 @@ QUnit.test('populates quality levels list when available', function(assert) {
// media
this.standardXHRResponse(this.requests.shift());

assert.equal(addCount, 4, 'four levels added from main');
assert.equal(addCount, 3, 'three levels added from main');
assert.equal(changeCount, 1, 'selected initial quality level');

this.player.dispose();
Expand Down Expand Up @@ -5837,7 +5837,7 @@ QUnit.test('aborts all in-flight work when disposed', function(assert) {
const vhs = VhsSourceHandler.handleSource({
src: 'manifest/main.m3u8',
type: 'application/vnd.apple.mpegurl'
}, this.tech);
}, this.tech, { vhs: { useNetworkInformationApi: false } });

vhs.mediaSource.trigger('sourceopen');
// main
Expand All @@ -5859,7 +5859,7 @@ QUnit.test('stats are reset on dispose', function(assert) {
const vhs = VhsSourceHandler.handleSource({
src: 'manifest/main.m3u8',
type: 'application/vnd.apple.mpegurl'
}, this.tech);
}, this.tech, { vhs: { useNetworkInformationApi: false } });

vhs.mediaSource.trigger('sourceopen');
// main
Expand Down Expand Up @@ -5891,7 +5891,7 @@ QUnit.skip('detects fullscreen and triggers a fast quality change', function(ass
const vhs = VhsSourceHandler.handleSource({
src: 'manifest/main.m3u8',
type: 'application/vnd.apple.mpegurl'
}, this.tech);
}, this.tech, { vhs: { useNetworkInformationApi: false } });

let qualityChanges = 0;
let fullscreenElementName;
Expand Down Expand Up @@ -5933,7 +5933,7 @@ QUnit.test('downloads additional playlists if required', function(assert) {
const vhs = VhsSourceHandler.handleSource({
src: 'manifest/main.m3u8',
type: 'application/vnd.apple.mpegurl'
}, this.tech);
}, this.tech, { vhs: { useNetworkInformationApi: false } });

// Make segment metadata noop since most test segments dont have real data
vhs.playlistController_.mainSegmentLoader_.addSegmentMetadataCue_ = () => {};
Expand Down Expand Up @@ -5987,7 +5987,7 @@ QUnit.test('waits to download new segments until the media playlist is stable',
const vhs = VhsSourceHandler.handleSource({
src: 'manifest/main.m3u8',
type: 'application/vnd.apple.mpegurl'
}, this.tech);
}, this.tech, { vhs: { useNetworkInformationApi: false } });
const pc = vhs.playlistController_;

pc.mainSegmentLoader_.addSegmentMetadataCue_ = () => {};
Expand Down Expand Up @@ -6039,7 +6039,7 @@ QUnit.test('live playlist starts three target durations before live', function(a
const vhs = VhsSourceHandler.handleSource({
src: 'manifest/main.m3u8',
type: 'application/vnd.apple.mpegurl'
}, this.tech);
}, this.tech, { vhs: { useNetworkInformationApi: false } });

vhs.mediaSource.trigger('sourceopen');
this.requests.shift().respond(
Expand Down Expand Up @@ -6099,7 +6099,7 @@ QUnit.test(
let vhs = VhsSourceHandler.handleSource({
src: 'manifest/main.m3u8',
type: 'application/vnd.apple.mpegurl'
}, this.tech);
}, this.tech, { vhs: { useNetworkInformationApi: false } });

vhs.playlistController_.selectPlaylist();
assert.equal(defaultSelectPlaylistCount, 1, 'uses default playlist selector');
Expand All @@ -6116,7 +6116,7 @@ QUnit.test(
vhs = VhsSourceHandler.handleSource({
src: 'manifest/main.m3u8',
type: 'application/vnd.apple.mpegurl'
}, this.tech);
}, this.tech, { vhs: { useNetworkInformationApi: false } });

vhs.playlistController_.selectPlaylist();
assert.equal(defaultSelectPlaylistCount, 0, 'standard playlist selector not run');
Expand Down Expand Up @@ -6170,7 +6170,7 @@ QUnit.test('excludes playlist if key requests fail', function(assert) {
const vhs = VhsSourceHandler.handleSource({
src: 'manifest/encrypted-main.m3u8',
type: 'application/vnd.apple.mpegurl'
}, this.tech);
}, this.tech, { vhs: { useNetworkInformationApi: false } });

vhs.mediaSource.trigger('sourceopen');
this.requests.shift()
Expand Down Expand Up @@ -6219,7 +6219,7 @@ QUnit.test(
const vhs = VhsSourceHandler.handleSource({
src: 'manifest/encrypted-main.m3u8',
type: 'application/vnd.apple.mpegurl'
}, this.tech);
}, this.tech, { vhs: { useNetworkInformationApi: false } });

vhs.mediaSource.trigger('sourceopen');
this.requests.shift()
Expand Down

0 comments on commit 1289dd4

Please sign in to comment.