Browse Source

feat: useNetworkInfo API by default + exclude audio only renditions when 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
pull/1568/head
Dzianis Dashkevich 6 months ago
committed by GitHub
parent
commit
1289dd4ed0
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      README.md
  2. 2
      index.html
  3. 43
      src/playlist-loader.js
  4. 2
      src/util/codecs.js
  5. 3
      src/videojs-http-streaming.js
  6. 2
      test/manifests/multipleAudioGroupsCombinedMain.m3u8
  7. 2
      test/manifests/two-renditions.m3u8
  8. 29
      test/playlist-controller.test.js
  9. 12
      test/test-helpers.js
  10. 38
      test/videojs-http-streaming.test.js

2
README.md

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

2
index.html

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

43
src/playlist-loader.js

@ -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;
@ -523,7 +524,7 @@ 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,
@ -531,6 +532,19 @@ export default class PlaylistLoader extends EventTarget {
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 = {
@ -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.
*

2
src/util/codecs.js

@ -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 || {};

3
src/videojs-http-streaming.js

@ -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 || [];

2
test/manifests/multipleAudioGroupsCombinedMain.m3u8

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

2
test/manifests/two-renditions.m3u8

@ -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
test/playlist-controller.test.js

@ -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(),
@ -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'
);
});
});
@ -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(),
@ -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'
);
});
});
@ -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(

12
test/test-helpers.js

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

38
test/videojs-http-streaming.test.js

@ -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'
);
@ -1269,8 +1269,8 @@ 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;
@ -1278,7 +1278,7 @@ QUnit.test('selects a playlist below the current bandwidth', function(assert) {
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'
);
@ -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'
);
@ -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'
);
@ -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'
);
@ -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();
@ -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
@ -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
@ -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;
@ -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_ = () => {};
@ -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_ = () => {};
@ -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(
@ -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');
@ -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');
@ -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()
@ -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()

Loading…
Cancel
Save