Compare commits

...

35 Commits
v1.7 ... 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
8ee28d92d9 Bump version to 1.8.2 2023-08-02 12:45:23 +07:00
1f94058b99 Bump version to 1.8.2 2023-08-02 12:45:08 +07:00
95e94242aa Update README.md 2023-08-02 12:44:40 +07:00
91aa28450d Fix battery status 2023-08-02 11:58:18 +07:00
8eb8bbf598 Add In/Out badges (#48)
* Show In/Out badges

* Cache DOMs of Stream badges

* Refresh badges every 3s

* Shorten Video badge: "1920x1080" -> "1080p"

* Fix 404 error when spoofing User-Agent (#34)
2023-08-02 11:51:46 +07:00
47817d9d36 Update color of "Video" badge 2023-08-01 21:17:27 +07:00
b770a4c9d3 Bump version to 1.8.1 2023-08-01 08:32:29 +07:00
a27c0ed8f6 Bump version to 1.8.1 2023-08-01 08:31:56 +07:00
8ac37754e6 Combine "Resolution" and "Video" badges into one 2023-08-01 08:31:01 +07:00
d9288a322b Update battery stat 2023-08-01 08:23:48 +07:00
5facfd2348 Fix inaccurate percentages of PL & FL stats 2023-08-01 08:18:03 +07:00
889717be7d Show confirm dialog to refresh the stream after holding for 1s 2023-08-01 08:07:57 +07:00
4b0f0784ae Fix stats bar not showing sometimes 2023-08-01 08:05:18 +07:00
31217d01bb Update bug_report.md 2023-08-01 07:40:50 +07:00
2f6176e906 Update README.md 2023-07-31 14:49:03 +07:00
fe011fd0f2 Update README.md 2023-07-31 08:28:37 +07:00
c23f55ee6b Bump version to 1.8 2023-07-31 07:49:11 +07:00
0c85770ed1 Bump version to 1.8 2023-07-31 07:48:52 +07:00
8233192b8d Update README.md 2023-07-31 07:47:45 +07:00
6c5fa3c061 Simplify playtime text 2023-07-31 07:31:03 +07:00
e47c6d9103 Use colors from PICO-8 palette for stream badges 2023-07-31 07:25:52 +07:00
563ad65580 Fix battery calculation 2023-07-31 07:16:32 +07:00
7933d8d22c Update README.md 2023-07-31 06:43:47 +07:00
b0e23ca335 Hold the "Quit game" button for 1s to refresh the stream (#43)
* Hold "Quit game" button to refresh the stream

* Fix problem with touch screen

* Another fix
2023-07-30 22:03:25 +07:00
c9f3990173 Show playtime & battery level badges (#42) 2023-07-30 21:07:25 +07:00
5d301b6588 Add setting to hide/disable touch controller (#38) 2023-07-30 15:47:56 +07:00
32123a7891 Update README.md 2023-07-29 18:02:20 +07:00
4 changed files with 494 additions and 110 deletions

View File

@ -24,8 +24,9 @@ A clear and concise description of what you expected to happen.
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
**Platform (please complete the following information):** **Platform (please complete the following information):**
- Device: [e.g. Phone/Laptop/Desktop/TV]
- OS: [e.g. Android] - OS: [e.g. Android]
- Browser: [e.g. chrome, firefox] - Browser: [e.g. Chrome, Kiwi]
- Browser Version: [e.g. 100] - Browser Version: [e.g. 100]
- Better xCloud Version: [e.g. 1.4] - Better xCloud Version: [e.g. 1.4]

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/ef689e3b-0235-4635-8623-ebcc332774b2"> <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/905750b2-5d02-41e0-af36-2e8b590d81a0">
<img width="475" alt="Stream HUD UI" src="https://github.com/redphx/better-xcloud/assets/96280/b4f943f1-d0b4-4401-a8cb-0fd677a5c6f0">
&nbsp; &nbsp;
@ -25,6 +24,11 @@ Give this project a 🌟 if you like it. Thank you 🙏.
- **🔥 Show stream stats** - **🔥 Show stream stats**
> Check [Stream stats section](#stream-stats) for more info. > Check [Stream stats section](#stream-stats) for more info.
- **🔥 Capture screenshot**
> Exclusive to **Better xCloud**. Check the [**Capture screenshot** section](#capture-screenshot) for more info.
- **🔥 Hold the "Quit game" button for one second to refresh the stream**
> Sometimes you can fix the bad connection to the stream simply by refreshing the page.
> Useful on mobile where the pull-to-refresh feature doesn't work while playing.
- **Switch region of streaming server** - **Switch region of streaming server**
> Connect to another server instead of the default one. Check the [**FAQ** section](#faq) for some notes. > Connect to another server instead of the default one. Check the [**FAQ** section](#faq) for some notes.
- **Preferred game's language** - **Preferred game's language**
@ -43,36 +47,42 @@ Give this project a 🌟 if you like it. Thank you 🙏.
> Might reduce latency. > Might reduce latency.
- **Disable bandwidth checking** - **Disable bandwidth checking**
> xCloud won't warn about slow connection speed. > xCloud won't warn about slow connection speed.
- **🔥 Capture screenshot**
> Exclusive to **Better xCloud**. Check the [**Capture screenshot** section](#capture-screenshot) for more info.
- **Skip Xbox splash video** - **Skip Xbox splash video**
> Save 3 seconds. > Save 3 seconds.
- **Hide Dots icon while playing** - **Hide Dots icon while playing**
> You can still click on it, but it doesn't block the screen anymore. > You can still click on it, but it doesn't block the screen anymore.
- **Disable touch controller**
> 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.
- **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.
- **Reduce UI animations**
> Disable `transition` CSS property in some elements. The smooth scrolling cannot be disabled.
- **Stretch video to full sctreen** - **Stretch video to full sctreen**
> Useful when you don't have a 16:9 screen > Useful when you don't have a 16:9 screen
- **Adjust video filters** - **Adjust video filters**
> Brightness/Contrast/Saturation. > Brightness/Contrast/Saturation.
- **Display stream's statuses** - **Display stream's statuses**
> Region/Server/Quality/Resolution... > Region/Server/Codecs/Resolution...
> Current playtime of the session.
> Current battery level.
> Estimated total data sent/received.
- **Disable social features** - **Disable social features**
> Features like friends, chat... Disable these will make the page load faster. > Features like friends, chat... Disable these will make the page load faster.
- **Disable xCloud analytics** - **Disable xCloud analytics**
> 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).
- **Reduce UI animations**
> Disable `transition` CSS property in some elements. The smooth scrolling cannot be disabled.
- **Hide footer and other UI elements** - **Hide footer and other UI elements**
<sup>(\*)</sup> By default (for compatibility reasons) xCloud only uses high quality codec profile when you use Tizen TV or Chrome/Edge/Chromium browser on Chrome/MacOS. Enable this setting will give you the best experience no matter what platform & browser you're on. <sup>(\*)</sup> By default (for compatibility reasons) xCloud only uses high quality codec profile when you use Tizen TV or Chrome/Edge/Chromium browser on Chrome/MacOS. Enable this setting will give you the best experience no matter what platform & browser you're on.
## How to use ## How to use
1. Install [Tampermonkey extension](https://www.tampermonkey.net/) on suppported browsers. 1. Install [Tampermonkey extension](https://www.tampermonkey.net/) on suppported browsers. For Safari, use [Userscripts app](https://apps.apple.com/us/app/userscripts/id1463298887).
2. Install **Better xCloud**: 2. Install **Better xCloud**:
- [Stable version](https://github.com/redphx/better-xcloud/releases/latest/download/better-xcloud.user.js) - [Stable version](https://github.com/redphx/better-xcloud/releases/latest/download/better-xcloud.user.js)
- [Dev version](https://github.com/redphx/better-xcloud/raw/main/better-xcloud.user.js) - [Dev version](https://github.com/redphx/better-xcloud/raw/main/better-xcloud.user.js)
@ -113,16 +123,17 @@ 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 |
|------:|:-------------------|:-----------------------------------------------------------------------------------------------------------------------------------| |------:|:-------------------|:-------------------------------------------------------------------------------------------------------------------------------------------|
| FPS | Frames per Seconds | The number of decoded frames in the last second of the stream (equal to or lower than the FPS of the game) | | FPS | Frames per Seconds | The number of decoded frames in the last second of the stream (may not be the same as the FPS of the game) |
| DT | Decode Time | The average time it took to decode one frame in the last second (might be bugged [#26](https://github.com/redphx/better-xcloud/issues/26)) | | DT | Decode Time | The average time it took to decode one frame in the last second (might be bugged [#26](https://github.com/redphx/better-xcloud/issues/26)) |
| RTT | Round Trip Time | The number of seconds it takes for data to be sent from your device to the server and back over (similar to ping, lower is better) | | RTT | Round Trip Time | The number of seconds it takes for data to be sent from your device to the server and back over (similar to ping, lower is better) |
| BR | Bitrate | The amount of data the server sent to your device in the last second | | BR | Bitrate | The amount of data the server sent to your device in the last second |
@ -139,8 +150,7 @@ Colors:
- Green = Good - Green = Good
- White = Great - White = Great
📝 Having this info on all the time might reduce your enjoyment, so I'd recommend only using it when having network problems. ⚠️ Having this info on all the time will drain the battery faster, so I'd recommend only using it when having network problems.
## Capture screenshot ## Capture screenshot
- This feature is only available in **Better xCloud**. - This feature is only available in **Better xCloud**.
@ -169,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.7 // @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.7 // @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.7'; 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 = {};
@ -22,6 +22,10 @@ var $STREAM_VIDEO;
var $SCREENSHOT_CANVAS; var $SCREENSHOT_CANVAS;
var GAME_TITLE_ID; var GAME_TITLE_ID;
// Credit: https://www.iconfinder.com/iconsets/user-interface-outline-27
const ICON_VIDEO_SETTINGS = '<path d="M8 2c-1.293 0-2.395.843-2.812 2H3a1 1 0 1 0 0 2h2.186C5.602 7.158 6.706 8 8 8s2.395-.843 2.813-2h10.188a1 1 0 1 0 0-2H10.813C10.395 2.843 9.293 2 8 2zm0 2c.564 0 1 .436 1 1s-.436 1-1 1-1-.436-1-1 .436-1 1-1zm7 5c-1.293 0-2.395.843-2.812 2H3a1 1 0 1 0 0 2h9.186c.417 1.158 1.521 2 2.814 2s2.395-.843 2.813-2H21a1 1 0 1 0 0-2h-3.187c-.418-1.157-1.52-2-2.813-2zm0 2c.564 0 1 .436 1 1s-.436 1-1 1-1-.436-1-1 .436-1 1-1zm-7 5c-1.293 0-2.395.843-2.812 2H3a1 1 0 1 0 0 2h2.188c.417 1.157 1.519 2 2.813 2s2.398-.842 2.814-2H21a1 1 0 1 0 0-2H10.812c-.417-1.157-1.519-2-2.812-2zm0 2c.564 0 1 .436 1 1s-.436 1-1 1-1-.436-1-1 .436-1 1-1z"/>';
const ICON_STREAM_STATS = '<path d="M12.005 5C9.184 5 6.749 6.416 5.009 7.903c-.87.743-1.571 1.51-2.074 2.18-.251.335-.452.644-.605.934-.434.733-.389 1.314-.004 1.98a6.98 6.98 0 0 0 .609.949 13.62 13.62 0 0 0 2.076 2.182C6.753 17.606 9.188 19 12.005 19s5.252-1.394 6.994-2.873a13.62 13.62 0 0 0 2.076-2.182 6.98 6.98 0 0 0 .609-.949c.425-.737.364-1.343-.004-1.98-.154-.29-.354-.599-.605-.934-.503-.669-1.204-1.436-2.074-2.18C17.261 6.416 14.826 5 12.005 5zm0 2c2.135 0 4.189 1.135 5.697 2.424.754.644 1.368 1.32 1.773 1.859.203.27.354.509.351.733s-.151.462-.353.732c-.404.541-1.016 1.214-1.77 1.854C16.198 15.881 14.145 17 12.005 17s-4.193-1.12-5.699-2.398a11.8 11.8 0 0 1-1.77-1.854c-.202-.27-.351-.508-.353-.732s.149-.463.351-.733c.406-.54 1.019-1.215 1.773-1.859C7.816 8.135 9.87 7 12.005 7zm.025 1.975c-1.645 0-3 1.355-3 3s1.355 3 3 3 3-1.355 3-3-1.355-3-3-3zm0 2c.564 0 1 .436 1 1s-.436 1-1 1-1-.436-1-1 .436-1 1-1z"/>';
class MouseCursorHider { class MouseCursorHider {
static #timeout; static #timeout;
@ -60,6 +64,17 @@ class MouseCursorHider {
class StreamBadges { class StreamBadges {
static get BADGE_PLAYTIME() { return 'playtime'; };
static get BADGE_BATTERY() { return 'battery'; };
static get BADGE_IN() { return 'in'; };
static get BADGE_OUT() { return 'out'; };
static get BADGE_SERVER() { return 'server'; };
static get BADGE_VIDEO() { return 'video'; };
static get BADGE_AUDIO() { return 'audio'; };
static get BADGE_BREAK() { return 'break'; };
static ipv6 = false; static ipv6 = false;
static resolution = null; static resolution = null;
static video = null; static video = null;
@ -67,19 +82,129 @@ class StreamBadges {
static fps = 0; static fps = 0;
static region = ''; static region = '';
static startBatteryLevel = 100;
static startTimestamp = 0;
static #cachedDoms = {};
static #interval;
static get #REFRESH_INTERVAL() { return 3000; };
static #renderBadge(name, value, color) { static #renderBadge(name, value, color) {
const CE = createElement; const CE = createElement;
const $badge = CE('div', {'class': 'better-xcloud-badge'},
CE('span', {'class': 'better-xcloud-badge-name'}, name),
CE('span', {'class': 'better-xcloud-badge-value', 'style': `background-color: ${color}`}, value));
if (name === StreamBadges.BADGE_BREAK) {
return CE('div', {'style': 'display: block'});
}
let $badge;
if (StreamBadges.#cachedDoms[name]) {
$badge = StreamBadges.#cachedDoms[name];
$badge.lastElementChild.textContent = value;
return $badge; return $badge;
} }
static render() { $badge = CE('div', {'class': 'better-xcloud-badge'},
let video; CE('span', {'class': 'better-xcloud-badge-name'}, name),
CE('span', {'class': 'better-xcloud-badge-value', 'style': `background-color: ${color}`}, value));
StreamBadges.#cachedDoms[name] = $badge;
return $badge;
}
static async #updateBadges(forceUpdate) {
if (!forceUpdate && !document.querySelector('.better-xcloud-badges')) {
StreamBadges.#stop();
return;
}
// Playtime
let now = +new Date;
const diffSeconds = Math.ceil((now - StreamBadges.startTimestamp) / 1000);
const playtime = StreamBadges.#secondsToHm(diffSeconds);
// Battery
let batteryLevel = '100%';
let batteryLevelInt = 100;
if (navigator.getBattery) {
try {
batteryLevelInt = Math.round((await navigator.getBattery()).level * 100);
batteryLevel = `${batteryLevelInt}%`;
if (batteryLevelInt != StreamBadges.startBatteryLevel) {
const diffLevel = Math.round(batteryLevelInt - StreamBadges.startBatteryLevel);
const sign = diffLevel > 0 ? '+' : '';
batteryLevel += ` (${sign}${diffLevel}%)`;
}
} catch(e) {}
}
const stats = await STREAM_WEBRTC.getStats();
let totalIn = 0;
let totalOut = 0;
stats.forEach(stat => {
if (stat.type === 'candidate-pair' && stat.state == 'succeeded') {
totalIn += stat.bytesReceived;
totalOut += stat.bytesSent;
}
});
const badges = {
[StreamBadges.BADGE_IN]: totalIn ? StreamBadges.#humanFileSize(totalIn) : null,
[StreamBadges.BADGE_OUT]: totalOut ? StreamBadges.#humanFileSize(totalOut) : null,
[StreamBadges.BADGE_PLAYTIME]: playtime,
[StreamBadges.BADGE_BATTERY]: batteryLevel,
};
for (let name in badges) {
const value = badges[name];
if (value === null) {
continue;
}
const $elm = StreamBadges.#cachedDoms[name];
$elm && ($elm.lastElementChild.textContent = value);
if (name === StreamBadges.BADGE_BATTERY) {
if (StreamBadges.startBatteryLevel === 100 && batteryLevelInt === 100) {
$elm.style.display = 'none';
} else {
$elm.style = '';
}
}
}
}
static #stop() {
StreamBadges.#interval && clearInterval(StreamBadges.#interval);
StreamBadges.#interval = null;
}
static #secondsToHm(seconds) {
const h = Math.floor(seconds / 3600);
const m = Math.floor(seconds % 3600 / 60) + 1;
const hDisplay = h > 0 ? `${h}h`: '';
const mDisplay = m > 0 ? `${m}m`: '';
return hDisplay + mDisplay;
}
// https://stackoverflow.com/a/20732091
static #humanFileSize(size) {
let i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
return (size / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i];
}
static async render() {
// Video
let video = '';
if (StreamBadges.resolution) {
video = `${StreamBadges.resolution.height}p`;
}
if (StreamBadges.video) { if (StreamBadges.video) {
video = StreamBadges.video.codec; video && (video += '/');
video += StreamBadges.video.codec;
if (StreamBadges.video.profile) { if (StreamBadges.video.profile) {
let profile = StreamBadges.video.profile; let profile = StreamBadges.video.profile;
profile = profile.startsWith('4d') ? 'High' : (profile.startsWith('42') ? 'Normal' : profile); profile = profile.startsWith('4d') ? 'High' : (profile.startsWith('42') ? 'Normal' : profile);
@ -87,6 +212,7 @@ class StreamBadges {
} }
} }
// Audio
let audio; let audio;
if (StreamBadges.audio) { if (StreamBadges.audio) {
audio = StreamBadges.audio.codec; audio = StreamBadges.audio.codec;
@ -94,17 +220,34 @@ class StreamBadges {
audio += ` (${bitrate} kHz)`; audio += ` (${bitrate} kHz)`;
} }
// Battery
let batteryLevel = '';
if (navigator.getBattery) {
batteryLevel = '100%';
}
// Server + Region
let server = StreamBadges.region;
server += '@' + (StreamBadges.ipv6 ? 'IPv6' : 'IPv4');
const BADGES = [ const BADGES = [
['region', StreamBadges.region, '#d7450b'], [StreamBadges.BADGE_PLAYTIME, '1m', '#ff004d'],
['server', StreamBadges.ipv6 ? 'IPv6' : 'IPv4', '#008746'], [StreamBadges.BADGE_BATTERY, batteryLevel, '#00b543'],
video ? ['video', video, '#007c8f'] : null, [StreamBadges.BADGE_IN, StreamBadges.#humanFileSize(0), '#29adff'],
audio ? ['audio', audio, '#007c8f'] : null, [StreamBadges.BADGE_OUT, StreamBadges.#humanFileSize(0), '#ff77a8'],
StreamBadges.resolution && ['resolution', `${StreamBadges.resolution.width}x${StreamBadges.resolution.height}`, '#ff3977'], [StreamBadges.BADGE_BREAK],
[StreamBadges.BADGE_SERVER, server, '#ff6c24'],
video ? [StreamBadges.BADGE_VIDEO, video, '#742f29'] : null,
audio ? [StreamBadges.BADGE_AUDIO, audio, '#5f574f'] : null,
]; ];
const $wrapper = createElement('div', {'class': 'better-xcloud-badges'}); const $wrapper = createElement('div', {'class': 'better-xcloud-badges'});
BADGES.forEach(item => item && $wrapper.appendChild(StreamBadges.#renderBadge(...item))); BADGES.forEach(item => item && $wrapper.appendChild(StreamBadges.#renderBadge(...item)));
await StreamBadges.#updateBadges(true);
StreamBadges.#stop();
StreamBadges.#interval = setInterval(StreamBadges.#updateBadges, StreamBadges.#REFRESH_INTERVAL);
return $wrapper; return $wrapper;
} }
} }
@ -126,28 +269,83 @@ class StreamStats {
static #lastStat; static #lastStat;
static start() { static #quickGlanceObserver;
StreamStats.#$container.style.display = 'block';
static start(glancing=false) {
if (!StreamStats.isHidden() || (glancing && StreamStats.#isGlancing())) {
return;
}
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.style.display = 'none'; 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.style.display === 'none'; 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;
} }
@ -161,13 +359,15 @@ class StreamStats {
// Packets Lost // Packets Lost
const packetsLost = stat.packetsLost; const packetsLost = stat.packetsLost;
const packetsReceived = stat.packetsReceived || 1; const packetsReceived = stat.packetsReceived;
StreamStats.#$pl.textContent = `${packetsLost} (${(packetsLost * 100 / packetsReceived).toFixed(2)}%)`; const packetsLostPercentage = (packetsLost * 100 / ((packetsLost + packetsReceived) || 1)).toFixed(2);
StreamStats.#$pl.textContent = `${packetsLost} (${packetsLostPercentage}%)`;
// Frames Dropped // Frames Dropped
const framesDropped = stat.framesDropped; const framesDropped = stat.framesDropped;
const framesReceived = stat.framesReceived || 1; const framesReceived = stat.framesReceived;
StreamStats.#$fl.textContent = `${framesDropped} (${(framesDropped * 100 / framesReceived).toFixed(2)}%)`; const framesDroppedPercentage = (framesDropped * 100 / ((framesDropped + framesReceived) || 1)).toFixed(2);
StreamStats.#$fl.textContent = `${framesDropped} (${framesDroppedPercentage}%)`;
if (StreamStats.#lastStat) { if (StreamStats.#lastStat) {
const lastStat = StreamStats.#lastStat; const lastStat = StreamStats.#lastStat;
@ -217,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() {
@ -230,7 +434,7 @@ class StreamStats {
} }
const CE = createElement; const CE = createElement;
StreamStats.#$container = CE('div', {'class': 'better-xcloud-stats-bar'}, StreamStats.#$container = CE('div', {'class': 'better-xcloud-stats-bar better-xcloud-gone'},
CE('label', {}, 'FPS'), CE('label', {}, 'FPS'),
StreamStats.#$fps = CE('span', {}, 0), StreamStats.#$fps = CE('span', {}, 0),
CE('label', {}, 'RTT'), CE('label', {}, 'RTT'),
@ -267,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);
@ -279,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
@ -308,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',
} }
@ -352,6 +564,7 @@ class UserAgent {
get: () => this._state, get: () => this._state,
set: (state) => { set: (state) => {
state.appContext.requestInfo.userAgent = userAgent; state.appContext.requestInfo.userAgent = userAgent;
state.appContext.requestInfo.origin = 'https://www.xbox.com';
this._state = state; this._state = state;
} }
}); });
@ -370,6 +583,8 @@ class Preferences {
static get USE_DESKTOP_CODEC() { return 'use_desktop_codec'; } static get USE_DESKTOP_CODEC() { return 'use_desktop_codec'; }
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_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'; }
@ -385,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'; }
@ -477,6 +693,14 @@ class Preferences {
'label': 'Hide Dots icon while playing', 'label': 'Hide Dots icon while playing',
'default': false, 'default': false,
}, },
[Preferences.STREAM_HIDE_TOUCH_CONTROLLER]: {
'label': 'Disable touch controller',
'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,
@ -499,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',
}, },
@ -537,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': {
@ -724,12 +953,13 @@ 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
} }
.better-xcloud-settings-gone { .better-xcloud-gone {
display: none; display: none !important;
} }
.better-xcloud-settings-wrapper { .better-xcloud-settings-wrapper {
@ -847,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 {
@ -858,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;
@ -911,8 +1142,9 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
} }
.better-xcloud-stats-bar { .better-xcloud-stats-bar {
display: none; 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;
@ -924,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;
} }
@ -999,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 {
@ -1056,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%;
@ -1139,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
@ -1184,6 +1412,80 @@ div[class*=StreamHUD-module__buttonsContainer] {
`; `;
} }
// Hide touch controller
if (PREFS.get(Preferences.STREAM_HIDE_TOUCH_CONTROLLER)) {
css += `
#MultiTouchSurface, #BabylonCanvasContainer-main {
display: none !important;
}
`;
}
// 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);
} }
@ -1454,7 +1756,7 @@ function injectSettingsButton($parent) {
const $button = CE('button', {'class': 'better-xcloud-settings-button'}, PREF_PREFERRED_REGION); const $button = CE('button', {'class': 'better-xcloud-settings-button'}, PREF_PREFERRED_REGION);
$button.addEventListener('click', e => { $button.addEventListener('click', e => {
const $settings = document.querySelector('.better_xcloud_settings'); const $settings = document.querySelector('.better_xcloud_settings');
$settings.classList.toggle('better-xcloud-settings-gone'); $settings.classList.toggle('better-xcloud-gone');
$settings.scrollIntoView(); $settings.scrollIntoView();
}); });
@ -1465,7 +1767,7 @@ function injectSettingsButton($parent) {
$parent.appendChild($button); $parent.appendChild($button);
const $container = CE('div', { const $container = CE('div', {
'class': 'better_xcloud_settings better-xcloud-settings-gone', 'class': 'better_xcloud_settings better-xcloud-gone',
}); });
let $updateAvailable; let $updateAvailable;
@ -1706,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;
} }
@ -1714,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);
@ -1727,7 +2030,7 @@ function injectVideoSettingsButton() {
return; return;
} }
item.addedNodes.forEach(node => { item.addedNodes.forEach(async node => {
if (!node.className || !node.className.startsWith('StreamMenu')) { if (!node.className || !node.className.startsWith('StreamMenu')) {
return; return;
} }
@ -1737,8 +2040,6 @@ function injectVideoSettingsButton() {
return; return;
} }
// Credit: https://www.iconfinder.com/iconsets/user-interface-outline-27
const ICON_VIDEO_SETTINGS = '<path d="M8 2c-1.293 0-2.395.843-2.812 2H3a1 1 0 1 0 0 2h2.186C5.602 7.158 6.706 8 8 8s2.395-.843 2.813-2h10.188a1 1 0 1 0 0-2H10.813C10.395 2.843 9.293 2 8 2zm0 2c.564 0 1 .436 1 1s-.436 1-1 1-1-.436-1-1 .436-1 1-1zm7 5c-1.293 0-2.395.843-2.812 2H3a1 1 0 1 0 0 2h9.186c.417 1.158 1.521 2 2.814 2s2.395-.843 2.813-2H21a1 1 0 1 0 0-2h-3.187c-.418-1.157-1.52-2-2.813-2zm0 2c.564 0 1 .436 1 1s-.436 1-1 1-1-.436-1-1 .436-1 1-1zm-7 5c-1.293 0-2.395.843-2.812 2H3a1 1 0 1 0 0 2h2.188c.417 1.157 1.519 2 2.813 2s2.398-.842 2.814-2H21a1 1 0 1 0 0-2H10.812c-.417-1.157-1.519-2-2.812-2zm0 2c.564 0 1 .436 1 1s-.436 1-1 1-1-.436-1-1 .436-1 1-1z"/>';
// Create Video Settings button // Create Video Settings button
const $btnVideoSettings = cloneStreamMenuButton($orgButton, 'Video settings', ICON_VIDEO_SETTINGS); const $btnVideoSettings = cloneStreamMenuButton($orgButton, 'Video settings', ICON_VIDEO_SETTINGS);
$btnVideoSettings.addEventListener('click', e => { $btnVideoSettings.addEventListener('click', e => {
@ -1749,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
@ -1764,7 +2065,6 @@ function injectVideoSettingsButton() {
$quickBar.style.display = 'none'; $quickBar.style.display = 'none';
}); });
const ICON_STREAM_STATS = '<path d="M12.005 5C9.184 5 6.749 6.416 5.009 7.903c-.87.743-1.571 1.51-2.074 2.18-.251.335-.452.644-.605.934-.434.733-.389 1.314-.004 1.98a6.98 6.98 0 0 0 .609.949 13.62 13.62 0 0 0 2.076 2.182C6.753 17.606 9.188 19 12.005 19s5.252-1.394 6.994-2.873a13.62 13.62 0 0 0 2.076-2.182 6.98 6.98 0 0 0 .609-.949c.425-.737.364-1.343-.004-1.98-.154-.29-.354-.599-.605-.934-.503-.669-1.204-1.436-2.074-2.18C17.261 6.416 14.826 5 12.005 5zm0 2c2.135 0 4.189 1.135 5.697 2.424.754.644 1.368 1.32 1.773 1.859.203.27.354.509.351.733s-.151.462-.353.732c-.404.541-1.016 1.214-1.77 1.854C16.198 15.881 14.145 17 12.005 17s-4.193-1.12-5.699-2.398a11.8 11.8 0 0 1-1.77-1.854c-.202-.27-.351-.508-.353-.732s.149-.463.351-.733c.406-.54 1.019-1.215 1.773-1.859C7.816 8.135 9.87 7 12.005 7zm.025 1.975c-1.645 0-3 1.355-3 3s1.355 3 3 3 3-1.355 3-3-1.355-3-3-3zm0 2c.564 0 1 .436 1 1s-.436 1-1 1-1-.436-1-1 .436-1 1-1z"/>';
// Create Stream Stats button // Create Stream Stats button
const $btnStreamStats = cloneStreamMenuButton($orgButton, 'Stream stats', ICON_STREAM_STATS); const $btnStreamStats = cloneStreamMenuButton($orgButton, 'Stream stats', ICON_STREAM_STATS);
$btnStreamStats.addEventListener('click', e => { $btnStreamStats.addEventListener('click', e => {
@ -1780,9 +2080,37 @@ function injectVideoSettingsButton() {
// Insert after Video Settings button // Insert after Video Settings button
$orgButton.parentElement.insertBefore($btnStreamStats, $btnVideoSettings); $orgButton.parentElement.insertBefore($btnStreamStats, $btnVideoSettings);
// Get "Quit game" button
const $btnQuit = $orgButton.parentElement.querySelector('button:last-of-type');
let isHolding = false;
let holdTimeout;
const onMouseDown = e => {
isHolding = false;
holdTimeout = setTimeout(() => {
isHolding = true;
confirm('Do you want to refresh the stream?') && window.location.reload();
}, 1000);
};
const onMouseUp = e => {
holdTimeout && clearTimeout(holdTimeout);
if (isHolding) {
e.preventDefault();
e.stopPropagation();
}
isHolding = false;
};
$btnQuit.addEventListener('mousedown', onMouseDown);
$btnQuit.addEventListener('click', onMouseUp);
$btnQuit.addEventListener('touchstart', onMouseDown);
$btnQuit.addEventListener('touchend', onMouseUp);
// Render stream badges // Render stream badges
const $menu = document.querySelector('div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module]'); const $menu = document.querySelector('div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module]');
$menu.appendChild(StreamBadges.render()); $menu.appendChild(await StreamBadges.render());
}); });
}); });
}); });
@ -1793,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;
@ -1804,36 +2133,76 @@ 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};
STREAM_WEBRTC.getStats().then(stats => { StreamBadges.resolution = {width: this.videoWidth, height: this.videoHeight};
stats.forEach(stat => { StreamBadges.startTimestamp = +new Date;
if (stat.type !== 'codec') {
return; // Get battery level
if (navigator.getBattery) {
try {
navigator.getBattery().then(bm => {
StreamBadges.startBatteryLevel = Math.round(bm.level * 100);
});
} catch(e) {}
} }
STREAM_WEBRTC.getStats().then(stats => {
const allVideoCodecs = {};
let videoCodecId;
const allAudioCodecs = {};
let audioCodecId;
stats.forEach(stat => {
if (stat.type == 'codec') {
const mimeType = stat.mimeType.split('/'); const mimeType = stat.mimeType.split('/');
if (mimeType[0] === 'video') { if (mimeType[0] === 'video') {
// Store all video stats
allVideoCodecs[stat.id] = stat;
} else if (mimeType[0] === 'audio') {
// Store all audio stats
allAudioCodecs[stat.id] = stat;
}
} else if (stat.type === 'inbound-rtp' && stat.packetsReceived > 0) {
// Get the codecId of the video/audio track currently being used
if (stat.kind === 'video') {
videoCodecId = stat.codecId;
} else if (stat.kind === 'audio') {
audioCodecId = stat.codecId;
}
}
});
// Get video codec from codecId
if (videoCodecId) {
const videoStat = allVideoCodecs[videoCodecId];
const video = { const video = {
codec: mimeType[1], codec: videoStat.mimeType.substring(6),
}; };
if (video.codec === 'H264') { if (video.codec === 'H264') {
const match = /profile-level-id=([0-9a-f]{6})/.exec(stat.sdpFmtpLine); const match = /profile-level-id=([0-9a-f]{6})/.exec(videoStat.sdpFmtpLine);
video.profile = match ? match[1] : null; video.profile = match ? match[1] : null;
} }
StreamBadges.video = video; StreamBadges.video = video;
} else if (!StreamBadges.audio && mimeType[0] === 'audio') {
StreamBadges.audio = {
codec: mimeType[1],
bitrate: stat.clockRate,
};
} }
});
// 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();
@ -2097,7 +2466,7 @@ function patchHistoryMethod(type) {
function onHistoryChange() { function onHistoryChange() {
const $settings = document.querySelector('.better_xcloud_settings'); const $settings = document.querySelector('.better_xcloud_settings');
if ($settings) { if ($settings) {
$settings.classList.add('better-xcloud-settings-gone'); $settings.classList.add('better-xcloud-gone');
} }
const $quickBar = document.querySelector('.better-xcloud-quick-settings-bar'); const $quickBar = document.querySelector('.better-xcloud-quick-settings-bar');
@ -2107,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();
@ -2156,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,
});