commit 4489ea2a416acbc6c40484427fb250aff0147c0d Author: redphx <96280+redphx@users.noreply.github.com> Date: Fri Jul 14 08:37:42 2023 +0700 Initial commit diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..649efa4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 redphx + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c3f7463 --- /dev/null +++ b/README.md @@ -0,0 +1,64 @@ +# Better xCloud +Improve [Xbox Cloud Gaming (xCloud)](https://www.xbox.com/play/) experience. +The main target of this script is Android users, but it should work great on desktop too. + +## Features: + +- Switch region of streaming server. +- Prefer IPv6 streaming server (might improve latency). +- Force HD stream by disabling bandwidth checking -> xCloud always tries to use the best possible quality. +- Skip Xbox splash video. +- Make the top-left dots icon invisible while playing. You can still click on it, but it doesn't block the screen anymore. +- Adjust video filters (brightness/contrast/saturation). +- Hide footer and other UI elements. +- Reduce UI animations (the smooth scrolling cannot be disabled). +- Disable analytics. +- Disable social features (friends, chat...). + +## How to use: +1. Install [Tampermonkey extension](https://www.tampermonkey.net/) on suppported browsers. It's also available for Firefox on Android. +2. Install **Better xCloud**: + - [Directly on Github](https://github.com/redphx/better-xcloud/raw/main/better-xcloud.user.js) +4. Refresh [xCloud web page](https://www.xbox.com/play/). +5. Click on the new "SERVER NAME" button next to your profile picture to adjust settings. +6. Optional but recommended: change your browser's User-Agent. Check the [User-Agent section](#user-agent) below for more info. +7. Don't forget to reload the page after changing settings. + +## User-Agent +Optional, as changing User-Agent won't guarantee a better streaming experience, but it's worth a try. You might need to install an external extension to do that. + +It's recommended to change User-Agent to: +``` +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1823.67 +``` +This will trick xCloud into thinking you're using Edge browser on desktop. + +Other options (only do one of these): +- Add ` smarttv` to switch to Smart TV layout. +- Add ` Xbox;` to become an Xbox console. +- Add ` 36102dd3-6953-45f6-8b48-031fb95e0e0d` to become a Logitech G Cloud device. +- Add ` 0ed22b6f-b61d-41eb-810a-a1ed586a550b` to become a Razer Edge device. + +## Tested on: +- Chrome on macOS. +- Firefox for Android with Tampermonkey add-on. +- *(NOT RECOMMENDED at the moment since its Userscript implementation is not working properly)* [Hermit Browser](https://hermit.chimbori.com) on Android. It supports custom User-Agent and has built-in Userscript support (premium features, only $7.99) so you don't have to install anything else. I built **Better xCloud** just so I could use it with Hermit. + +## FAQ +1. **Why is it an Userscript and not extension?** +It's because not many browsers on Android support installing extensions (and not all extensions can be installed). + +2. **I see "???" button instead of server's name** +That means Tampermonkey is not working properly. Please make sure you're using the latest version or switch to a well-known browser. + +3. **Can I use this with the Xbox Android app?** +No you can't. You'll have to modidy the app. + +## Acknowledgements +**Better xCloud** is inspired by these projects: +- [n-thumann/xbox-cloud-server-selector](https://github.com/n-thumann/xbox-cloud-server-selector) + +## Disclaimers +- Use as your own risk. +- This project is not affiliated with Xbox in any way. All Xbox logos/icons/trademarks are copyright of their respective owners. + diff --git a/better-xcloud.user.js b/better-xcloud.user.js new file mode 100644 index 0000000..a1fb3e8 --- /dev/null +++ b/better-xcloud.user.js @@ -0,0 +1,798 @@ +// ==UserScript== +// @name Better xCloud +// @namespace https://github.com/redphx +// @version 1.0 +// @description Improve Xbox Cloud Gaming (xCloud) experience +// @author redphx +// @license MIT +// @match https://www.xbox.com/*/play* +// @run-at document-start +// @grant none +// @updateURL https://github.com/redphx/better-xcloud/raw/main/better-xcloud.user.js +// @downloadURL https://github.com/redphx/better-xcloud/raw/main/better-xcloud.user.js +// ==/UserScript== +'use strict'; + +const SCRIPT_VERSION = '1.0'; +const SCRIPT_HOME = 'https://github.com/redphx/better-xcloud'; + +const SERVER_REGIONS = {}; + + +class Preferences { + static get SERVER_REGION() { return 'server_region'; } + static get PREFER_IPV6_SERVER() { return 'prefer_ipv6_server'; } + + static get BLOCK_TRACKING() { return 'block_tracking'; } + static get BLOCK_SOCIAL_FEATURES() { return 'block_social_features'; } + static get DISABLE_BANDWIDTH_CHECKING() { return 'disable_bandwidth_checking'; } + static get SKIP_SPLASH_VIDEO() { return 'skip_splash_video'; } + static get HIDE_DOTS_ICON() { return 'hide_dots_icon'; } + static get REDUCE_ANIMATIONS() { return 'reduce_animations'; } + + static get VIDEO_BRIGHTNESS() { return 'video_brightness'; } + static get VIDEO_CONTRAST() { return 'video_contrast'; } + static get VIDEO_SATURATION() { return 'video_saturation'; } + + static SETTINGS = [ + { + 'id': Preferences.SERVER_REGION, + 'label': 'Region of streaming server', + 'default': 'default', + }, + + { + 'id': Preferences.PREFER_IPV6_SERVER, + 'label': 'Prefer IPv6 streaming server', + 'default': false, + }, + + { + 'id': Preferences.DISABLE_BANDWIDTH_CHECKING, + 'label': 'Force HD stream', + 'default': false, + }, + + { + 'id': Preferences.SKIP_SPLASH_VIDEO, + 'label': 'Skip Xbox splash video', + 'default': false, + }, + + { + 'id': Preferences.HIDE_DOTS_ICON, + 'label': 'Hide Dots icon while playing', + 'default': false, + }, + + { + 'id': Preferences.REDUCE_ANIMATIONS, + 'label': 'Reduce UI animations', + 'default': false, + }, + + { + 'id': Preferences.BLOCK_SOCIAL_FEATURES, + 'label': 'Disable social features (Friends, Chat...)', + 'default': false, + }, + + { + 'id': Preferences.BLOCK_TRACKING, + 'label': 'Disable analytics', + 'default': false, + }, + + { + 'id': Preferences.VIDEO_SATURATION, + 'label': 'Video saturation (%)', + 'default': 100, + 'min': 0, + 'max': 200, + }, + + { + 'id': Preferences.VIDEO_CONTRAST, + 'label': 'Video contrast (%)', + 'default': 100, + 'min': 0, + 'max': 200, + }, + + { + 'id': Preferences.VIDEO_BRIGHTNESS, + 'label': 'Video brightness (%)', + 'default': 100, + 'min': 0, + 'max': 200, + }, + ] + + constructor() { + this._storage = localStorage; + this._key = 'better_xcloud'; + + let savedPrefs = this._storage.getItem(this._key); + if (savedPrefs == null) { + savedPrefs = '{}'; + } + savedPrefs = JSON.parse(savedPrefs); + + this._prefs = {}; + for (let setting of Preferences.SETTINGS) { + if (setting.id in savedPrefs) { + this._prefs[setting.id] = savedPrefs[setting.id]; + } else { + this._prefs[setting.id] = setting.default; + } + } + } + + get(key, defaultValue=null) { + const value = this._prefs[key]; + + if (typeof value !== 'undefined' && value !== null && value !== '') { + return value; + } + + if (defaultValue !== null) { + return defaultValue; + } + + // Get default value + for (let setting of Preferences.SETTINGS) { + if (setting.id == key) { + return setting.default; + } + } + + return null; + } + + set(key, value) { + this._prefs[key] = value; + this._update_storage(); + } + + _update_storage() { + this._storage.setItem(this._key, JSON.stringify(this._prefs)); + } +} + + +const PREFS = new Preferences(); + + +function addCss() { + let css = ` +.better_xcloud_settings_button { + background-color: transparent; + border: none; + color: white; + font-weight: bold; + line-height: 30px; + border-radius: 4px; + padding: 8px; +} + +.better_xcloud_settings_button:hover, .better_xlcoud_settings_button:focus { + background-color: #515863; +} + +.better_xcloud_settings { + background-color: #151515; + user-select: none; + color: #fff; + font-family: "Segoe UI", Arial, Helvetica, sans-serif +} + +.better_xcloud_settings_gone { + display: none; +} + +.better_xcloud_settings_wrapper { + width: 400px; + margin: auto; + padding: 12px; +} + +.better_xcloud_settings_wrapper *:focus { + outline: none !important; +} + +.better_xcloud_settings_wrapper a { + font-family: Bahnschrift, Arial, Helvetica, sans-serif; + font-size: 20px; + text-decoration: none; + font-weight: bold; + display: block; + margin-bottom: 8px; +} + +.better_xcloud_settings_wrapper .setting_row { + display: flex; + margin-bottom: 8px; +} + +.better_xcloud_settings_wrapper .setting_row label { + flex: 1; + align-self: center; + margin-bottom: 0; +} + +.better_xcloud_settings_wrapper .setting_row input { + align-self: center; +} + +.better_xcloud_settings_wrapper .setting_button { + padding: 8px 32px; + margin: 10px auto 0; + border: none; + border-radius: 4px; + display: block; + background-color: #044e2a; + text-align: center; + color: white; + text-transform: uppercase; + font-family: Bahnschrift, Arial, Helvetica, sans-serif; + font-weight: 400; + line-height: 24px; +} + +.better_xcloud_settings_wrapper .setting_button:hover { + background-color: #06743f; +} + +.better_xcloud_settings_color_bars { + display: none; + width: 100%; + aspect-ratio: 16/6; + margin-top: 10px; +} + +.better_xcloud_settings_color_bars div { + flex: 1; +} + +/* Hide UI elements */ +#headerArea, #uhfSkipToMain, .uhf-footer { + display: none; +} + +div[class*=NotFocusedDialog] { + position: absolute !important; + top: -9999px !important; + left: -9999px !important; + width: 0px !important; + height: 0px !important; +} + +`; + + // Reduce animations + if (PREFS.get(Preferences.REDUCE_ANIMATIONS)) { + css += 'div[class*=GameImageItem-module], div[class*=ScrollArrows-module] { transition: none !important; }'; + } + + // Hide the top-left dots icon while playing + if (PREFS.get(Preferences.HIDE_DOTS_ICON)) { + css += ` +div[class*=Grip-module__container] { + visibility: hidden; +} + +@media (hover: hover) { + button[class*=GripHandle-module__container]:hover div[class*=Grip-module__container] { + visibility: visible; + } +} + +button[class*=GripHandle-module__container][aria-expanded=true] div[class*=Grip-module__container] { + visibility: visible; +} +`; + } + + const $style = createElement('style', {}, css); + document.documentElement.appendChild($style); +} + + +function getPreferredServerRegion() { + let preferredRegion = PREFS.get(Preferences.SERVER_REGION); + if (preferredRegion in SERVER_REGIONS) { + return preferredRegion; + } + + for (let regionName in SERVER_REGIONS) { + const region = SERVER_REGIONS[regionName]; + if (region.isDefault) { + return regionName; + } + } + + return '???'; +} + + +function updateIceCandidates(candidates) { + const pattern = new RegExp(/a=candidate:(?\d+) (?\d+) UDP (?\d+) (?[^\s]+) (?.*)/); + + const lst = []; + for (let item of candidates) { + if (item.candidate == 'a=end-of-candidates') { + continue; + } + + const groups = pattern.exec(item.candidate).groups; + lst.push(groups); + } + + lst.sort((a, b) => (a.ip.includes(':') || a.ip > b.ip) ? -1 : 1); + + const newCandidates = []; + let order = 1; + let priority = 100; + lst.forEach(item => { + item.order = order; + item.priority = priority; + + newCandidates.push({ + 'candidate': `a=candidate:${item.order} 1 UDP ${item.priority} ${item.ip} ${item.the_rest}`, + 'messageType': 'iceCandidate', + 'sdpMLineIndex': '0', + 'sdpMid': '0', + }); + + ++order; + --priority; + }); + + newCandidates.push({ + 'candidate': 'a=end-of-candidates', + 'messageType': 'iceCandidate', + 'sdpMLineIndex': '0', + 'sdpMid': '0', + }); + + return newCandidates; +} + + +function interceptHttpRequests() { + var BLOCKED_URLS = []; + if (PREFS.get(Preferences.BLOCK_TRACKING)) { + BLOCKED_URLS = BLOCKED_URLS.concat([ + 'https://arc.msn.com', + 'https://browser.events.data.microsoft.com', + 'https://dc.services.visualstudio.com', + // 'https://2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io', + ]); + } + + if (PREFS.get(Preferences.BLOCK_SOCIAL_FEATURES)) { + // Disable WebSocket + WebSocket = { + CLOSING: 2, + }; + + BLOCKED_URLS = BLOCKED_URLS.concat([ + 'https://peoplehub.xboxlive.com/users/me', + 'https://accounts.xboxlive.com/family/memberXuid', + 'https://notificationinbox.xboxlive.com', + ]); + } + + const xhrPrototype = XMLHttpRequest.prototype; + xhrPrototype.orgOpen = xhrPrototype.open; + xhrPrototype.orgSend = xhrPrototype.send; + + xhrPrototype.open = function(method, url) { + // Save URL to use it later in send() + this._url = url; + return this.orgOpen.apply(this, arguments); + }; + + xhrPrototype.send = function(...arg) { + for (let blocked of BLOCKED_URLS) { + if (this._url.startsWith(blocked)) { + return false; + } + } + + return this.orgSend.apply(this, arguments); + }; + + const PREF_PREFER_IPV6_SERVER = PREFS.get(Preferences.PREFER_IPV6_SERVER); + + const orgFetch = window.fetch; + window.fetch = async (...arg) => { + const request = arg[0]; + const url = (typeof request === 'string') ? request : request.url; + + // Server list + if (url.endsWith('/v2/login/user')) { + const promise = orgFetch(...arg); + + return promise.then(response => { + return response.clone().json().then(obj => { + // Get server list + if (!Object.keys(SERVER_REGIONS).length) { + for (let region of obj.offeringSettings.regions) { + SERVER_REGIONS[region.name] = Object.assign({}, region); + } + + // Start rendering UI + if (!document.getElementById('gamepass-root')) { + setTimeout(watchHeader, 2000); + } else { + watchHeader(); + } + } + + const preferredRegion = getPreferredServerRegion(); + if (preferredRegion in SERVER_REGIONS) { + const tmp = Object.assign({}, SERVER_REGIONS[preferredRegion]); + tmp.isDefault = true; + + obj.offeringSettings.regions = [tmp]; + } + + response.json = () => Promise.resolve(obj); + return response; + }); + }); + } + + // ICE server candidates + if (PREF_PREFER_IPV6_SERVER && url.endsWith('/ice') && url.includes('/sessions/cloud/')) { + const promise = orgFetch(...arg); + + return promise.then(response => { + return response.clone().text().then(text => { + if (!text.length) { + return response; + } + + const obj = JSON.parse(text); + let exchangeResponse = JSON.parse(obj.exchangeResponse); + exchangeResponse = updateIceCandidates(exchangeResponse) + obj.exchangeResponse = JSON.stringify(exchangeResponse); + + response.json = () => Promise.resolve(obj); + return response; + }); + }); + } + + for (let blocked of BLOCKED_URLS) { + if (!url.startsWith(blocked)) { + continue; + } + + return new Response('{"acc":1,"webResult":{}}', { + status: 200, + statusText: '200 OK', + }); + } + + return orgFetch(...arg); + } +} + + +// Quickly create a tree of elements without having to use innerHTML +function createElement(elmName, props = {}) { + const $elm = document.createElement(elmName); + for (let key in props) { + if (!props.hasOwnProperty(key) || $elm.hasOwnProperty(key)) { + continue; + } + + let value = props[key]; + $elm.setAttribute(key, value); + } + + for (let i = 2, size = arguments.length; i < size; i++) { + const arg = arguments[i]; + const argType = typeof arg; + + if (argType == 'string' || argType == 'number') { + $elm.innerText = arg; + } else { + $elm.appendChild(arg); + } + } + + return $elm; +} + + +function injectSettingsButton($parent) { + if (!$parent) { + return; + } + + const CE = createElement; + const preferredRegion = getPreferredServerRegion(); + + const $button = CE('button', {'class': 'better_xcloud_settings_button'}, preferredRegion); + $button.addEventListener('click', e => { + const $settings = document.querySelector('.better_xcloud_settings'); + $settings.classList.toggle('better_xcloud_settings_gone'); + $settings.scrollIntoView(); + }); + $parent.appendChild($button); + + const $container = CE('div', { + 'class': 'better_xcloud_settings better_xcloud_settings_gone', + }); + + const $wrapper = CE('div', { + 'class': 'better_xcloud_settings_wrapper', + }); + $container.appendChild($wrapper); + + const $title = CE('a', { + href: SCRIPT_HOME, + target: '_blank', + }, 'Better xCloud ' + SCRIPT_VERSION); + $wrapper.appendChild($title); + + for (let setting of Preferences.SETTINGS) { + let $control; + + if (setting.id === Preferences.SERVER_REGION) { + $control = CE('select', {id: 'xcloud_setting_' + setting.id}); + $control.addEventListener('change', e => { + PREFS.set(Preferences.SERVER_REGION, e.target.value); + }); + + for (let regionName in SERVER_REGIONS) { + const region = SERVER_REGIONS[regionName]; + let value = regionName; + + let label = regionName; + if (region.isDefault) { + label += ' (Default)'; + value = 'default'; + } + + const $option = CE('option', {value: value}, label); + $option.selected = regionName === preferredRegion; + + $control.appendChild($option); + } + } else if (typeof setting.default === 'number') { + $control = CE('input', { + id: 'xcloud_setting_' + setting.id, + type: 'number', + size: 5, + 'data-key': setting.id, + }); + + if ('min' in setting) { + $control.setAttribute('min', setting.min); + } + + if ('max' in setting) { + $control.setAttribute('max', setting.max); + } + + $control.value = PREFS.get(setting.id); + if (setting.id.startsWith('video_')) { + $control.addEventListener('change', e => { + if (!e.target.value) { + return; + } + + PREFS.set(e.target.getAttribute('data-key'), parseInt(e.target.value)); + + const filters = getVideoPlayerFilterStyle(); + const $elm = document.querySelector('.better_xcloud_settings_color_bars'); + $elm.style.display = 'flex'; + $elm.style.filter = filters; + + updateVideoPlayerCss(); + }); + } + } else { + $control = CE('input', { + id: 'xcloud_setting_' + setting.id, + type: 'checkbox', + 'data-key': setting.id, + }); + $control.addEventListener('change', e => { + PREFS.set(e.target.getAttribute('data-key'), e.target.checked); + }); + + setting.value = PREFS.get(setting.id); + $control.checked = setting.value; + } + + const $elm = CE('div', {'class': 'setting_row'}, + CE('label', {'for': 'xcloud_setting_' + setting.id}, setting.label), + $control + ); + + $wrapper.appendChild($elm); + } + + const COLOR_BARS = [ + 'white', + 'yellow', + 'cyan', + 'green', + 'magenta', + 'red', + 'blue', + 'black', + ]; + + const $colorBars = CE('div', {'class': 'better_xcloud_settings_color_bars'}); + COLOR_BARS.forEach(color => { + $colorBars.appendChild(CE('div', { + style: `background-color: ${color}`, + })); + }); + + $wrapper.appendChild($colorBars); + + const $reloadBtn = CE('button', {'class': 'setting_button'}, 'Reload page to reflect changes'); + $reloadBtn.addEventListener('click', e => window.location.reload()); + $wrapper.appendChild($reloadBtn); + + const $pageContent = document.getElementById('PageContent'); + $pageContent.parentNode.insertBefore($container, $pageContent); +} + +function getVideoPlayerFilterStyle() { + const filters = []; + + const saturation = PREFS.get(Preferences.VIDEO_SATURATION); + if (saturation != 100) { + filters.push(`saturate(${saturation}%)`); + } + + const contrast = PREFS.get(Preferences.VIDEO_CONTRAST); + if (contrast != 100) { + filters.push(`contrast(${contrast}%)`); + } + + const brightness = PREFS.get(Preferences.VIDEO_BRIGHTNESS); + if (brightness != 100) { + filters.push(`brightness(${brightness}%)`); + } + + return filters.join(' '); +} + + +function updateVideoPlayerCss() { + let $elm = document.getElementById('better-xcloud-video-css'); + if (!$elm) { + $elm = createElement('style', {id: 'better-xcloud-video-css'}); + document.documentElement.appendChild($elm); + } + + let filters = getVideoPlayerFilterStyle(); + let css = ''; + if (filters) { + css = `#game-stream video {filter: ${filters}}`; + } + + $elm.textContent = css; +} + + +function checkHeader() { + const $button = document.querySelector('#PageContent header .better_xcloud_settings_button'); + + if (!$button) { + const $rightHeader = document.querySelector('#PageContent header div[class*=EdgewaterHeader-module__rightSectionSpacing]'); + injectSettingsButton($rightHeader); + + updateVideoPlayerCss(); + } +} + + +function watchHeader() { + const $header = document.querySelector('#PageContent header'); + if (!$header) { + return; + } + + let timeout; + const observer = new MutationObserver(mutationList => { + if (timeout) { + clearTimeout(timeout); + } + + timeout = setTimeout(checkHeader, 2000); + }); + observer.observe($header, { subtree: true, childList: true}); + + checkHeader(); +} + + +function patchVideoApi() { + const PREF_SKIP_SPLASH_VIDEO = PREFS.get(Preferences.SKIP_SPLASH_VIDEO); + // Do nothing if the "Skip splash video" setting is off + if (!PREF_SKIP_SPLASH_VIDEO) { + return; + } + + HTMLMediaElement.prototype.orgPlay = HTMLMediaElement.prototype.play; + HTMLMediaElement.prototype.play = function() { + if (!this.className.startsWith('XboxSplashVideo')) { + return this.orgPlay.apply(this); + } + + this.volume = 0; + this.style.display = 'none'; + this.dispatchEvent(new Event('ended')); + + return { + catch: () => {}, + }; + }; +} + + +function patchHistoryMethod(type) { + var orig = window.history[type]; + return function(...args) { + const rv = orig.apply(this, arguments); + + const event = new Event('xcloud_popstate'); + event.arguments = args; + window.dispatchEvent(event); + + return rv; + }; +}; + + +function hideSettingsOnPageChange() { + const $settings = document.querySelector('.better_xcloud_settings'); + if ($settings) { + $settings.classList.add('better_xcloud_settings_gone'); + } +} + + +// Hide Settings UI when navigate to another page +window.addEventListener('xcloud_popstate', hideSettingsOnPageChange); +window.addEventListener('popstate', hideSettingsOnPageChange); +// Make pushState/replaceState methods dispatch "xcloud_popstate" event +window.history.pushState = patchHistoryMethod('pushState'); +window.history.replaceState = patchHistoryMethod('replaceState'); + +// Add additional CSS +addCss(); + +// Clear data of window.navigator.userAgentData, force Xcloud to detect browser based on User-Agent header +Object.defineProperty(window.navigator, 'userAgentData', {}); + +// Disable bandwidth checking +if (PREFS.get(Preferences.DISABLE_BANDWIDTH_CHECKING)) { + Object.defineProperty(window.navigator, 'connection', { + get: () => undefined, + }); +} + +interceptHttpRequests(); + +patchVideoApi(); + +// Workaround for Hermit browser +var onLoadTriggered = false; +window.onload = () => { + onLoadTriggered = true; +}; + +if (document.readyState === 'complete' && !onLoadTriggered) { + watchHeader(); +}