Compare commits

..

8 Commits
v1.8.2 ... v1.9

Author SHA1 Message Date
ecad1dc51b Bump version to 1.9 2023-08-04 18:40:24 +07:00
e1c1d74a22 Bump version to 1.9 2023-08-04 18:40:08 +07:00
b1881678b1 Update README.md 2023-08-04 18:30:08 +07:00
9d8d9680d3 Add "Quick glance" setting for Stream stats (#59) 2023-08-04 18:16:12 +07:00
4d2b6c5ef7 Simplify stream menu (#58)
* Add "Simplify Stream's menu" setting

* Fix Smart TV layout

* Fix not able to hide Video bar if the "Disable touch controller" is on

* Combine Region + Server badges into one

* Reduce badge's font-size from 16 to 14px

* Fix battery level showing .99999

* Don't show battery badge when it's 100%

* Fix showing incorrect Audio codec in Safari

* Add "Safari on macOS" User-Agent profile

* Fix showing incorrect Video codec in Safari

* Use "-webkit-user-select" for Safari

* Update Video badge's color
2023-08-04 16:19:18 +07:00
0c80e3ab1d Disable PWA prompt in Safari on iOS/iPadOS (#52) 2023-08-04 07:17:59 +07:00
6f326e8f2a Update README.md 2023-08-03 09:37:28 +07:00
92fe3756cf Update README.md 2023-08-02 20:35:27 +07:00
3 changed files with 260 additions and 70 deletions

View File

@ -14,10 +14,9 @@ Give this project a 🌟 if you like it. Thank you 🙏.
## Features ## Features
<img width="475" alt="Settings UI" src="https://github.com/redphx/better-xcloud/assets/96280/f4187ef8-fa10-4eab-b085-ef3c3aed3201"> <img width="475" alt="Settings UI" src="https://github.com/redphx/better-xcloud/assets/96280/575d566a-7759-4cce-962d-7e5f55a70d9e">
<img width="475" alt="Stream HUD UI" src="https://github.com/redphx/better-xcloud/assets/96280/7e0fe3e1-b826-4a69-9843-a3acb866a2f9">
<img width="475" alt="Stream HUD UI" src="https://github.com/redphx/better-xcloud/assets/96280/b4f943f1-d0b4-4401-a8cb-0fd677a5c6f0">
&nbsp; &nbsp;
@ -55,6 +54,8 @@ Give this project a 🌟 if you like it. Thank you 🙏.
- **Disable touch controller** - **Disable touch controller**
> Stop the touch controller from showing when touching the screen. > Stop the touch controller from showing when touching the screen.
> Useful when you play on a device with a built-in controller like Logitech G Cloud, Steam Deck, Retroid, etc. > Useful when you play on a device with a built-in controller like Logitech G Cloud, Steam Deck, Retroid, etc.
- **Simplify Stream's menu**
> Hide the labels of the menu buttons.
- **Hide mouse cursor while playing** - **Hide mouse cursor while playing**
> Hide the mouse cursor after 3 seconds of not moving. > Hide the mouse cursor after 3 seconds of not moving.
- **Stretch video to full sctreen** - **Stretch video to full sctreen**
@ -72,7 +73,6 @@ Give this project a 🌟 if you like it. Thank you 🙏.
> The analytics contains statistics of your streaming session, so I'd recommend allowing analytics to help Xbox improve xCloud's experience in the future. > The analytics contains statistics of your streaming session, so I'd recommend allowing analytics to help Xbox improve xCloud's experience in the future.
- **Change User-Agent** - **Change User-Agent**
> Useful when you're using unsupported browsers. > Useful when you're using unsupported browsers.
> If you're on Safari, changing User-Agent to "Edge on Windows" will allow you to use Mic feature.
> This setting only affects xCloud, and it doesn't change browser's global User-Agent. > This setting only affects xCloud, and it doesn't change browser's global User-Agent.
> 📝 If you get 404 error after using this feature, try refreshing the page a few times. See [#34](https://github.com/redphx/better-xcloud/issues/34). > 📝 If you get 404 error after using this feature, try refreshing the page a few times. See [#34](https://github.com/redphx/better-xcloud/issues/34).
- **Reduce UI animations** - **Reduce UI animations**
@ -123,11 +123,12 @@ Don't see your browser in the table? If it supports Tampermonkey/Userscript then
- **Better xCloud** also works on Android TV, but you'll have to sideload the browser APK and need a Bluetooth mouse if you want to interact with the Settings. - **Better xCloud** also works on Android TV, but you'll have to sideload the browser APK and need a Bluetooth mouse if you want to interact with the Settings.
## Stream stats ## Stream stats
<img width="500" alt="Stream stats" src="https://github.com/redphx/better-xcloud/assets/96280/70f4b1bb-4e3d-4f27-9b2f-afcfe1b8b261"> <img width="500" alt="Stream stats" src="https://github.com/redphx/better-xcloud/assets/96280/0d4abb6b-49ab-4c9a-a52d-df7e396d2145">
- While playing > `...` > `Stream Stats`. - While playing > `...` > `Stream Stats` (the one with the eye icon).
- Double-click on the stats bar to show Settings dialog. - Double-click on the stats bar to show the Settings dialog.
- This bar is updated every second. - This bar is updated every second.
- **Quick glance** feature: only show the stats bar when the System buttons bar is expanded. The 👀 emoji at the beginning indicates that the stats bar is in the quick glance mode.
- ⚠️ Using **Better xCloud** or showing the stats bar also affects the performance of the stream. - ⚠️ Using **Better xCloud** or showing the stats bar also affects the performance of the stream.
| Abbr. | Full name | Explain | | Abbr. | Full name | Explain |
@ -178,8 +179,8 @@ I think it's very unlikely that you'll get banned for using this. Most of the fe
2. **Why is it an Userscript and not an extension?** 2. **Why is it an Userscript and not an extension?**
It's because not many browsers on Android support installing extensions (and not all extensions can be installed). It's because not many browsers on Android support installing extensions (and not all extensions can be installed).
3. **I see "???" button instead of the server's name** 3. **Why doesn't the xCloud website implement *this* or *that* feature from Better xCloud?**
That means Tampermonkey is not working properly. Please make sure you're using the latest version or switch to a well-known browser. For being an unofficial tool, **Better xCloud** has the luxury to implement anything on the xCloud website. On the xCloud's side, they have a lot more users and devices to support, so it's more difficult for them to implement a new feature. Also it's not easy to explain some of the features of **Better xCloud** to normal xCloud users.
4. **Can I use this with the Xbox Android app?** 4. **Can I use this with the Xbox Android app?**
No, you can't. You'll have to modify the app. No, you can't. You'll have to modify the app.

View File

@ -1,5 +1,5 @@
// ==UserScript== // ==UserScript==
// @name Better xCloud // @name Better xCloud
// @namespace https://github.com/redphx // @namespace https://github.com/redphx
// @version 1.8.2 // @version 1.9
// ==/UserScript== // ==/UserScript==

View File

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name Better xCloud // @name Better xCloud
// @namespace https://github.com/redphx // @namespace https://github.com/redphx
// @version 1.8.2 // @version 1.9
// @description Improve Xbox Cloud Gaming (xCloud) experience // @description Improve Xbox Cloud Gaming (xCloud) experience
// @author redphx // @author redphx
// @license MIT // @license MIT
@ -13,7 +13,7 @@
// ==/UserScript== // ==/UserScript==
'use strict'; 'use strict';
const SCRIPT_VERSION = '1.8.2'; const SCRIPT_VERSION = '1.9';
const SCRIPT_HOME = 'https://github.com/redphx/better-xcloud'; const SCRIPT_HOME = 'https://github.com/redphx/better-xcloud';
const SERVER_REGIONS = {}; const SERVER_REGIONS = {};
@ -69,7 +69,6 @@ class StreamBadges {
static get BADGE_IN() { return 'in'; }; static get BADGE_IN() { return 'in'; };
static get BADGE_OUT() { return 'out'; }; static get BADGE_OUT() { return 'out'; };
static get BADGE_REGION() { return 'region'; };
static get BADGE_SERVER() { return 'server'; }; static get BADGE_SERVER() { return 'server'; };
static get BADGE_VIDEO() { return 'video'; }; static get BADGE_VIDEO() { return 'video'; };
static get BADGE_AUDIO() { return 'audio'; }; static get BADGE_AUDIO() { return 'audio'; };
@ -126,13 +125,14 @@ class StreamBadges {
// Battery // Battery
let batteryLevel = '100%'; let batteryLevel = '100%';
let batteryLevelInt = 100;
if (navigator.getBattery) { if (navigator.getBattery) {
try { try {
const currentLevel = (await navigator.getBattery()).level * 100; batteryLevelInt = Math.round((await navigator.getBattery()).level * 100);
batteryLevel = `${currentLevel}%`; batteryLevel = `${batteryLevelInt}%`;
if (currentLevel != StreamBadges.startBatteryLevel) { if (batteryLevelInt != StreamBadges.startBatteryLevel) {
const diffLevel = currentLevel - StreamBadges.startBatteryLevel; const diffLevel = Math.round(batteryLevelInt - StreamBadges.startBatteryLevel);
const sign = diffLevel > 0 ? '+' : ''; const sign = diffLevel > 0 ? '+' : '';
batteryLevel += ` (${sign}${diffLevel}%)`; batteryLevel += ` (${sign}${diffLevel}%)`;
} }
@ -164,6 +164,14 @@ class StreamBadges {
const $elm = StreamBadges.#cachedDoms[name]; const $elm = StreamBadges.#cachedDoms[name];
$elm && ($elm.lastElementChild.textContent = value); $elm && ($elm.lastElementChild.textContent = value);
if (name === StreamBadges.BADGE_BATTERY) {
if (StreamBadges.startBatteryLevel === 100 && batteryLevelInt === 100) {
$elm.style.display = 'none';
} else {
$elm.style = '';
}
}
} }
} }
@ -218,15 +226,18 @@ class StreamBadges {
batteryLevel = '100%'; batteryLevel = '100%';
} }
// Server + Region
let server = StreamBadges.region;
server += '@' + (StreamBadges.ipv6 ? 'IPv6' : 'IPv4');
const BADGES = [ const BADGES = [
[StreamBadges.BADGE_PLAYTIME, '1m', '#ff004d'], [StreamBadges.BADGE_PLAYTIME, '1m', '#ff004d'],
[StreamBadges.BADGE_BATTERY, batteryLevel, '#00b543'], [StreamBadges.BADGE_BATTERY, batteryLevel, '#00b543'],
[StreamBadges.BADGE_IN, StreamBadges.#humanFileSize(0), '#29adff'], [StreamBadges.BADGE_IN, StreamBadges.#humanFileSize(0), '#29adff'],
[StreamBadges.BADGE_OUT, StreamBadges.#humanFileSize(0), '#ff77a8'], [StreamBadges.BADGE_OUT, StreamBadges.#humanFileSize(0), '#ff77a8'],
[StreamBadges.BADGE_BREAK], [StreamBadges.BADGE_BREAK],
[StreamBadges.BADGE_REGION, StreamBadges.region, '#ff6c24'], [StreamBadges.BADGE_SERVER, server, '#ff6c24'],
[StreamBadges.BADGE_SERVER, StreamBadges.ipv6 ? 'IPv6' : 'IPv4', '#065ab5'], video ? [StreamBadges.BADGE_VIDEO, video, '#742f29'] : null,
video ? [StreamBadges.BADGE_VIDEO, video, '#754665'] : null,
audio ? [StreamBadges.BADGE_AUDIO, audio, '#5f574f'] : null, audio ? [StreamBadges.BADGE_AUDIO, audio, '#5f574f'] : null,
]; ];
@ -258,28 +269,83 @@ class StreamStats {
static #lastStat; static #lastStat;
static start() { static #quickGlanceObserver;
static start(glancing=false) {
if (!StreamStats.isHidden() || (glancing && StreamStats.#isGlancing())) {
return;
}
StreamStats.#$container.classList.remove('better-xcloud-gone'); StreamStats.#$container.classList.remove('better-xcloud-gone');
StreamStats.#$container.setAttribute('data-display', glancing ? 'glancing' : 'fixed');
StreamStats.#interval = setInterval(StreamStats.update, StreamStats.#updateInterval); StreamStats.#interval = setInterval(StreamStats.update, StreamStats.#updateInterval);
} }
static stop() { static stop(glancing=false) {
clearInterval(StreamStats.#interval); if (glancing && !StreamStats.#isGlancing()) {
return;
}
StreamStats.#$container.classList.add('better-xcloud-gone'); clearInterval(StreamStats.#interval);
StreamStats.#interval = null; StreamStats.#interval = null;
StreamStats.#lastStat = null; StreamStats.#lastStat = null;
StreamStats.#$container.removeAttribute('data-display');
StreamStats.#$container.classList.add('better-xcloud-gone');
} }
static toggle() { static toggle() {
StreamStats.#isHidden() ? StreamStats.start() : StreamStats.stop(); if (StreamStats.#isGlancing()) {
StreamStats.#$container.setAttribute('data-display', 'fixed');
} else {
StreamStats.isHidden() ? StreamStats.start() : StreamStats.stop();
}
} }
static #isHidden = () => StreamStats.#$container.classList.contains('better-xcloud-gone'); static onStoppedPlaying() {
StreamStats.stop();
StreamStats.quickGlanceStop();
StreamStats.hideSettingsUi();
}
static isHidden = () => StreamStats.#$container.classList.contains('better-xcloud-gone');
static #isGlancing = () => StreamStats.#$container.getAttribute('data-display') === 'glancing';
static quickGlanceSetup() {
if (StreamStats.#quickGlanceObserver) {
return;
}
const $uiContainer = document.querySelector('div[data-testid=ui-container]');
StreamStats.#quickGlanceObserver = new MutationObserver((mutationList, observer) => {
for (let record of mutationList) {
if (record.attributeName && record.attributeName === 'aria-expanded') {
const expanded = record.target.ariaExpanded;
if (expanded === 'true') {
StreamStats.isHidden() && StreamStats.start(true);
} else {
StreamStats.stop(true);
}
}
}
});
StreamStats.#quickGlanceObserver.observe($uiContainer, {
attributes: true,
attributeFilter: ['aria-expanded'],
subtree: true,
});
}
static quickGlanceStop() {
StreamStats.#quickGlanceObserver && StreamStats.#quickGlanceObserver.disconnect();
StreamStats.#quickGlanceObserver = null;
}
static update() { static update() {
if (StreamStats.#isHidden() || !STREAM_WEBRTC) { if (StreamStats.isHidden() || !STREAM_WEBRTC) {
StreamStats.stop(); StreamStats.onStoppedPlaying();
return; return;
} }
@ -351,6 +417,10 @@ class StreamStats {
static hideSettingsUi() { static hideSettingsUi() {
StreamStats.#$settings.style.display = 'none'; StreamStats.#$settings.style.display = 'none';
if (StreamStats.#isGlancing() && !PREFS.get(Preferences.STATS_QUICK_GLANCE)) {
StreamStats.stop();
}
} }
static #toggleSettingsUi() { static #toggleSettingsUi() {
@ -401,7 +471,10 @@ class StreamStats {
const $position = PREFS.toElement(Preferences.STATS_POSITION, refreshFunc); const $position = PREFS.toElement(Preferences.STATS_POSITION, refreshFunc);
let $close; let $close;
const $showStartup = PREFS.toElement(Preferences.STATS_SHOW_WHEN_PLAYING, refreshFunc); const $showStartup = PREFS.toElement(Preferences.STATS_SHOW_WHEN_PLAYING);
const $quickGlance = PREFS.toElement(Preferences.STATS_QUICK_GLANCE, e => {
e.target.checked ? StreamStats.quickGlanceSetup() : StreamStats.quickGlanceStop();
});
const $transparent = PREFS.toElement(Preferences.STATS_TRANSPARENT, refreshFunc); const $transparent = PREFS.toElement(Preferences.STATS_TRANSPARENT, refreshFunc);
const $formatting = PREFS.toElement(Preferences.STATS_CONDITIONAL_FORMATTING, refreshFunc); const $formatting = PREFS.toElement(Preferences.STATS_CONDITIONAL_FORMATTING, refreshFunc);
const $opacity = PREFS.toElement(Preferences.STATS_OPACITY, refreshFunc); const $opacity = PREFS.toElement(Preferences.STATS_OPACITY, refreshFunc);
@ -413,6 +486,10 @@ class StreamStats {
CE('label', {'for': `xcloud_setting_${Preferences.STATS_SHOW_WHEN_PLAYING}`}, 'Show stats when starting the game'), CE('label', {'for': `xcloud_setting_${Preferences.STATS_SHOW_WHEN_PLAYING}`}, 'Show stats when starting the game'),
$showStartup $showStartup
), ),
CE('div', {},
CE('label', {'for': `xcloud_setting_${Preferences.STATS_QUICK_GLANCE}`}, 'Enable quick glance'),
$quickGlance
),
CE('div', {}, CE('div', {},
CE('label', {}, 'Position'), CE('label', {}, 'Position'),
$position $position
@ -442,15 +519,16 @@ class StreamStats {
} }
} }
class UserAgent { class UserAgent {
static get PROFILE_EDGE_WINDOWS() { return 'edge-windows'; } static get PROFILE_EDGE_WINDOWS() { return 'edge-windows'; }
static get PROFILE_SAFARI_MACOS() { return 'safari-macos'; }
static get PROFILE_SMARTTV_TIZEN() { return 'smarttv-tizen'; } static get PROFILE_SMARTTV_TIZEN() { return 'smarttv-tizen'; }
static get PROFILE_DEFAULT() { return 'default'; } static get PROFILE_DEFAULT() { return 'default'; }
static get PROFILE_CUSTOM() { return 'custom'; } static get PROFILE_CUSTOM() { return 'custom'; }
static #USER_AGENTS = { static #USER_AGENTS = {
[UserAgent.PROFILE_EDGE_WINDOWS]: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.188', [UserAgent.PROFILE_EDGE_WINDOWS]: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.188',
[UserAgent.PROFILE_SAFARI_MACOS]: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5.2 Safari/605.1.1',
[UserAgent.PROFILE_SMARTTV_TIZEN]: 'Mozilla/5.0 (SMART-TV; LINUX; Tizen 7.0) AppleWebKit/537.36 (KHTML, like Gecko) 94.0.4606.31/7.0 TV Safari/537.36', [UserAgent.PROFILE_SMARTTV_TIZEN]: 'Mozilla/5.0 (SMART-TV; LINUX; Tizen 7.0) AppleWebKit/537.36 (KHTML, like Gecko) 94.0.4606.31/7.0 TV Safari/537.36',
} }
@ -506,6 +584,7 @@ class Preferences {
static get USER_AGENT_PROFILE() { return 'user_agent_profile'; } static get USER_AGENT_PROFILE() { return 'user_agent_profile'; }
static get USER_AGENT_CUSTOM() { return 'user_agent_custom'; } static get USER_AGENT_CUSTOM() { return 'user_agent_custom'; }
static get STREAM_HIDE_TOUCH_CONTROLLER() { return 'stream_hide_touch_controller'; } static get STREAM_HIDE_TOUCH_CONTROLLER() { return 'stream_hide_touch_controller'; }
static get STREAM_SIMPLIFY_MENU() { return 'stream_simplify_menu'; }
static get SCREENSHOT_BUTTON_POSITION() { return 'screenshot_button_position'; } static get SCREENSHOT_BUTTON_POSITION() { return 'screenshot_button_position'; }
static get BLOCK_TRACKING() { return 'block_tracking'; } static get BLOCK_TRACKING() { return 'block_tracking'; }
@ -521,6 +600,7 @@ class Preferences {
static get VIDEO_SATURATION() { return 'video_saturation'; } static get VIDEO_SATURATION() { return 'video_saturation'; }
static get STATS_SHOW_WHEN_PLAYING() { return 'stats_show_when_playing'; } static get STATS_SHOW_WHEN_PLAYING() { return 'stats_show_when_playing'; }
static get STATS_QUICK_GLANCE() { return 'stats_quick_glance'; }
static get STATS_POSITION() { return 'stats_position'; } static get STATS_POSITION() { return 'stats_position'; }
static get STATS_TEXT_SIZE() { return 'stats_text_size'; } static get STATS_TEXT_SIZE() { return 'stats_text_size'; }
static get STATS_TRANSPARENT() { return 'stats_transparent'; } static get STATS_TRANSPARENT() { return 'stats_transparent'; }
@ -617,6 +697,10 @@ class Preferences {
'label': 'Disable touch controller', 'label': 'Disable touch controller',
'default': false, 'default': false,
}, },
[Preferences.STREAM_SIMPLIFY_MENU]: {
'label': 'Simplify Stream\'s menu',
'default': false,
},
[Preferences.HIDE_IDLE_CURSOR]: { [Preferences.HIDE_IDLE_CURSOR]: {
'label': 'Hide mouse cursor while playing', 'label': 'Hide mouse cursor while playing',
'default': false, 'default': false,
@ -639,6 +723,7 @@ class Preferences {
'options': { 'options': {
[UserAgent.PROFILE_DEFAULT]: 'Default', [UserAgent.PROFILE_DEFAULT]: 'Default',
[UserAgent.PROFILE_EDGE_WINDOWS]: 'Edge on Windows', [UserAgent.PROFILE_EDGE_WINDOWS]: 'Edge on Windows',
[UserAgent.PROFILE_SAFARI_MACOS]: 'Safari on macOS',
[UserAgent.PROFILE_SMARTTV_TIZEN]: 'Samsung Smart TV', [UserAgent.PROFILE_SMARTTV_TIZEN]: 'Samsung Smart TV',
[UserAgent.PROFILE_CUSTOM]: 'Custom', [UserAgent.PROFILE_CUSTOM]: 'Custom',
}, },
@ -677,6 +762,10 @@ class Preferences {
'default': false, 'default': false,
'hidden': true, 'hidden': true,
}, },
[Preferences.STATS_QUICK_GLANCE]: {
'default': false,
'hidden': true,
},
[Preferences.STATS_POSITION]: { [Preferences.STATS_POSITION]: {
'default': 'top-left', 'default': 'top-left',
'options': { 'options': {
@ -864,6 +953,7 @@ function addCss() {
.better_xcloud_settings { .better_xcloud_settings {
background-color: #151515; background-color: #151515;
user-select: none; user-select: none;
-webkit-user-select: none;
color: #fff; color: #fff;
font-family: "Segoe UI", Arial, Helvetica, sans-serif font-family: "Segoe UI", Arial, Helvetica, sans-serif
} }
@ -987,9 +1077,9 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
.better-xcloud-badges { .better-xcloud-badges {
position: absolute; position: absolute;
top: 155px;
margin-left: 0px; margin-left: 0px;
user-select: none; user-select: none;
-webkit-user-select: none;
} }
.better-xcloud-badge { .better-xcloud-badge {
@ -998,6 +1088,7 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
line-height: 24px; line-height: 24px;
color: #fff; color: #fff;
font-family: Bahnschrift Semibold, Arial, Helvetica, sans-serif; font-family: Bahnschrift Semibold, Arial, Helvetica, sans-serif;
font-size: 14px;
font-weight: 400; font-weight: 400;
margin: 0 8px 8px 0; margin: 0 8px 8px 0;
box-shadow: 0px 0px 6px #000; box-shadow: 0px 0px 6px #000;
@ -1053,6 +1144,7 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
.better-xcloud-stats-bar { .better-xcloud-stats-bar {
display: block; display: block;
user-select: none; user-select: none;
-webkit-user-select: none;
position: fixed; position: fixed;
top: 0; top: 0;
background-color: #000; background-color: #000;
@ -1064,6 +1156,11 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
text-wrap: nowrap; text-wrap: nowrap;
} }
.better-xcloud-stats-bar[data-display=glancing]::before {
content: '👀 ';
vertical-align: middle;
}
.better-xcloud-stats-bar[data-position=top-left] { .better-xcloud-stats-bar[data-position=top-left] {
left: 0; left: 0;
} }
@ -1139,6 +1236,7 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
font-family: "Segoe UI", Arial, Helvetica, sans-serif; font-family: "Segoe UI", Arial, Helvetica, sans-serif;
box-shadow: 0 0 6px #000; box-shadow: 0 0 6px #000;
user-select: none; user-select: none;
-webkit-user-select: none;
} }
.better-xcloud-stats-settings *:focus { .better-xcloud-stats-settings *:focus {
@ -1196,6 +1294,7 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
.better-xcloud-quick-settings-bar { .better-xcloud-quick-settings-bar {
display: none; display: none;
user-select: none; user-select: none;
-webkit-user-select: none;
position: fixed; position: fixed;
bottom: 20px; bottom: 20px;
left: 50%; left: 50%;
@ -1279,17 +1378,6 @@ div[class*=NotFocusedDialog] {
#game-stream video { #game-stream video {
visibility: hidden; visibility: hidden;
} }
/* Adjust Stream menu icon's size */
button[class*=MenuItem-module__container] {
min-width: auto !important;
width: 100px !important;
}
button[class*=MenuItem-module__container] div[class*=MenuItem-module__label] {
margin-left: 8px !important;
margin-right: 8px !important;
}
`; `;
// Reduce animations // Reduce animations
@ -1333,6 +1421,71 @@ div[class*=StreamHUD-module__buttonsContainer] {
`; `;
} }
// Simplify Stream's menu
css += `
div[class*=StreamMenu-module__menu] {
min-width: 100vw !important;
}
`;
if (PREFS.get(Preferences.STREAM_SIMPLIFY_MENU)) {
css += `
div[class*=Menu-module__scrollable] {
--bxStreamMenuItemSize: 80px;
--streamMenuItemSize: calc(var(--bxStreamMenuItemSize) + 40px) !important;
}
.better-xcloud-badges {
top: calc(var(--streamMenuItemSize) - 20px);
}
body[data-media-type=tv] .better-xcloud-badges {
top: calc(var(--streamMenuItemSize) - 10px) !important;
}
button[class*=MenuItem-module__container] {
min-width: auto !important;
min-height: auto !important;
width: var(--bxStreamMenuItemSize) !important;
height: var(--bxStreamMenuItemSize) !important;
}
div[class*=MenuItem-module__label] {
display: none !important;
}
svg[class*=MenuItem-module__icon] {
width: 36px;
height: 100% !important;
padding: 0 !important;
margin: 0 !important;
}
`;
} else {
css += `
body[data-media-type=tv] .better-xcloud-badges {
top: calc(var(--streamMenuItemSize) + 30px);
}
body:not([data-media-type=tv]) .better-xcloud-badges {
top: calc(var(--streamMenuItemSize) + 20px);
}
body:not([data-media-type=tv]) button[class*=MenuItem-module__container] {
min-width: auto !important;
width: 100px !important;
}
body:not([data-media-type=tv]) button[class*=MenuItem-module__container]:nth-child(n+2) {
margin-left: 10px !important;
}
body:not([data-media-type=tv]) div[class*=MenuItem-module__label] {
margin-left: 8px !important;
margin-right: 8px !important;
}
`;
}
const $style = createElement('style', {}, css); const $style = createElement('style', {}, css);
document.documentElement.appendChild($style); document.documentElement.appendChild($style);
} }
@ -1855,7 +2008,8 @@ function injectVideoSettingsButton() {
const $quickBar = document.querySelector('.better-xcloud-quick-settings-bar'); const $quickBar = document.querySelector('.better-xcloud-quick-settings-bar');
const $parent = $screen.parentElement; const $parent = $screen.parentElement;
const hideQuickBarFunc = e => { const hideQuickBarFunc = e => {
if (e.target != $parent && e.target.id !== 'MultiTouchSurface') { e.stopPropagation();
if (e.target != $parent && e.target.id !== 'MultiTouchSurface' && !e.target.querySelector('#BabylonCanvasContainer-main')) {
return; return;
} }
@ -1863,7 +2017,7 @@ function injectVideoSettingsButton() {
$quickBar.style.display = 'none'; $quickBar.style.display = 'none';
$parent.removeEventListener('click', hideQuickBarFunc); $parent.removeEventListener('click', hideQuickBarFunc);
$parent.removeEventListener('touchend', hideQuickBarFunc); $parent.removeEventListener('touchstart', hideQuickBarFunc);
if (e.target.id === 'MultiTouchSurface') { if (e.target.id === 'MultiTouchSurface') {
e.target.removeEventListener('touchstart', hideQuickBarFunc); e.target.removeEventListener('touchstart', hideQuickBarFunc);
@ -1896,10 +2050,10 @@ function injectVideoSettingsButton() {
$quickBar.style.display = 'flex'; $quickBar.style.display = 'flex';
$parent.addEventListener('click', hideQuickBarFunc); $parent.addEventListener('click', hideQuickBarFunc);
$parent.addEventListener('touchend', hideQuickBarFunc); $parent.addEventListener('touchstart', hideQuickBarFunc);
const $touchSurface = document.querySelector('#MultiTouchSurface'); const $touchSurface = document.querySelector('#MultiTouchSurface');
$touchSurface && $touchSurface.addEventListener('touchstart', hideQuickBarFunc); $touchSurface && $touchSurface.style.display != 'none' && $touchSurface.addEventListener('touchstart', hideQuickBarFunc);
}); });
// Add button at the beginning // Add button at the beginning
@ -1967,6 +2121,7 @@ function injectVideoSettingsButton() {
function patchVideoApi() { function patchVideoApi() {
const PREF_SKIP_SPLASH_VIDEO = PREFS.get(Preferences.SKIP_SPLASH_VIDEO); const PREF_SKIP_SPLASH_VIDEO = PREFS.get(Preferences.SKIP_SPLASH_VIDEO);
const PREF_SCREENSHOT_BUTTON_POSITION = PREFS.get(Preferences.SCREENSHOT_BUTTON_POSITION); const PREF_SCREENSHOT_BUTTON_POSITION = PREFS.get(Preferences.SCREENSHOT_BUTTON_POSITION);
const PREF_STATS_QUICK_GLANCE = PREFS.get(Preferences.STATS_QUICK_GLANCE);
// Show video player when it's ready // Show video player when it's ready
var showFunc; var showFunc;
@ -1978,47 +2133,77 @@ function patchVideoApi() {
return; return;
} }
if (PREF_STATS_QUICK_GLANCE) {
StreamStats.quickGlanceSetup();
}
$STREAM_VIDEO = this; $STREAM_VIDEO = this;
$SCREENSHOT_CANVAS.width = this.videoWidth; $SCREENSHOT_CANVAS.width = this.videoWidth;
$SCREENSHOT_CANVAS.height = this.videoHeight; $SCREENSHOT_CANVAS.height = this.videoHeight;
StreamBadges.resolution = {width: this.videoWidth, height: this.videoHeight}; StreamBadges.resolution = {width: this.videoWidth, height: this.videoHeight};
StreamBadges.startTimestamp = +new Date; StreamBadges.startTimestamp = +new Date;
// Get battery level // Get battery level
if (navigator.getBattery) { if (navigator.getBattery) {
try { try {
navigator.getBattery().then(bm => { navigator.getBattery().then(bm => {
StreamBadges.startBatteryLevel = bm.level * 100; StreamBadges.startBatteryLevel = Math.round(bm.level * 100);
}); });
} catch(e) {} } catch(e) {}
} }
STREAM_WEBRTC.getStats().then(stats => { STREAM_WEBRTC.getStats().then(stats => {
const allVideoCodecs = {};
let videoCodecId;
const allAudioCodecs = {};
let audioCodecId;
stats.forEach(stat => { stats.forEach(stat => {
if (stat.type !== 'codec') { if (stat.type == 'codec') {
return; const mimeType = stat.mimeType.split('/');
} if (mimeType[0] === 'video') {
// Store all video stats
const mimeType = stat.mimeType.split('/'); allVideoCodecs[stat.id] = stat;
if (mimeType[0] === 'video') { } else if (mimeType[0] === 'audio') {
const video = { // Store all audio stats
codec: mimeType[1], allAudioCodecs[stat.id] = stat;
}; }
} else if (stat.type === 'inbound-rtp' && stat.packetsReceived > 0) {
if (video.codec === 'H264') { // Get the codecId of the video/audio track currently being used
const match = /profile-level-id=([0-9a-f]{6})/.exec(stat.sdpFmtpLine); if (stat.kind === 'video') {
video.profile = match ? match[1] : null; videoCodecId = stat.codecId;
} else if (stat.kind === 'audio') {
audioCodecId = stat.codecId;
} }
StreamBadges.video = video;
} else if (!StreamBadges.audio && mimeType[0] === 'audio') {
StreamBadges.audio = {
codec: mimeType[1],
bitrate: stat.clockRate,
};
} }
}); });
// Get video codec from codecId
if (videoCodecId) {
const videoStat = allVideoCodecs[videoCodecId];
const video = {
codec: videoStat.mimeType.substring(6),
};
if (video.codec === 'H264') {
const match = /profile-level-id=([0-9a-f]{6})/.exec(videoStat.sdpFmtpLine);
video.profile = match ? match[1] : null;
}
StreamBadges.video = video;
}
// Get audio codec from codecId
if (audioCodecId) {
const audioStat = allAudioCodecs[audioCodecId];
StreamBadges.audio = {
codec: audioStat.mimeType.substring(6),
bitrate: audioStat.clockRate,
}
}
if (PREFS.get(Preferences.STATS_SHOW_WHEN_PLAYING)) { if (PREFS.get(Preferences.STATS_SHOW_WHEN_PLAYING)) {
StreamStats.start(); StreamStats.start();
} }
@ -2291,8 +2476,7 @@ function onHistoryChange() {
STREAM_WEBRTC = null; STREAM_WEBRTC = null;
$STREAM_VIDEO = null; $STREAM_VIDEO = null;
StreamStats.stop(); StreamStats.onStoppedPlaying();
StreamStats.hideSettingsUi();
document.querySelector('.better-xcloud-screenshot-button').style = ''; document.querySelector('.better-xcloud-screenshot-button').style = '';
MouseCursorHider.stop(); MouseCursorHider.stop();
@ -2340,3 +2524,8 @@ updateVideoPlayerCss();
setupVideoSettingsBar(); setupVideoSettingsBar();
setupScreenshotButton(); setupScreenshotButton();
StreamStats.render(); StreamStats.render();
// Disable PWA prompt in Safari on iOS/iPadOS
Object.defineProperty(window.navigator, 'standalone', {
value: true,
});