From 061cf3c76fba1736506ec8a85dfd188256dfcc97 Mon Sep 17 00:00:00 2001 From: Evan Farina Date: Mon, 8 Nov 2021 12:05:25 -0500 Subject: [PATCH] feat: Add an option to use the NetworkInformation API, when available (#1218) When enabled, if the NetworkInformation API is available, it will be used for bandwidth estimation, If our estimation is greater than 10MBps and the downlink returns 10MBps, then our estimation is used. --- README.md | 6 ++ index.html | 5 ++ scripts/index.js | 5 +- src/videojs-http-streaming.js | 24 +++++- test/videojs-http-streaming.test.js | 114 ++++++++++++++++++++++++++++ 5 files changed, 152 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 27f28ceb4..ef4c480eb 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ Video.js Compatibility: 6.0, 7.0 - [cacheEncryptionKeys](#cacheencryptionkeys) - [handlePartialData](#handlepartialdata) - [liveRangeSafeTimeDelta](#liverangesafetimedelta) + - [useNetworkInformationApi](#usenetworkinformationapi) - [captionServices](#captionservices) - [Format](#format) - [Example](#example) @@ -473,6 +474,11 @@ This option defaults to `false`. * Default: [`SAFE_TIME_DELTA`](https://github.com/videojs/http-streaming/blob/e7cb63af010779108336eddb5c8fd138d6390e95/src/ranges.js#L17) * Allow to re-define length (in seconds) of time delta when you compare current time and the end of the buffered range. +##### useNetworkInformationApi +* Type: `boolean`, +* Default: `false` +* 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. + ##### captionServices * Type: `object` * Default: undefined diff --git a/index.html b/index.html index ef2d08342..77ccf83fd 100644 --- a/index.html +++ b/index.html @@ -146,6 +146,11 @@ +
+ + +
+
diff --git a/scripts/index.js b/scripts/index.js index 27c314d3f..4a6d2c35e 100644 --- a/scripts/index.js +++ b/scripts/index.js @@ -447,6 +447,7 @@ 'buffer-water', 'exact-manifest-timings', 'pixel-diff-selector', + 'network-info', 'override-native', 'preload', 'mirror-source' @@ -499,6 +500,7 @@ 'override-native', 'liveui', 'pixel-diff-selector', + 'network-info', 'exact-manifest-timings' ].forEach(function(name) { stateEls[name].addEventListener('change', function(event) { @@ -565,7 +567,8 @@ experimentalBufferBasedABR: getInputValue(stateEls['buffer-water']), experimentalLLHLS: getInputValue(stateEls.llhls), experimentalExactManifestTimings: getInputValue(stateEls['exact-manifest-timings']), - experimentalLeastPixelDiffSelector: getInputValue(stateEls['pixel-diff-selector']) + experimentalLeastPixelDiffSelector: getInputValue(stateEls['pixel-diff-selector']), + useNetworkInformationApi: getInputValue(stateEls['network-info']) } } }); diff --git a/src/videojs-http-streaming.js b/src/videojs-http-streaming.js index d5e78284d..813ed4230 100644 --- a/src/videojs-http-streaming.js +++ b/src/videojs-http-streaming.js @@ -630,6 +630,7 @@ class VhsHandler extends Component { typeof this.source_.useBandwidthFromLocalStorage !== 'undefined' ? this.source_.useBandwidthFromLocalStorage : this.options_.useBandwidthFromLocalStorage || false; + this.options_.useNetworkInformationApi = this.options_.useNetworkInformationApi || false; this.options_.customTagParsers = this.options_.customTagParsers || []; this.options_.customTagMappers = this.options_.customTagMappers || []; this.options_.cacheEncryptionKeys = this.options_.cacheEncryptionKeys || false; @@ -682,6 +683,7 @@ class VhsHandler extends Component { 'experimentalBufferBasedABR', 'liveRangeSafeTimeDelta', 'experimentalLLHLS', + 'useNetworkInformationApi', 'experimentalExactManifestTimings', 'experimentalLeastPixelDiffSelector' ].forEach((option) => { @@ -789,7 +791,27 @@ class VhsHandler extends Component { }, bandwidth: { get() { - return this.masterPlaylistController_.mainSegmentLoader_.bandwidth; + let playerBandwidthEst = this.masterPlaylistController_.mainSegmentLoader_.bandwidth; + + const networkInformation = window.navigator.connection || window.navigator.mozConnection || window.navigator.webkitConnection; + const tenMbpsAsBitsPerSecond = 10e6; + + if (this.options_.useNetworkInformationApi && networkInformation) { + // downlink returns Mbps + // https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/downlink + const networkInfoBandwidthEstBitsPerSec = networkInformation.downlink * 1000 * 1000; + + // downlink maxes out at 10 Mbps. In the event that both networkInformationApi and the player + // estimate a bandwidth greater than 10 Mbps, use the larger of the two estimates to ensure that + // high quality streams are not filtered out. + if (networkInfoBandwidthEstBitsPerSec >= tenMbpsAsBitsPerSecond && playerBandwidthEst >= tenMbpsAsBitsPerSecond) { + playerBandwidthEst = Math.max(playerBandwidthEst, networkInfoBandwidthEstBitsPerSec); + } else { + playerBandwidthEst = networkInfoBandwidthEstBitsPerSec; + } + } + + return playerBandwidthEst; }, set(bandwidth) { this.masterPlaylistController_.mainSegmentLoader_.bandwidth = bandwidth; diff --git a/test/videojs-http-streaming.test.js b/test/videojs-http-streaming.test.js index dd552cd90..2825d6c95 100644 --- a/test/videojs-http-streaming.test.js +++ b/test/videojs-http-streaming.test.js @@ -1160,6 +1160,120 @@ QUnit.test( } ); +QUnit.module('NetworkInformationApi', hooks => { + hooks.beforeEach(function(assert) { + this.env = useFakeEnvironment(assert); + this.ogNavigator = window.navigator; + this.clock = this.env.clock; + + this.resetNavigatorConnection = (connection = {}) => { + // Need to delete the property before setting since navigator doesn't have a setter + delete window.navigator; + window.navigator = { + connection + }; + }; + }); + + hooks.afterEach(function() { + this.env.restore(); + window.navigator = this.ogNavigator; + }); + + QUnit.test( + 'bandwidth returns networkInformation.downlink when useNetworkInformationApi option is enabled', + function(assert) { + this.resetNavigatorConnection({ + downlink: 10 + }); + this.player = createPlayer({ html5: { vhs: { useNetworkInformationApi: true } } }); + this.player.src({ + src: 'manifest/master.m3u8', + type: 'application/vnd.apple.mpegurl' + }); + + this.clock.tick(1); + + // downlink in bits = 10 * 1000000 = 10e6 + assert.strictEqual( + this.player.tech_.vhs.bandwidth, + 10e6, + 'bandwidth equals networkInfo.downlink represented as bits per second' + ); + } + ); + + QUnit.test( + 'bandwidth uses player-estimated bandwidth when its value is greater than networkInformation.downLink and both values are >= 10 Mbps', + function(assert) { + this.resetNavigatorConnection({ + // 10 Mbps or 10e6 + downlink: 10 + }); + this.player = createPlayer({ html5: { vhs: { useNetworkInformationApi: true } } }); + this.player.src({ + src: 'manifest/master.m3u8', + type: 'application/vnd.apple.mpegurl' + }); + + this.clock.tick(1); + + this.player.tech_.vhs.bandwidth = 20e6; + assert.strictEqual( + this.player.tech_.vhs.bandwidth, + 20e6, + 'bandwidth getter returned the player-estimated bandwidth value' + ); + } + ); + + QUnit.test( + 'bandwidth uses network-information-api bandwidth when its value is less than the player bandwidth and 10 Mbps', + function(assert) { + this.resetNavigatorConnection({ + // 9 Mbps or 9e6 + downlink: 9 + }); + this.player = createPlayer({ html5: { vhs: { useNetworkInformationApi: true } } }); + this.player.src({ + src: 'manifest/master.m3u8', + type: 'application/vnd.apple.mpegurl' + }); + + this.clock.tick(1); + + this.player.tech_.vhs.bandwidth = 20e10; + assert.strictEqual( + this.player.tech_.vhs.bandwidth, + 9e6, + 'bandwidth getter returned the network-information-api bandwidth value since it was less than 10 Mbps' + ); + } + ); + + QUnit.test( + 'bandwidth uses player-estimated bandwidth when networkInformation is not supported', + function(assert) { + // Nullify the `connection` property on Navigator + this.resetNavigatorConnection(null); + this.player = createPlayer({ html5: { vhs: { useNetworkInformationApi: true } } }); + this.player.src({ + src: 'manifest/master.m3u8', + type: 'application/vnd.apple.mpegurl' + }); + + this.clock.tick(1); + + this.player.tech_.vhs.bandwidth = 20e10; + assert.strictEqual( + this.player.tech_.vhs.bandwidth, + 20e10, + 'bandwidth getter returned the player-estimated bandwidth value' + ); + } + ); +}); + QUnit.test('requests a reasonable rendition to start', function(assert) { this.player.src({ src: 'manifest/master.m3u8',