From c1898b477d1c0f1da1bfc7303feabd5a999836c3 Mon Sep 17 00:00:00 2001 From: mister-ben Date: Tue, 22 Nov 2022 23:16:32 +0100 Subject: [PATCH] feat: Use userAgentData in favour of userAgent (#7979) --- src/js/tech/html5.js | 41 +------ src/js/utils/browser.js | 229 +++++++++++++++++++++-------------- test/unit/tech/html5.test.js | 135 --------------------- 3 files changed, 142 insertions(+), 263 deletions(-) diff --git a/src/js/tech/html5.js b/src/js/tech/html5.js index c75e681286..b646f9a129 100644 --- a/src/js/tech/html5.js +++ b/src/js/tech/html5.js @@ -105,8 +105,7 @@ class Html5 extends Tech { // Our goal should be to get the custom controls on mobile solid everywhere // so we can remove this all together. Right now this will block custom // controls on touch enabled laptops like the Chrome Pixel - if ((browser.TOUCH_ENABLED || browser.IS_IPHONE || - browser.IS_NATIVE_ANDROID) && options.nativeControlsForTouch === true) { + if ((browser.TOUCH_ENABLED || browser.IS_IPHONE) && options.nativeControlsForTouch === true) { this.setControls(true); } @@ -675,10 +674,8 @@ class Html5 extends Tech { */ supportsFullScreen() { if (typeof this.el_.webkitEnterFullScreen === 'function') { - const userAgent = window.navigator && window.navigator.userAgent || ''; - - // Seems to be broken in Chromium/Chrome && Safari in Leopard - if ((/Android/).test(userAgent) || !(/Chrome|Mac OS X 10.5/).test(userAgent)) { + // Still needed? + if (browser.IS_ANDROID) { return true; } } @@ -1330,38 +1327,6 @@ Html5.prototype.featuresTimeupdateEvents = true; */ Html5.prototype.featuresVideoFrameCallback = !!(Html5.TEST_VID && Html5.TEST_VID.requestVideoFrameCallback); -// HTML5 Feature detection and Device Fixes --------------------------------- // -let canPlayType; - -Html5.patchCanPlayType = function() { - - // Android 4.0 and above can play HLS to some extent but it reports being unable to do so - // Firefox and Chrome report correctly - if (browser.ANDROID_VERSION >= 4.0 && !browser.IS_FIREFOX && !browser.IS_CHROME) { - canPlayType = Html5.TEST_VID && Html5.TEST_VID.constructor.prototype.canPlayType; - Html5.TEST_VID.constructor.prototype.canPlayType = function(type) { - const mpegurlRE = /^application\/(?:x-|vnd\.apple\.)mpegurl/i; - - if (type && mpegurlRE.test(type)) { - return 'maybe'; - } - return canPlayType.call(this, type); - }; - } -}; - -Html5.unpatchCanPlayType = function() { - const r = Html5.TEST_VID.constructor.prototype.canPlayType; - - if (canPlayType) { - Html5.TEST_VID.constructor.prototype.canPlayType = canPlayType; - } - return r; -}; - -// by default, patch the media element -Html5.patchCanPlayType(); - Html5.disposeMediaElement = function(el) { if (!el) { return; diff --git a/src/js/utils/browser.js b/src/js/utils/browser.js index 8c2ecc1433..4b30ef6bd6 100644 --- a/src/js/utils/browser.js +++ b/src/js/utils/browser.js @@ -5,196 +5,245 @@ import * as Dom from './dom'; import window from 'global/window'; -const USER_AGENT = window.navigator && window.navigator.userAgent || ''; -const webkitVersionMap = (/AppleWebKit\/([\d.]+)/i).exec(USER_AGENT); -const appleWebkitVersion = webkitVersionMap ? parseFloat(webkitVersionMap.pop()) : null; - /** * Whether or not this device is an iPod. * * @static - * @const * @type {Boolean} */ -export const IS_IPOD = (/iPod/i).test(USER_AGENT); +export let IS_IPOD = false; /** * The detected iOS version - or `null`. * * @static - * @const * @type {string|null} */ -export const IOS_VERSION = (function() { - const match = USER_AGENT.match(/OS (\d+)_/i); - - if (match && match[1]) { - return match[1]; - } - return null; -}()); +export let IOS_VERSION = null; /** * Whether or not this is an Android device. * * @static - * @const * @type {Boolean} */ -export const IS_ANDROID = (/Android/i).test(USER_AGENT); +export let IS_ANDROID = false; /** - * The detected Android version - or `null`. + * The detected Android version - or `null` if not Android or indeterminable. * * @static - * @const * @type {number|string|null} */ -export const ANDROID_VERSION = (function() { - // This matches Android Major.Minor.Patch versions - // ANDROID_VERSION is Major.Minor as a Number, if Minor isn't available, then only Major is returned - const match = USER_AGENT.match(/Android (\d+)(?:\.(\d+))?(?:\.(\d+))*/i); - - if (!match) { - return null; - } - - const major = match[1] && parseFloat(match[1]); - const minor = match[2] && parseFloat(match[2]); - - if (major && minor) { - return parseFloat(match[1] + '.' + match[2]); - } else if (major) { - return major; - } - return null; -}()); +export let ANDROID_VERSION; /** - * Whether or not this is a native Android browser. + * Whether or not this is Mozilla Firefox. * * @static - * @const * @type {Boolean} */ -export const IS_NATIVE_ANDROID = IS_ANDROID && ANDROID_VERSION < 5 && appleWebkitVersion < 537; +export let IS_FIREFOX = false; /** - * Whether or not this is Mozilla Firefox. + * Whether or not this is Microsoft Edge. * * @static - * @const * @type {Boolean} */ -export const IS_FIREFOX = (/Firefox/i).test(USER_AGENT); +export let IS_EDGE = false; /** - * Whether or not this is Microsoft Edge. + * Whether or not this is any Chromium Browser * * @static - * @const * @type {Boolean} */ -export const IS_EDGE = (/Edg/i).test(USER_AGENT); +export let IS_CHROMIUM = false; /** - * Whether or not this is Google Chrome. + * Whether or not this is any Chromium browser that is not Edge. * * This will also be `true` for Chrome on iOS, which will have different support * as it is actually Safari under the hood. * + * Depreacted, as the behaviour to not match Edge was to prevent Legacy Edge's UA matching. + * IS_CHROMIUM should be used instead. + * "Chromium but not Edge" could be explicitly tested with IS_CHROMIUM && !IS_EDGE + * * @static - * @const + * @deprecated * @type {Boolean} */ -export const IS_CHROME = !IS_EDGE && ((/Chrome/i).test(USER_AGENT) || (/CriOS/i).test(USER_AGENT)); +export let IS_CHROME = false; /** - * The detected Google Chrome version - or `null`. + * The detected Chromium version - or `null`. * * @static - * @const * @type {number|null} */ -export const CHROME_VERSION = (function() { - const match = USER_AGENT.match(/(Chrome|CriOS)\/(\d+)/); +export let CHROMIUM_VERSION = null; - if (match && match[2]) { - return parseFloat(match[2]); - } - return null; -}()); +/** + * The detected Google Chrome version - or `null`. + * This has always been the _Chromium_ version, i.e. would return on Chromium Edge. + * Depreacted, use CHROMIUM_VERSION instead. + * + * @static + * @deprecated + * @type {number|null} + */ +export let CHROME_VERSION = null; /** * The detected Internet Explorer version - or `null`. * * @static - * @const + * @deprecated * @type {number|null} */ -export const IE_VERSION = (function() { - const result = (/MSIE\s(\d+)\.\d/).exec(USER_AGENT); - let version = result && parseFloat(result[1]); - - if (!version && (/Trident\/7.0/i).test(USER_AGENT) && (/rv:11.0/).test(USER_AGENT)) { - // IE 11 has a different user agent string than other IE versions - version = 11.0; - } - - return version; -}()); +export let IE_VERSION = null; /** * Whether or not this is desktop Safari. * * @static - * @const * @type {Boolean} */ -export const IS_SAFARI = (/Safari/i).test(USER_AGENT) && !IS_CHROME && !IS_ANDROID && !IS_EDGE; +export let IS_SAFARI = false; /** * Whether or not this is a Windows machine. * * @static - * @const * @type {Boolean} */ -export const IS_WINDOWS = (/Windows/i).test(USER_AGENT); +export let IS_WINDOWS = false; /** - * Whether or not this device is touch-enabled. + * Whether or not this device is an iPad. * * @static - * @const * @type {Boolean} */ -export const TOUCH_ENABLED = Boolean(Dom.isReal() && ( - 'ontouchstart' in window || - window.navigator.maxTouchPoints || - window.DocumentTouch && window.document instanceof window.DocumentTouch)); +export let IS_IPAD = false; /** - * Whether or not this device is an iPad. + * Whether or not this device is an iPhone. * * @static - * @const * @type {Boolean} */ -export const IS_IPAD = (/iPad/i).test(USER_AGENT) || - (IS_SAFARI && TOUCH_ENABLED && !(/iPhone/i).test(USER_AGENT)); +// The Facebook app's UIWebView identifies as both an iPhone and iPad, so +// to identify iPhones, we need to exclude iPads. +// http://artsy.github.io/blog/2012/10/18/the-perils-of-ios-user-agent-sniffing/ +export let IS_IPHONE = false; /** - * Whether or not this device is an iPhone. + * Whether or not this device is touch-enabled. * * @static * @const * @type {Boolean} */ -// The Facebook app's UIWebView identifies as both an iPhone and iPad, so -// to identify iPhones, we need to exclude iPads. -// http://artsy.github.io/blog/2012/10/18/the-perils-of-ios-user-agent-sniffing/ -export const IS_IPHONE = (/iPhone/i).test(USER_AGENT) && !IS_IPAD; +export const TOUCH_ENABLED = Boolean(Dom.isReal() && ( + 'ontouchstart' in window || + window.navigator.maxTouchPoints || + window.DocumentTouch && window.document instanceof window.DocumentTouch)); + +const UAD = window.navigator && window.navigator.userAgentData; + +if (UAD) { + // If userAgentData is present, use it instead of userAgent to avoid warnings + // Currently only implemented on Chromium + // userAgentData does not expose Android version, so ANDROID_VERSION remains `null` + + IS_ANDROID = UAD.platform === 'Android'; + IS_EDGE = Boolean(UAD.brands.find(b => b.brand === 'Microsoft Edge')); + IS_CHROMIUM = Boolean(UAD.brands.find(b => b.brand === 'Chromium')); + IS_CHROME = !IS_EDGE && IS_CHROMIUM; + CHROMIUM_VERSION = CHROME_VERSION = (UAD.brands.find(b => b.brand === 'Chromium') || {}).version || null; + IS_WINDOWS = UAD.platform === 'Windows'; +} + +// If the broser is not Chromium, either userAgentData is not present which could be an old Chromium browser, +// or it's a browser that has added userAgentData since that we don't have tests for yet. In either case, +// the checks need to be made agiainst the regular userAgent string. +if (!IS_CHROMIUM) { + const USER_AGENT = window.navigator && window.navigator.userAgent || ''; + + IS_IPOD = (/iPod/i).test(USER_AGENT); + + IOS_VERSION = (function() { + const match = USER_AGENT.match(/OS (\d+)_/i); + + if (match && match[1]) { + return match[1]; + } + return null; + }()); + + IS_ANDROID = (/Android/i).test(USER_AGENT); + + ANDROID_VERSION = (function() { + // This matches Android Major.Minor.Patch versions + // ANDROID_VERSION is Major.Minor as a Number, if Minor isn't available, then only Major is returned + const match = USER_AGENT.match(/Android (\d+)(?:\.(\d+))?(?:\.(\d+))*/i); + + if (!match) { + return null; + } + + const major = match[1] && parseFloat(match[1]); + const minor = match[2] && parseFloat(match[2]); + + if (major && minor) { + return parseFloat(match[1] + '.' + match[2]); + } else if (major) { + return major; + } + return null; + }()); + + IS_FIREFOX = (/Firefox/i).test(USER_AGENT); + + IS_EDGE = (/Edg/i).test(USER_AGENT); + + IS_CHROMIUM = ((/Chrome/i).test(USER_AGENT) || (/CriOS/i).test(USER_AGENT)); + + IS_CHROME = !IS_EDGE && IS_CHROMIUM; + + CHROMIUM_VERSION = CHROME_VERSION = (function() { + const match = USER_AGENT.match(/(Chrome|CriOS)\/(\d+)/); + + if (match && match[2]) { + return parseFloat(match[2]); + } + return null; + }()); + + IE_VERSION = (function() { + const result = (/MSIE\s(\d+)\.\d/).exec(USER_AGENT); + let version = result && parseFloat(result[1]); + + if (!version && (/Trident\/7.0/i).test(USER_AGENT) && (/rv:11.0/).test(USER_AGENT)) { + // IE 11 has a different user agent string than other IE versions + version = 11.0; + } + + return version; + }()); + + IS_SAFARI = (/Safari/i).test(USER_AGENT) && !IS_CHROME && !IS_ANDROID && !IS_EDGE; + + IS_WINDOWS = (/Windows/i).test(USER_AGENT); + + IS_IPAD = (/iPad/i).test(USER_AGENT) || + (IS_SAFARI && TOUCH_ENABLED && !(/iPhone/i).test(USER_AGENT)); + + IS_IPHONE = (/iPhone/i).test(USER_AGENT) && !IS_IPAD; +} /** * Whether or not this is an iOS device. diff --git a/test/unit/tech/html5.test.js b/test/unit/tech/html5.test.js index 6aed0d58b1..093d725803 100644 --- a/test/unit/tech/html5.test.js +++ b/test/unit/tech/html5.test.js @@ -218,141 +218,6 @@ QUnit.test('should remove the controls attribute when recreating the element', f assert.ok(player.tagAttributes.controls, 'tag attribute is still present'); }); -QUnit.test('patchCanPlayType patches canplaytype with our function, conditionally', function(assert) { - // the patch runs automatically so we need to first unpatch - Html5.unpatchCanPlayType(); - - const oldAV = browser.ANDROID_VERSION; - const oldIsFirefox = browser.IS_FIREFOX; - const oldIsChrome = browser.IS_CHROME; - const video = document.createElement('video'); - const canPlayType = Html5.TEST_VID.constructor.prototype.canPlayType; - - browser.stub_ANDROID_VERSION(4.0); - browser.stub_IS_FIREFOX(false); - browser.stub_IS_CHROME(false); - Html5.patchCanPlayType(); - - assert.notStrictEqual( - video.canPlayType, - canPlayType, - 'original canPlayType and patched canPlayType should not be equal' - ); - - const patchedCanPlayType = video.canPlayType; - const unpatchedCanPlayType = Html5.unpatchCanPlayType(); - - assert.strictEqual( - canPlayType, - Html5.TEST_VID.constructor.prototype.canPlayType, - 'original canPlayType and unpatched canPlayType should be equal' - ); - assert.strictEqual( - patchedCanPlayType, - unpatchedCanPlayType, - 'patched canPlayType and function returned from unpatch are equal' - ); - - browser.stub_ANDROID_VERSION(oldAV); - browser.stub_IS_FIREFOX(oldIsFirefox); - browser.stub_IS_CHROME(oldIsChrome); - Html5.unpatchCanPlayType(); -}); - -QUnit.test('patchCanPlayType doesn\'t patch canplaytype with our function in Chrome for Android', function(assert) { - // the patch runs automatically so we need to first unpatch - Html5.unpatchCanPlayType(); - - const oldAV = browser.ANDROID_VERSION; - const oldIsChrome = browser.IS_CHROME; - const oldIsFirefox = browser.IS_FIREFOX; - const video = document.createElement('video'); - const canPlayType = Html5.TEST_VID.constructor.prototype.canPlayType; - - browser.stub_ANDROID_VERSION(4.0); - browser.stub_IS_CHROME(true); - browser.stub_IS_FIREFOX(false); - Html5.patchCanPlayType(); - - assert.strictEqual( - video.canPlayType, - canPlayType, - 'original canPlayType and patched canPlayType should be equal' - ); - - browser.stub_ANDROID_VERSION(oldAV); - browser.stub_IS_CHROME(oldIsChrome); - browser.stub_IS_FIREFOX(oldIsFirefox); - Html5.unpatchCanPlayType(); -}); - -QUnit.test('patchCanPlayType doesn\'t patch canplaytype with our function in Firefox for Android', function(assert) { - // the patch runs automatically so we need to first unpatch - Html5.unpatchCanPlayType(); - - const oldAV = browser.ANDROID_VERSION; - const oldIsFirefox = browser.IS_FIREFOX; - const oldIsChrome = browser.IS_CHROME; - const video = document.createElement('video'); - const canPlayType = Html5.TEST_VID.constructor.prototype.canPlayType; - - browser.stub_ANDROID_VERSION(4.0); - browser.stub_IS_FIREFOX(true); - browser.stub_IS_CHROME(false); - Html5.patchCanPlayType(); - - assert.strictEqual( - video.canPlayType, - canPlayType, - 'original canPlayType and patched canPlayType should be equal' - ); - - browser.stub_ANDROID_VERSION(oldAV); - browser.stub_IS_FIREFOX(oldIsFirefox); - browser.stub_IS_CHROME(oldIsChrome); - Html5.unpatchCanPlayType(); -}); - -QUnit.test('should return maybe for HLS urls on Android 4.0 or above when not Chrome or Firefox', function(assert) { - const oldAV = browser.ANDROID_VERSION; - const oldIsFirefox = browser.IS_FIREFOX; - const oldIsChrome = browser.IS_CHROME; - const video = document.createElement('video'); - - browser.stub_ANDROID_VERSION(4.0); - browser.stub_IS_FIREFOX(false); - browser.stub_IS_CHROME(false); - Html5.patchCanPlayType(); - - assert.strictEqual( - video.canPlayType('application/x-mpegurl'), - 'maybe', - 'android version 4.0 or above should be a maybe for x-mpegurl' - ); - assert.strictEqual( - video.canPlayType('application/x-mpegURL'), - 'maybe', - 'android version 4.0 or above should be a maybe for x-mpegURL' - ); - assert.strictEqual( - video.canPlayType('application/vnd.apple.mpegurl'), - 'maybe', - 'android version 4.0 or above should be a ' + - 'maybe for vnd.apple.mpegurl' - ); - assert.strictEqual( - video.canPlayType('application/vnd.apple.mpegURL'), - 'maybe', - 'android version 4.0 or above should be a ' + - 'maybe for vnd.apple.mpegurl' - ); - - browser.stub_ANDROID_VERSION(oldAV); - browser.stub_IS_FIREFOX(oldIsFirefox); - browser.stub_IS_CHROME(oldIsChrome); - Html5.unpatchCanPlayType(); -}); - QUnit.test('error events may not set the errors property', function(assert) { assert.equal(tech.error(), undefined, 'no tech-level error'); tech.trigger('error');