Fix browser detection overwrites (#7411)

* Fix browser detection overwrites

* Fix Opera TV detection

* Move tv flag logic

* Fix indentation
This commit is contained in:
Bill Thornton
2025-12-10 09:54:08 -05:00
committed by GitHub
parent b3725e9dd5
commit 6bfff061ce
3 changed files with 127 additions and 104 deletions

View File

@@ -37,4 +37,6 @@ declare namespace browser {
export let iOSVersion: number | undefined;
}
export function detectBrowser(userAgent?: string): browser;
export default browser;

View File

@@ -1,6 +1,5 @@
function isTv() {
function isTv(userAgent) {
// This is going to be really difficult to get right
const userAgent = navigator.userAgent.toLowerCase();
// The OculusBrowsers userAgent also has the samsungbrowser defined but is not a tv.
if (userAgent.includes('oculusbrowser')) {
@@ -23,12 +22,10 @@ function isTv() {
return true;
}
return isWeb0s();
return isWeb0s(userAgent);
}
function isWeb0s() {
const userAgent = navigator.userAgent.toLowerCase();
function isWeb0s(userAgent) {
return userAgent.includes('netcast')
|| userAgent.includes('web0s');
}
@@ -46,10 +43,8 @@ function isMobile(userAgent) {
'opera mini'
];
const lower = userAgent.toLowerCase();
for (let i = 0, length = terms.length; i < length; i++) {
if (lower.includes(terms[i])) {
for (const term of terms) {
if (userAgent.includes(term)) {
return true;
}
}
@@ -191,8 +186,7 @@ function supportsCssAnimation(allowPrefix) {
}
const uaMatch = function (ua) {
ua = ua.toLowerCase();
// Motorola Edge device UA triggers false positive for Edge browser
ua = ua.replace(/(motorola edge)/, '').trim();
const match = /(edg)[ /]([\w.]+)/.exec(ua)
@@ -248,99 +242,98 @@ const uaMatch = function (ua) {
};
};
const userAgent = navigator.userAgent;
export const detectBrowser = (userAgent = navigator.userAgent) => {
const normalizedUA = userAgent.toLowerCase();
const matched = uaMatch(userAgent);
const browser = {};
const matched = uaMatch(normalizedUA);
const browser = {};
if (matched.browser) {
browser[matched.browser] = true;
browser.version = matched.version;
browser.versionMajor = matched.versionMajor;
}
if (matched.platform) {
browser[matched.platform] = true;
}
browser.edgeChromium = browser.edg || browser.edga || browser.edgios;
if (!browser.chrome && !browser.edgeChromium && !browser.edge && !browser.opera && userAgent.toLowerCase().includes('webkit')) {
browser.safari = true;
}
browser.osx = userAgent.toLowerCase().includes('mac os x');
// This is a workaround to detect iPads on iOS 13+ that report as desktop Safari
// This may break in the future if Apple releases a touchscreen Mac
// https://forums.developer.apple.com/thread/119186
if (browser.osx && !browser.iphone && !browser.ipod && !browser.ipad && navigator.maxTouchPoints > 1) {
browser.ipad = true;
}
if (userAgent.toLowerCase().includes('playstation 4')) {
browser.ps4 = true;
browser.tv = true;
}
if (isMobile(userAgent)) {
browser.mobile = true;
}
if (userAgent.toLowerCase().includes('xbox')) {
browser.xboxOne = true;
browser.tv = true;
}
browser.animate = typeof document !== 'undefined' && document.documentElement.animate != null;
browser.hisense = userAgent.toLowerCase().includes('hisense');
browser.tizen = userAgent.toLowerCase().includes('tizen') || window.tizen != null;
browser.vidaa = userAgent.toLowerCase().includes('vidaa');
browser.web0s = isWeb0s();
browser.edgeUwp = (browser.edge || browser.edgeChromium) && (userAgent.toLowerCase().includes('msapphost') || userAgent.toLowerCase().includes('webview'));
if (browser.web0s) {
browser.web0sVersion = web0sVersion(browser);
// UserAgent string contains 'Chrome' and 'Safari', but we only want 'web0s' to be true
delete browser.chrome;
delete browser.safari;
} else if (browser.tizen) {
const v = RegExp(/Tizen (\d+).(\d+)/).exec(userAgent);
browser.tizenVersion = parseInt(v[1], 10) + parseInt(v[2], 10) / 10;
// UserAgent string contains 'Chrome' and 'Safari', but we only want 'tizen' to be true
delete browser.chrome;
delete browser.safari;
} else if (browser.titanos) {
// UserAgent string contains 'Opr' and 'Safari', but we only want 'titanos' to be true
delete browser.operaTv;
delete browser.safari;
} else {
browser.orsay = userAgent.toLowerCase().includes('smarthub');
}
browser.tv = isTv();
browser.operaTv = browser.tv && userAgent.toLowerCase().includes('opr/');
if (browser.mobile || browser.tv) {
browser.slow = true;
}
if (typeof document !== 'undefined' && ('ontouchstart' in window) || (navigator.maxTouchPoints > 0)) {
browser.touch = true;
}
browser.keyboard = hasKeyboard(browser);
browser.supportsCssAnimation = supportsCssAnimation;
browser.iOS = browser.ipad || browser.iphone || browser.ipod;
if (browser.iOS) {
browser.iOSVersion = iOSversion();
if (browser.iOSVersion && browser.iOSVersion.length >= 2) {
browser.iOSVersion = browser.iOSVersion[0] + (browser.iOSVersion[1] / 10);
if (matched.browser) {
browser[matched.browser] = true;
browser.version = matched.version;
browser.versionMajor = matched.versionMajor;
}
}
export default browser;
if (matched.platform) {
browser[matched.platform] = true;
}
browser.edgeChromium = browser.edg || browser.edga || browser.edgios;
if (!browser.chrome && !browser.edgeChromium && !browser.edge && !browser.opera && normalizedUA.includes('webkit')) {
browser.safari = true;
}
browser.osx = normalizedUA.includes('mac os x');
// This is a workaround to detect iPads on iOS 13+ that report as desktop Safari
// This may break in the future if Apple releases a touchscreen Mac
// https://forums.developer.apple.com/thread/119186
if (browser.osx && !browser.iphone && !browser.ipod && !browser.ipad && navigator.maxTouchPoints > 1) {
browser.ipad = true;
}
if (isMobile(normalizedUA)) {
browser.mobile = true;
}
browser.ps4 = normalizedUA.includes('playstation 4');
browser.xboxOne = normalizedUA.includes('xbox');
browser.animate = typeof document !== 'undefined' && document.documentElement.animate != null;
browser.hisense = normalizedUA.includes('hisense');
browser.tizen = normalizedUA.includes('tizen') || window.tizen != null;
browser.vidaa = normalizedUA.includes('vidaa');
browser.web0s = isWeb0s(normalizedUA);
browser.tv = browser.ps4 || browser.xboxOne || isTv(normalizedUA);
browser.operaTv = browser.tv && normalizedUA.includes('opr/');
browser.edgeUwp = (browser.edge || browser.edgeChromium) && (normalizedUA.includes('msapphost') || normalizedUA.includes('webview'));
if (browser.web0s) {
browser.web0sVersion = web0sVersion(browser);
// UserAgent string contains 'Chrome' and 'Safari', but we only want 'web0s' to be true
delete browser.chrome;
delete browser.safari;
} else if (browser.tizen) {
const v = RegExp(/Tizen (\d+).(\d+)/).exec(userAgent);
browser.tizenVersion = parseInt(v[1], 10) + parseInt(v[2], 10) / 10;
// UserAgent string contains 'Chrome' and 'Safari', but we only want 'tizen' to be true
delete browser.chrome;
delete browser.safari;
} else if (browser.titanos) {
// UserAgent string contains 'Opr' and 'Safari', but we only want 'titanos' to be true
delete browser.operaTv;
delete browser.safari;
} else {
browser.orsay = normalizedUA.includes('smarthub');
}
if (browser.mobile || browser.tv) {
browser.slow = true;
}
if (typeof document !== 'undefined' && ('ontouchstart' in window) || (navigator.maxTouchPoints > 0)) {
browser.touch = true;
}
browser.keyboard = hasKeyboard(browser);
browser.supportsCssAnimation = supportsCssAnimation;
browser.iOS = browser.ipad || browser.iphone || browser.ipod;
if (browser.iOS) {
browser.iOSVersion = iOSversion();
if (browser.iOSVersion && browser.iOSVersion.length >= 2) {
browser.iOSVersion = browser.iOSVersion[0] + (browser.iOSVersion[1] / 10);
}
}
return browser;
};
export default detectBrowser();

View File

@@ -0,0 +1,28 @@
import { describe, expect, it } from 'vitest';
import { detectBrowser } from './browser';
describe('Browser', () => {
it('should identify TitanOS devices', async () => {
// Ref: https://docs.titanos.tv/user-agents-specifications
// Philips example
let browser = detectBrowser('Mozilla/5.0 (Linux armv7l) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.4147.62 Safari/537.36 OPR/46.0.2207.0 OMI/4.24, TV_NT72690_2025_4K /<SW version> (Philips, <CTN>, wired) CE-HTML/1.0 NETTV/4.6.0.8 SignOn/2.0 SmartTvA/5.0.0 TitanOS/3.0 en Ginga');
expect(browser.titanos).toBe(true);
expect(browser.operaTv).toBeFalsy();
expect(browser.safari).toBeFalsy();
expect(browser.tv).toBe(true);
// JVC example
browser = detectBrowser('Mozilla/5.0 (Linux ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.128 Safari/537.36 OMI/4.24.3.93.MIKE.227 Model/Vestel-MB190 VSTVB MB100 FVC/9.0 (VESTEL; MB190; ) HbbTV/1.7.1 (+DRM; VESTEL; MB190; 0.9.0.0; ; _TV__2025;) TitanOS/3.0 (Vestel MB190 VESTEL) SmartTvA/3.0.0');
expect(browser.titanos).toBe(true);
expect(browser.operaTv).toBeFalsy();
expect(browser.safari).toBeFalsy();
expect(browser.tv).toBe(true);
});
it('should identify Xbox devices', async () => {
const browser = detectBrowser('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0 WebView2 Xbox');
expect(browser.xboxOne).toBe(true);
expect(browser.tv).toBe(true);
});
});