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',