mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-07-06 06:11:43 +02:00
Compare commits
20 Commits
Author | SHA1 | Date | |
---|---|---|---|
ab6208a948 | |||
bae8c193d3 | |||
b9283eb435 | |||
e73e018380 | |||
e98ad00bb9 | |||
d232dc164f | |||
ee192bc98e | |||
1210176413 | |||
9262d0e14f | |||
df87a51329 | |||
8c7013659b | |||
b3b7a51979 | |||
831ccb31c1 | |||
d87ac78e57 | |||
67b419c37d | |||
a7b796362a | |||
889ee890f1 | |||
71a48f8afb | |||
354ecac97e | |||
45a7c28d3f |
94
README.md
94
README.md
@ -4,9 +4,14 @@ The main target of this script is mobile users, but it should work great on desk
|
|||||||
|
|
||||||
Give this project a 🌟 if you like it. Thank you 🙏.
|
Give this project a 🌟 if you like it. Thank you 🙏.
|
||||||
|
|
||||||
|
[](https://github.com/redphx/better-xcloud/releases)
|
||||||
|
[](https://github.com/redphx/better-xcloud/releases)
|
||||||
|
[](https://github.com/redphx/better-xcloud/stargazers)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
<img width="475" alt="Settings UI" src="https://github.com/redphx/better-xcloud/assets/96280/ad687344-214d-4822-affe-21f1b1e105c8">
|
<img width="475" alt="Settings UI" src="https://github.com/redphx/better-xcloud/assets/96280/20aca05d-ff20-4adb-ac21-08b4b1cfd07f">
|
||||||
|
|
||||||
<img width="475" alt="Stream HUD UI" src="https://github.com/redphx/better-xcloud/assets/96280/ff695d3a-b077-4b21-b778-beb0a1fdd6be">
|
<img width="475" alt="Stream HUD UI" src="https://github.com/redphx/better-xcloud/assets/96280/ff695d3a-b077-4b21-b778-beb0a1fdd6be">
|
||||||
|
|
||||||
|
|
||||||
@ -15,21 +20,19 @@ Give this project a 🌟 if you like it. Thank you 🙏.
|
|||||||
|
|
||||||
- **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.
|
||||||
> Not working in Hermit ([#5](https://github.com/redphx/better-xcloud/issues/5)).
|
|
||||||
- **Force 1080p stream**
|
- **Force 1080p stream**
|
||||||
> By default you only get 1080p stream when playing on desktop.
|
> By default you only get 1080p stream when playing on desktop.
|
||||||
> This feature will give you 1080p stream even on mobile, without having to change User-Agent.
|
> This feature will give you 1080p stream even on mobile, without having to change User-Agent.
|
||||||
> Not working in Hermit ([#5](https://github.com/redphx/better-xcloud/issues/5)).
|
|
||||||
- **Force high quality codec (if possible)<sup>(\*)</sup>**
|
- **Force high quality codec (if possible)<sup>(\*)</sup>**
|
||||||
> Force xCloud to use the best streaming codec profile (same as desktop & TV) if possible. You don't have to change User-Agent anymore.
|
> Force xCloud to use the best streaming codec profile (same as desktop & TV) if possible. You don't have to change User-Agent anymore.
|
||||||
> You should enable this feature even if you're on desktop.
|
> You should enable this feature even if you're on desktop.
|
||||||
> Not available for some browsers (Firefox, Safari...).
|
> Not available for some browsers (Firefox, Safari...). Use the [changing User-Agent method](https://github.com/redphx/better-xcloud/wiki/User‐Agent) instead.
|
||||||
> Use more bandwidth & battery.
|
> Use more bandwidth & battery.
|
||||||
> Comparison video with the setting ON & OFF: https://youtu.be/-9PuBJJSgR4
|
> Comparison video with the setting ON & OFF: https://youtu.be/-9PuBJJSgR4
|
||||||
- **Prefer IPv6 streaming server**
|
- **Prefer IPv6 streaming server**
|
||||||
> Might reduce latency.
|
> Might reduce latency.
|
||||||
- **Disable bandwidth checking**
|
- **Disable bandwidth checking**
|
||||||
> xCloud won't reduce quality when the internet speed is slow
|
> xCloud won't warn about slow connection speed.
|
||||||
- **🔥 Capture screenshot**
|
- **🔥 Capture screenshot**
|
||||||
> Exclusive to **Better xCloud**. Check the [**Capture screenshot** section](#capture-screenshot) for more info.
|
> Exclusive to **Better xCloud**. Check the [**Capture screenshot** section](#capture-screenshot) for more info.
|
||||||
- **Skip Xbox splash video**
|
- **Skip Xbox splash video**
|
||||||
@ -46,10 +49,8 @@ Give this project a 🌟 if you like it. Thank you 🙏.
|
|||||||
> Region/Server/Quality/Resolution...
|
> Region/Server/Quality/Resolution...
|
||||||
- **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.
|
||||||
> Not working in Hermit ([#5](https://github.com/redphx/better-xcloud/issues/5)).
|
|
||||||
- **Disable xCloud analytics**
|
- **Disable xCloud analytics**
|
||||||
> The analytics contains statistics of your streaming session, so I'd recommend to allow analytics to help Xbox improve xCloud's experence 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.
|
||||||
> Not working in Hermit ([#5](https://github.com/redphx/better-xcloud/issues/5)).
|
|
||||||
- **Hide footer and other UI elements**
|
- **Hide footer and other UI elements**
|
||||||
- **🔥 Show stream stats**
|
- **🔥 Show stream stats**
|
||||||
> Check [Stream stats section](#stream-stats) for more info.
|
> Check [Stream stats section](#stream-stats) for more info.
|
||||||
@ -89,88 +90,81 @@ Don't see your browser in the table? If it supports Tampermonkey/Userscript then
|
|||||||
|
|
||||||
---
|
---
|
||||||
- **Kiwi Browser** is the best choice on Android. All features work, it means you can get 1080p stream + high quality codec profile (the best possible quality).
|
- **Kiwi Browser** is the best choice on Android. All features work, it means you can get 1080p stream + high quality codec profile (the best possible quality).
|
||||||
- **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">
|
||||||
|
|
||||||
- While playing > `...` > `Stream Stats`.
|
- While playing > `...` > `Stream Stats`.
|
||||||
|
- Double-click on the stats bar to show Settings dialog.
|
||||||
- This bar is updated every second.
|
- This bar is updated every second.
|
||||||
|
- Showing the stats bar does affect 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 |
|
| 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) |
|
||||||
| RTT | Round Trip Time | The number of seconds it takes for data to be sent from your device to the server and back over (lower is better) |
|
| DT | Decode Time | The average time it took to decode one frame in the last second (might be bugged) |
|
||||||
| BR | Bitrate | The amount of data server send to your device in the last second |
|
| 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) |
|
||||||
| PL | Packets Lost | Total number of packets lost |
|
| BR | Bitrate | The amount of data the server sent to your device in the last second |
|
||||||
| FL | Frames Lost | The total number of frames dropped prior to decode or dropped because the frame missed its display deadline |
|
| PL | Packets Lost | The total number of packets lost |
|
||||||
|
| FL | Frames Lost | The total number of frames dropped prior to decode or dropped because the frame missed its display deadline |
|
||||||
|
|
||||||
These info are provied by WebRTC API. You can use browser's built-in tool to see more info:
|
This info is provided by WebRTC API. You can use browser's built-in tool to see more info:
|
||||||
- Chrome/Edge/Chromium variants: `chrome://webrtc-internals`
|
- Chrome/Edge/Chromium variants: `chrome://webrtc-internals`
|
||||||
- Firefox: `about:webrtc`
|
- Firefox: `about:webrtc`
|
||||||
|
|
||||||
|
Colors:
|
||||||
|
- Red = Bad
|
||||||
|
- Yellow = Okay
|
||||||
|
- Green = Good
|
||||||
|
- White = Great
|
||||||
|
|
||||||
|
📝 Having this info on all the time might reduce your enjoyment, 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**.
|
||||||
- Works on both desktop & mobile, but it's designed for mobile users.
|
- Works on both desktop & mobile, but it was designed for mobile users.
|
||||||
- It's client-side only.
|
- It's client-side only.
|
||||||
- It captures the current frame of the stream and save to a file. That means you won't get the raw quality like when you play on console, but it's still better than using the built-in screenshot feature on your phone.
|
- It captures the current frame of the stream and saves it to a file. That means you won't get the raw quality like when you play on a console, but it's still better than using the built-in screenshot feature on your phone.
|
||||||
- Screenshot's resolution & quality depend on the quality of the stream at the moment.
|
- Screenshot's resolution & quality depend on the quality of the stream at the moment.
|
||||||
- Screenshot doesn't include touch UI, notification bar... only the gameplay.
|
- Screenshot doesn't include touch UI, notification bar... only the gameplay.
|
||||||
- There might be a slight delay.
|
- There might be a slight delay.
|
||||||
- ⚠️ It's not possible to map the Share/Screenshot button on your controller to this feature.
|
- ⚠️ It's not possible to map the Share/Screenshot button on your controller to this feature.
|
||||||
|
|
||||||
### How to capture screenshot
|
### How to capture screenshot
|
||||||
1. Enable this feature in setting.
|
1. Enable this feature in the Settings.
|
||||||
2. Play a game.
|
2. Play a game.
|
||||||
3. Tap once at the bottom left/right (depend on your setting) to show the Screenshot button.
|
3. Tap once at the bottom left/right (depending on your setting) to show the Screenshot button.
|
||||||
4. Tap on that button to capture screenshot.
|
4. Tap on that button to capture screenshot.
|
||||||
5. Screenshot will be saved by browser.
|
5. Screenshot will be saved by the browser.
|
||||||
6. You can double tap that corner to capture screenshot.
|
6. You can double-tap that corner to capture screenshot.
|
||||||
|
|
||||||
<img width="600" alt="Screenshot button" src="https://github.com/redphx/better-xcloud/assets/96280/a911b141-5dc0-450a-aeac-30d9cf202b44">
|
<img width="600" alt="Screenshot button" src="https://github.com/redphx/better-xcloud/assets/96280/a911b141-5dc0-450a-aeac-30d9cf202b44">
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
1. **Will I get banned for using this?**
|
1. **Will I get banned for using this?**
|
||||||
I think it's very unlikely that you'll get banned for using this. Most of the features only affect client-side, except for switching region of streaming server (you'll connect to another server instead of the default one). If you want to be safe just avoid using that. As always, use as your own risk.
|
I think it's very unlikely that you'll get banned for using this. Most of the features only affect client-side, except for switching region of streaming server (you'll connect to another server instead of the default one). If you want to be safe just avoid using that. As always, use it as your own risk.
|
||||||
|
|
||||||
2. **Why is it an Userscript and not 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 server's name**
|
3. **I see "???" button instead of the 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.
|
That means Tampermonkey is not working properly. Please make sure you're using the latest version or switch to a well-known browser.
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
5. **Will you able to enable "Clarity Boost" feature on non-Edge browsers?**
|
5. **Will you be able to enable the "Clarity Boost" feature on non-Edge browsers?**
|
||||||
No. "Clarity Boost" feature uses an exclusive API (`Video.msVideoProcessing`) that's only available on Edge browser for desktop at the moment.
|
No. The "Clarity Boost" feature uses an exclusive API (`Video.msVideoProcessing`) that's only available on Edge browser for desktop at the moment.
|
||||||
|
|
||||||
## User-Agent
|
## User-Agent
|
||||||
You're no longer needed to change User-Agent since you can just use the **Force high quality stream** setting.
|
Moved to [wiki](https://github.com/redphx/better-xcloud/wiki/User‐Agent).
|
||||||
If your browser doesn't support **Force high quality stream** setting, try changing User-Agent to:
|
|
||||||
```
|
|
||||||
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
|
|
||||||
```
|
|
||||||
This will change your device to a Samsung TV running Tizen OS. It will improve the stream quality.
|
|
||||||
|
|
||||||
---
|
|
||||||
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.
|
|
||||||
|
|
||||||
## Acknowledgements
|
## Acknowledgements
|
||||||
- [n-thumann/xbox-cloud-server-selector](https://github.com/n-thumann/xbox-cloud-server-selector) for the idea of IPv6 feature
|
- [n-thumann/xbox-cloud-server-selector](https://github.com/n-thumann/xbox-cloud-server-selector) for the idea of IPv6 feature
|
||||||
- Icons by [Adam Design](https://www.iconfinder.com/iconsets/user-interface-outline-27)
|
- Icons by [Adam Design](https://www.iconfinder.com/iconsets/user-interface-outline-27)
|
||||||
|
|
||||||
## Disclaimers
|
## Disclaimers
|
||||||
- Use as your own risk.
|
- Use as it your own risk.
|
||||||
- This project is not affiliated with Xbox in any way. All Xbox logos/icons/trademarks are copyright of their respective owners.
|
- This project is not affiliated with Xbox in any way. All Xbox logos/icons/trademarks are copyright of their respective owners.
|
||||||
|
5
better-xcloud.meta.js
Normal file
5
better-xcloud.meta.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name Better xCloud
|
||||||
|
// @namespace https://github.com/redphx
|
||||||
|
// @version 1.6.2
|
||||||
|
// ==/UserScript==
|
@ -1,19 +1,19 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name Better xCloud
|
// @name Better xCloud
|
||||||
// @namespace https://github.com/redphx
|
// @namespace https://github.com/redphx
|
||||||
// @version 1.6
|
// @version 1.6.2
|
||||||
// @description Improve Xbox Cloud Gaming (xCloud) experience
|
// @description Improve Xbox Cloud Gaming (xCloud) experience
|
||||||
// @author redphx
|
// @author redphx
|
||||||
// @license MIT
|
// @license MIT
|
||||||
// @match https://www.xbox.com/*/play*
|
// @match https://www.xbox.com/*/play*
|
||||||
// @run-at document-start
|
// @run-at document-start
|
||||||
// @grant none
|
// @grant none
|
||||||
// @updateURL https://github.com/redphx/better-xcloud/releases/latest/download/better-xcloud.user.js
|
// @updateURL https://raw.githubusercontent.com/redphx/better-xcloud/main/better-xcloud.meta.js
|
||||||
// @downloadURL https://github.com/redphx/better-xcloud/releases/latest/download/better-xcloud.user.js
|
// @downloadURL https://github.com/redphx/better-xcloud/releases/latest/download/better-xcloud.user.js
|
||||||
// ==/UserScript==
|
// ==/UserScript==
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const SCRIPT_VERSION = '1.6';
|
const SCRIPT_VERSION = '1.6.2';
|
||||||
const SCRIPT_HOME = 'https://github.com/redphx/better-xcloud';
|
const SCRIPT_HOME = 'https://github.com/redphx/better-xcloud';
|
||||||
|
|
||||||
const SERVER_REGIONS = {};
|
const SERVER_REGIONS = {};
|
||||||
@ -74,28 +74,32 @@ class StreamBadges {
|
|||||||
|
|
||||||
|
|
||||||
class StreamStats {
|
class StreamStats {
|
||||||
static #timeout;
|
static #interval;
|
||||||
static #updateInterval = 1000;
|
static #updateInterval = 1000;
|
||||||
|
|
||||||
static #$container;
|
static #$container;
|
||||||
static #$fps;
|
static #$fps;
|
||||||
static #$rtt;
|
static #$rtt;
|
||||||
|
static #$dt;
|
||||||
static #$pl;
|
static #$pl;
|
||||||
static #$fl;
|
static #$fl;
|
||||||
static #$br;
|
static #$br;
|
||||||
|
|
||||||
static #lastInbound;
|
static #$settings;
|
||||||
|
|
||||||
|
static #lastStat;
|
||||||
|
|
||||||
static start() {
|
static start() {
|
||||||
StreamStats.#$container.style.display = 'block';
|
StreamStats.#$container.style.display = 'block';
|
||||||
StreamStats.update();
|
StreamStats.#interval = setInterval(StreamStats.update, StreamStats.#updateInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
static stop() {
|
static stop() {
|
||||||
|
clearInterval(StreamStats.#interval);
|
||||||
|
|
||||||
StreamStats.#$container.style.display = 'none';
|
StreamStats.#$container.style.display = 'none';
|
||||||
clearTimeout(StreamStats.#timeout);
|
StreamStats.#interval = null;
|
||||||
StreamStats.#timeout = null;
|
StreamStats.#lastStat = null;
|
||||||
StreamStats.#lastInbound = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static toggle() {
|
static toggle() {
|
||||||
@ -105,22 +109,20 @@ class StreamStats {
|
|||||||
static #isHidden = () => StreamStats.#$container.style.display === 'none';
|
static #isHidden = () => StreamStats.#$container.style.display === 'none';
|
||||||
|
|
||||||
static update() {
|
static update() {
|
||||||
if (StreamStats.#isHidden()) {
|
if (StreamStats.#isHidden() || !STREAM_WEBRTC) {
|
||||||
return;
|
StreamStats.stop();
|
||||||
}
|
|
||||||
|
|
||||||
if (!STREAM_WEBRTC) {
|
|
||||||
StreamStats.#timeout = setTimeout(StreamStats.update, StreamStats.#updateInterval);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PREF_STATS_CONDITIONAL_FORMATTING = PREFS.get(Preferences.STATS_CONDITIONAL_FORMATTING);
|
||||||
STREAM_WEBRTC.getStats().then(stats => {
|
STREAM_WEBRTC.getStats().then(stats => {
|
||||||
stats.forEach(stat => {
|
stats.forEach(stat => {
|
||||||
|
let grade = '';
|
||||||
if (stat.type === 'inbound-rtp' && stat.kind === 'video') {
|
if (stat.type === 'inbound-rtp' && stat.kind === 'video') {
|
||||||
// FPS
|
// FPS
|
||||||
StreamStats.#$fps.textContent = stat.framesPerSecond || 0;
|
StreamStats.#$fps.textContent = stat.framesPerSecond || 0;
|
||||||
|
|
||||||
// Packets Loss
|
// Packets Lost
|
||||||
const packetsLost = stat.packetsLost;
|
const packetsLost = stat.packetsLost;
|
||||||
const packetsReceived = stat.packetsReceived || 1;
|
const packetsReceived = stat.packetsReceived || 1;
|
||||||
StreamStats.#$pl.textContent = `${packetsLost} (${(packetsLost * 100 / packetsReceived).toFixed(2)}%)`;
|
StreamStats.#$pl.textContent = `${packetsLost} (${(packetsLost * 100 / packetsReceived).toFixed(2)}%)`;
|
||||||
@ -130,25 +132,61 @@ class StreamStats {
|
|||||||
const framesReceived = stat.framesReceived || 1;
|
const framesReceived = stat.framesReceived || 1;
|
||||||
StreamStats.#$fl.textContent = `${framesDropped} (${(framesDropped * 100 / framesReceived).toFixed(2)}%)`;
|
StreamStats.#$fl.textContent = `${framesDropped} (${(framesDropped * 100 / framesReceived).toFixed(2)}%)`;
|
||||||
|
|
||||||
// Bitrate
|
if (StreamStats.#lastStat) {
|
||||||
if (StreamStats.#lastInbound) {
|
const lastStat = StreamStats.#lastStat;
|
||||||
const timeDiff = stat.timestamp - StreamStats.#lastInbound.timestamp;
|
// Bitrate
|
||||||
const bitrate = 8 * (stat.bytesReceived - StreamStats.#lastInbound.bytesReceived) / timeDiff / 1000;
|
const timeDiff = stat.timestamp - lastStat.timestamp;
|
||||||
|
const bitrate = 8 * (stat.bytesReceived - lastStat.bytesReceived) / timeDiff / 1000;
|
||||||
StreamStats.#$br.textContent = `${bitrate.toFixed(2)} Mbps`;
|
StreamStats.#$br.textContent = `${bitrate.toFixed(2)} Mbps`;
|
||||||
|
|
||||||
|
// Decode time
|
||||||
|
const totalDecodeTimeDiff = stat.totalDecodeTime - lastStat.totalDecodeTime;
|
||||||
|
const framesDecodedDiff = stat.framesDecoded - lastStat.framesDecoded;
|
||||||
|
const currentDecodeTime = totalDecodeTimeDiff / framesDecodedDiff * 1000;
|
||||||
|
StreamStats.#$dt.textContent = `${currentDecodeTime.toFixed(2)}ms`;
|
||||||
|
|
||||||
|
if (PREF_STATS_CONDITIONAL_FORMATTING) {
|
||||||
|
grade = (currentDecodeTime > 12) ? 'bad' : (currentDecodeTime > 9) ? 'ok' : (currentDecodeTime > 6) ? 'good' : '';
|
||||||
|
}
|
||||||
|
StreamStats.#$dt.setAttribute('data-grade', grade);
|
||||||
}
|
}
|
||||||
|
|
||||||
StreamStats.#lastInbound = stat;
|
StreamStats.#lastStat = stat;
|
||||||
} else if (stat.type === 'candidate-pair' && stat.state === 'succeeded') {
|
} else if (stat.type === 'candidate-pair' && stat.state === 'succeeded') {
|
||||||
// Round Trip Time
|
// Round Trip Time
|
||||||
const roundTripTime = typeof stat.currentRoundTripTime !== 'undefined' ? stat.currentRoundTripTime * 1000 : '???';
|
const roundTripTime = typeof stat.currentRoundTripTime !== 'undefined' ? stat.currentRoundTripTime * 1000 : '???';
|
||||||
StreamStats.#$rtt.textContent = `${roundTripTime}ms`;
|
StreamStats.#$rtt.textContent = `${roundTripTime}ms`;
|
||||||
|
|
||||||
|
if (PREF_STATS_CONDITIONAL_FORMATTING) {
|
||||||
|
grade = (roundTripTime > 100) ? 'bad' : (roundTripTime > 75) ? 'ok' : (roundTripTime > 40) ? 'good' : '';
|
||||||
|
}
|
||||||
|
StreamStats.#$rtt.setAttribute('data-grade', grade);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
StreamStats.#timeout = setTimeout(StreamStats.update, StreamStats.#updateInterval);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static #refreshStyles() {
|
||||||
|
const PREF_POSITION = PREFS.get(Preferences.STATS_POSITION);
|
||||||
|
const PREF_TRANSPARENT = PREFS.get(Preferences.STATS_TRANSPARENT);
|
||||||
|
const PREF_OPACITY = PREFS.get(Preferences.STATS_OPACITY);
|
||||||
|
const PREF_TEXT_SIZE = PREFS.get(Preferences.STATS_TEXT_SIZE);
|
||||||
|
|
||||||
|
StreamStats.#$container.setAttribute('data-position', PREF_POSITION);
|
||||||
|
StreamStats.#$container.setAttribute('data-transparent', PREF_TRANSPARENT);
|
||||||
|
StreamStats.#$container.style.opacity = PREF_OPACITY + '%';
|
||||||
|
StreamStats.#$container.style.fontSize = PREF_TEXT_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static hideSettingsUi() {
|
||||||
|
StreamStats.#$settings.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
static #toggleSettingsUi() {
|
||||||
|
const display = StreamStats.#$settings.style.display;
|
||||||
|
StreamStats.#$settings.style.display = display === 'block' ? 'none' : 'block';
|
||||||
|
}
|
||||||
|
|
||||||
static render() {
|
static render() {
|
||||||
if (StreamStats.#$container) {
|
if (StreamStats.#$container) {
|
||||||
return;
|
return;
|
||||||
@ -160,6 +198,8 @@ class StreamStats {
|
|||||||
StreamStats.#$fps = CE('span', {}, 0),
|
StreamStats.#$fps = CE('span', {}, 0),
|
||||||
CE('label', {}, 'RTT'),
|
CE('label', {}, 'RTT'),
|
||||||
StreamStats.#$rtt = CE('span', {}, '0ms'),
|
StreamStats.#$rtt = CE('span', {}, '0ms'),
|
||||||
|
CE('label', {}, 'DT'),
|
||||||
|
StreamStats.#$dt = CE('span', {}, '0ms'),
|
||||||
CE('label', {}, 'BR'),
|
CE('label', {}, 'BR'),
|
||||||
StreamStats.#$br = CE('span', {}, '0 Mbps'),
|
StreamStats.#$br = CE('span', {}, '0 Mbps'),
|
||||||
CE('label', {}, 'PL'),
|
CE('label', {}, 'PL'),
|
||||||
@ -167,12 +207,75 @@ class StreamStats {
|
|||||||
CE('label', {}, 'FL'),
|
CE('label', {}, 'FL'),
|
||||||
StreamStats.#$fl = CE('span', {}, '0 (0.00%)'));
|
StreamStats.#$fl = CE('span', {}, '0 (0.00%)'));
|
||||||
|
|
||||||
|
let clickTimeout;
|
||||||
|
StreamStats.#$container.addEventListener('mousedown', e => {
|
||||||
|
clearTimeout(clickTimeout);
|
||||||
|
if (clickTimeout) {
|
||||||
|
// Double-clicked
|
||||||
|
clickTimeout = null;
|
||||||
|
StreamStats.#toggleSettingsUi();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clickTimeout = setTimeout(() => {
|
||||||
|
clickTimeout = null;
|
||||||
|
}, 400);
|
||||||
|
});
|
||||||
|
|
||||||
document.documentElement.appendChild(StreamStats.#$container);
|
document.documentElement.appendChild(StreamStats.#$container);
|
||||||
|
|
||||||
|
const refreshFunc = e => {
|
||||||
|
StreamStats.#refreshStyles()
|
||||||
|
};
|
||||||
|
const $position = PREFS.toElement(Preferences.STATS_POSITION, refreshFunc);
|
||||||
|
|
||||||
|
let $close;
|
||||||
|
const $showStartup = PREFS.toElement(Preferences.STATS_SHOW_WHEN_PLAYING, refreshFunc);
|
||||||
|
const $transparent = PREFS.toElement(Preferences.STATS_TRANSPARENT, refreshFunc);
|
||||||
|
const $formatting = PREFS.toElement(Preferences.STATS_CONDITIONAL_FORMATTING, refreshFunc);
|
||||||
|
const $opacity = PREFS.toElement(Preferences.STATS_OPACITY, refreshFunc);
|
||||||
|
const $textSize = PREFS.toElement(Preferences.STATS_TEXT_SIZE, refreshFunc);
|
||||||
|
|
||||||
|
StreamStats.#$settings = CE('div', {'class': 'better_xcloud_stats_settings'},
|
||||||
|
CE('b', {}, 'Stream Stats Settings'),
|
||||||
|
CE('div', {},
|
||||||
|
CE('label', {'for': `xcloud_setting_${Preferences.STATS_SHOW_WHEN_PLAYING}`}, 'Show stats when starting the game'),
|
||||||
|
$showStartup
|
||||||
|
),
|
||||||
|
CE('div', {},
|
||||||
|
CE('label', {}, 'Position'),
|
||||||
|
$position
|
||||||
|
),
|
||||||
|
CE('div', {},
|
||||||
|
CE('label', {}, 'Text size'),
|
||||||
|
$textSize
|
||||||
|
),
|
||||||
|
CE('div', {},
|
||||||
|
CE('label', {'for': `xcloud_setting_${Preferences.STATS_OPACITY}`}, 'Opacity (50-100%)'),
|
||||||
|
$opacity
|
||||||
|
),
|
||||||
|
CE('div', {},
|
||||||
|
CE('label', {'for': `xcloud_setting_${Preferences.STATS_TRANSPARENT}`}, 'Transparent background'),
|
||||||
|
$transparent
|
||||||
|
),
|
||||||
|
CE('div', {},
|
||||||
|
CE('label', {'for': `xcloud_setting_${Preferences.STATS_CONDITIONAL_FORMATTING}`}, 'Conditional formatting text color'),
|
||||||
|
$formatting
|
||||||
|
),
|
||||||
|
$close = CE('button', {}, 'Close'));
|
||||||
|
|
||||||
|
$close.addEventListener('click', e => StreamStats.hideSettingsUi());
|
||||||
|
document.documentElement.appendChild(StreamStats.#$settings);
|
||||||
|
|
||||||
|
StreamStats.#refreshStyles();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class Preferences {
|
class Preferences {
|
||||||
|
static get LAST_UPDATE_CHECK() { return 'last_update_check'; }
|
||||||
|
static get LATEST_VERSION() { return 'latest_version'; }
|
||||||
|
|
||||||
static get SERVER_REGION() { return 'server_region'; }
|
static get SERVER_REGION() { return 'server_region'; }
|
||||||
static get PREFER_IPV6_SERVER() { return 'prefer_ipv6_server'; }
|
static get PREFER_IPV6_SERVER() { return 'prefer_ipv6_server'; }
|
||||||
static get FORCE_1080P_STREAM() { return 'force_1080p_stream'; }
|
static get FORCE_1080P_STREAM() { return 'force_1080p_stream'; }
|
||||||
@ -191,84 +294,127 @@ class Preferences {
|
|||||||
static get VIDEO_CONTRAST() { return 'video_contrast'; }
|
static get VIDEO_CONTRAST() { return 'video_contrast'; }
|
||||||
static get VIDEO_SATURATION() { return 'video_saturation'; }
|
static get VIDEO_SATURATION() { return 'video_saturation'; }
|
||||||
|
|
||||||
static SETTINGS = [
|
static get STATS_SHOW_WHEN_PLAYING() { return 'stats_show_when_playing'; }
|
||||||
{
|
static get STATS_POSITION() { return 'stats_position'; }
|
||||||
'id': Preferences.SERVER_REGION,
|
static get STATS_TEXT_SIZE() { return 'stats_text_size'; }
|
||||||
|
static get STATS_TRANSPARENT() { return 'stats_transparent'; }
|
||||||
|
static get STATS_OPACITY() { return 'stats_opacity'; }
|
||||||
|
static get STATS_CONDITIONAL_FORMATTING() { return 'stats_conditional_formatting'; }
|
||||||
|
|
||||||
|
static SETTINGS = {
|
||||||
|
[Preferences.SERVER_REGION]: {
|
||||||
'label': 'Region of streaming server',
|
'label': 'Region of streaming server',
|
||||||
'default': 'default',
|
'default': 'default',
|
||||||
}, {
|
},
|
||||||
'id': Preferences.FORCE_1080P_STREAM,
|
[Preferences.FORCE_1080P_STREAM]: {
|
||||||
'label': 'Force 1080p stream',
|
'label': 'Force 1080p stream',
|
||||||
'default': false,
|
'default': false,
|
||||||
}, {
|
},
|
||||||
'id': Preferences.USE_DESKTOP_CODEC,
|
[Preferences.USE_DESKTOP_CODEC]: {
|
||||||
'label': 'Force high quality codec (if possible)',
|
'label': 'Force high-quality codec (if supported)',
|
||||||
'default': false,
|
'default': false,
|
||||||
}, {
|
},
|
||||||
'id': Preferences.PREFER_IPV6_SERVER,
|
[Preferences.PREFER_IPV6_SERVER]: {
|
||||||
'label': 'Prefer IPv6 streaming server',
|
'label': 'Prefer IPv6 streaming server',
|
||||||
'default': false,
|
'default': false,
|
||||||
}, {
|
},
|
||||||
'id': Preferences.DISABLE_BANDWIDTH_CHECKING,
|
[Preferences.DISABLE_BANDWIDTH_CHECKING]: {
|
||||||
'label': 'Disable bandwidth checking',
|
'label': 'Disable bandwidth checking',
|
||||||
'default': false,
|
'default': false,
|
||||||
}, {
|
},
|
||||||
'id': Preferences.SCREENSHOT_BUTTON_POSITION,
|
[Preferences.SCREENSHOT_BUTTON_POSITION]: {
|
||||||
'label': 'Screenshot button\'s position',
|
'label': 'Screenshot button\'s position',
|
||||||
'default': 'bottom-left',
|
'default': 'bottom-left',
|
||||||
'options': {
|
'options':
|
||||||
|
{
|
||||||
'bottom-left': 'Bottom Left',
|
'bottom-left': 'Bottom Left',
|
||||||
'bottom-right': 'Bottom Right',
|
'bottom-right': 'Bottom Right',
|
||||||
'none': 'Disable',
|
'none': 'Disable',
|
||||||
},
|
},
|
||||||
}, {
|
},
|
||||||
'id': Preferences.SKIP_SPLASH_VIDEO,
|
[Preferences.SKIP_SPLASH_VIDEO]: {
|
||||||
'label': 'Skip Xbox splash video',
|
'label': 'Skip Xbox splash video',
|
||||||
'default': false,
|
'default': false,
|
||||||
}, {
|
},
|
||||||
'id': Preferences.HIDE_DOTS_ICON,
|
[Preferences.HIDE_DOTS_ICON]: {
|
||||||
'label': 'Hide Dots icon while playing',
|
'label': 'Hide Dots icon while playing',
|
||||||
'default': false,
|
'default': false,
|
||||||
}, {
|
},
|
||||||
'id': Preferences.REDUCE_ANIMATIONS,
|
[Preferences.REDUCE_ANIMATIONS]: {
|
||||||
'label': 'Reduce UI animations',
|
'label': 'Reduce UI animations',
|
||||||
'default': false,
|
'default': false,
|
||||||
}, {
|
},
|
||||||
'id': Preferences.BLOCK_SOCIAL_FEATURES,
|
[Preferences.BLOCK_SOCIAL_FEATURES]: {
|
||||||
'label': 'Disable social features',
|
'label': 'Disable social features',
|
||||||
'default': false,
|
'default': false,
|
||||||
}, {
|
},
|
||||||
'id': Preferences.BLOCK_TRACKING,
|
[Preferences.BLOCK_TRACKING]: {
|
||||||
'label': 'Disable xCloud analytics',
|
'label': 'Disable xCloud analytics',
|
||||||
'default': false,
|
'default': false,
|
||||||
}, {
|
},
|
||||||
'id': Preferences.VIDEO_FILL_FULL_SCREEN,
|
[Preferences.VIDEO_FILL_FULL_SCREEN]: {
|
||||||
'label': 'Stretch video to full screen',
|
'label': 'Stretch video to full screen',
|
||||||
'default': false,
|
'default': false,
|
||||||
'hidden': true,
|
'hidden': true,
|
||||||
}, {
|
},
|
||||||
'id': Preferences.VIDEO_SATURATION,
|
[Preferences.VIDEO_SATURATION]: {
|
||||||
'label': 'Video saturation (%)',
|
'label': 'Video saturation (%)',
|
||||||
'default': 100,
|
'default': 100,
|
||||||
'min': 0,
|
'min': 0,
|
||||||
'max': 150,
|
'max': 150,
|
||||||
'hidden': true,
|
'hidden': true,
|
||||||
}, {
|
},
|
||||||
'id': Preferences.VIDEO_CONTRAST,
|
[Preferences.VIDEO_CONTRAST]: {
|
||||||
'label': 'Video contrast (%)',
|
'label': 'Video contrast (%)',
|
||||||
'default': 100,
|
'default': 100,
|
||||||
'min': 0,
|
'min': 0,
|
||||||
'max': 150,
|
'max': 150,
|
||||||
'hidden': true,
|
'hidden': true,
|
||||||
}, {
|
},
|
||||||
'id': Preferences.VIDEO_BRIGHTNESS,
|
[Preferences.VIDEO_BRIGHTNESS]: {
|
||||||
'label': 'Video brightness (%)',
|
'label': 'Video brightness (%)',
|
||||||
'default': 100,
|
'default': 100,
|
||||||
'min': 0,
|
'min': 0,
|
||||||
'max': 150,
|
'max': 150,
|
||||||
'hidden': true,
|
'hidden': true,
|
||||||
},
|
},
|
||||||
]
|
[Preferences.STATS_SHOW_WHEN_PLAYING]: {
|
||||||
|
'default': false,
|
||||||
|
'hidden': true,
|
||||||
|
},
|
||||||
|
[Preferences.STATS_POSITION]: {
|
||||||
|
'default': 'top-left',
|
||||||
|
'options': {
|
||||||
|
'top-left': 'Top Left',
|
||||||
|
'top-center': 'Top Center',
|
||||||
|
'top-right': 'Top Right',
|
||||||
|
},
|
||||||
|
'hidden': true,
|
||||||
|
},
|
||||||
|
[Preferences.STATS_TEXT_SIZE]: {
|
||||||
|
'default': '0.9rem',
|
||||||
|
'options': {
|
||||||
|
'0.9rem': 'Small',
|
||||||
|
'1.0rem': 'Normal',
|
||||||
|
'1.1rem': 'Large',
|
||||||
|
},
|
||||||
|
'hidden': true,
|
||||||
|
},
|
||||||
|
[Preferences.STATS_TRANSPARENT]: {
|
||||||
|
'default': false,
|
||||||
|
'hidden': true,
|
||||||
|
},
|
||||||
|
[Preferences.STATS_OPACITY]: {
|
||||||
|
'default': 80,
|
||||||
|
'min': 50,
|
||||||
|
'max': 100,
|
||||||
|
'hidden': true,
|
||||||
|
},
|
||||||
|
[Preferences.STATS_CONDITIONAL_FORMATTING]: {
|
||||||
|
'default': false,
|
||||||
|
'hidden': true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this._storage = localStorage;
|
this._storage = localStorage;
|
||||||
@ -281,11 +427,12 @@ class Preferences {
|
|||||||
savedPrefs = JSON.parse(savedPrefs);
|
savedPrefs = JSON.parse(savedPrefs);
|
||||||
|
|
||||||
this._prefs = {};
|
this._prefs = {};
|
||||||
for (let setting of Preferences.SETTINGS) {
|
for (let settingId in Preferences.SETTINGS) {
|
||||||
if (setting.id in savedPrefs) {
|
const setting = Preferences.SETTINGS[settingId];
|
||||||
this._prefs[setting.id] = savedPrefs[setting.id];
|
if (settingId in savedPrefs) {
|
||||||
|
this._prefs[settingId] = savedPrefs[settingId];
|
||||||
} else {
|
} else {
|
||||||
this._prefs[setting.id] = setting.default;
|
this._prefs[settingId] = setting.default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -301,17 +448,26 @@ class Preferences {
|
|||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get default value
|
// Return default value
|
||||||
for (let setting of Preferences.SETTINGS) {
|
return Preferences.SETTINGS[key].default;
|
||||||
if (setting.id == key) {
|
|
||||||
return setting.default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
set(key, value) {
|
set(key, value) {
|
||||||
|
const config = Preferences.SETTINGS[key];
|
||||||
|
if (config) {
|
||||||
|
if ('min' in config) {
|
||||||
|
value = Math.max(config.min, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('max' in config) {
|
||||||
|
value = Math.min(config.max, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('options' in config && !(value in config.options)) {
|
||||||
|
value = config.default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this._prefs[key] = value;
|
this._prefs[key] = value;
|
||||||
this._update_storage();
|
this._update_storage();
|
||||||
}
|
}
|
||||||
@ -319,12 +475,77 @@ class Preferences {
|
|||||||
_update_storage() {
|
_update_storage() {
|
||||||
this._storage.setItem(this._key, JSON.stringify(this._prefs));
|
this._storage.setItem(this._key, JSON.stringify(this._prefs));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toElement(key, onChange) {
|
||||||
|
const CE = createElement;
|
||||||
|
const setting = Preferences.SETTINGS[key];
|
||||||
|
const currentValue = PREFS.get(key);
|
||||||
|
|
||||||
|
let $control;
|
||||||
|
if ('options' in setting) {
|
||||||
|
$control = CE('select', {id: 'xcloud_setting_' + key});
|
||||||
|
for (let value in setting.options) {
|
||||||
|
const label = setting.options[value];
|
||||||
|
|
||||||
|
const $option = CE('option', {value: value}, label);
|
||||||
|
$control.appendChild($option);
|
||||||
|
}
|
||||||
|
|
||||||
|
$control.value = currentValue;
|
||||||
|
$control.addEventListener('change', e => {
|
||||||
|
PREFS.set(key, e.target.value);
|
||||||
|
onChange && onChange(e);
|
||||||
|
});
|
||||||
|
} else if (typeof setting.default === 'number') {
|
||||||
|
$control = CE('input', {'type': 'number', 'min': setting.min, 'max': setting.max});
|
||||||
|
|
||||||
|
$control.value = currentValue;
|
||||||
|
$control.addEventListener('change', e => {
|
||||||
|
let value = Math.max(setting.min, Math.min(setting.max, parseInt(e.target.value)));
|
||||||
|
e.target.value = value;
|
||||||
|
|
||||||
|
PREFS.set(key, e.target.value);
|
||||||
|
onChange && onChange(e);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$control = CE('input', {'type': 'checkbox'});
|
||||||
|
$control.checked = currentValue;
|
||||||
|
|
||||||
|
$control.addEventListener('change', e => {
|
||||||
|
PREFS.set(key, e.target.checked);
|
||||||
|
onChange && onChange(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$control.id = `xcloud_setting_${key}`;
|
||||||
|
return $control;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const PREFS = new Preferences();
|
const PREFS = new Preferences();
|
||||||
|
|
||||||
|
|
||||||
|
function checkForUpdate() {
|
||||||
|
const CHECK_INTERVAL_SECONDS = 4 * 3600 * 1000; // check every 4 hours
|
||||||
|
const lastCheck = PREFS.get(Preferences.LAST_UPDATE_CHECK, 0);
|
||||||
|
const now = +new Date;
|
||||||
|
|
||||||
|
if (now - lastCheck < CHECK_INTERVAL_SECONDS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start checking
|
||||||
|
PREFS.set(Preferences.LAST_UPDATE_CHECK, now);
|
||||||
|
fetch('https://api.github.com/repos/redphx/better-xcloud/releases/latest')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(json => {
|
||||||
|
// Store the latest version
|
||||||
|
PREFS.set(Preferences.LATEST_VERSION, json.tag_name.substring(1));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function addCss() {
|
function addCss() {
|
||||||
let css = `
|
let css = `
|
||||||
.better_xcloud_settings_button {
|
.better_xcloud_settings_button {
|
||||||
@ -341,6 +562,10 @@ function addCss() {
|
|||||||
background-color: #515863;
|
background-color: #515863;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.better_xcloud_settings_button[data-update-available]::after {
|
||||||
|
content: ' 🌟';
|
||||||
|
}
|
||||||
|
|
||||||
.better_xcloud_settings {
|
.better_xcloud_settings {
|
||||||
background-color: #151515;
|
background-color: #151515;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@ -362,7 +587,11 @@ function addCss() {
|
|||||||
outline: none !important;
|
outline: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.better_xcloud_settings_wrapper a {
|
.better_xcloud_settings_wrapper .better_xcloud_settings_title_wrapper {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.better_xcloud_settings_wrapper a.better_xcloud_settings_title {
|
||||||
font-family: Bahnschrift, Arial, Helvetica, sans-serif;
|
font-family: Bahnschrift, Arial, Helvetica, sans-serif;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@ -370,18 +599,37 @@ function addCss() {
|
|||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
color: #5dc21e;
|
color: #5dc21e;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
.better_xcloud_settings_wrapper a:hover {
|
.better_xcloud_settings_wrapper a.better_xcloud_settings_title:hover {
|
||||||
color: #83f73a;
|
color: #83f73a;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.better_xcloud_settings_wrapper a:focus {
|
.better_xcloud_settings_wrapper a.better_xcloud_settings_title:focus {
|
||||||
color: #83f73a;
|
color: #83f73a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.better_xcloud_settings_wrapper a.better_xcloud_settings_update {
|
||||||
|
display: none;
|
||||||
|
color: #ff834b;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
.better_xcloud_settings_wrapper a.better_xcloud_settings_update:hover {
|
||||||
|
color: #ff9869;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.better_xcloud_settings_wrapper a.better_xcloud_settings_update:focus {
|
||||||
|
color: #ff9869;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
.better_xcloud_settings_wrapper .setting_row {
|
.better_xcloud_settings_wrapper .setting_row {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
@ -504,23 +752,42 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
|
|||||||
|
|
||||||
.better_xcloud_stats_bar {
|
.better_xcloud_stats_bar {
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
user-select: none;
|
||||||
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
opacity: 0.8;
|
|
||||||
background-color: #000;
|
background-color: #000;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-family: Consolas, "Courier New", Courier, monospace;
|
font-family: Consolas, "Courier New", Courier, monospace;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
|
text-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.better_xcloud_stats_bar[data-position=top-left] {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.better_xcloud_stats_bar[data-position=top-right] {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.better_xcloud_stats_bar[data-position=top-center] {
|
||||||
|
transform: translate(-50%, 0);
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.better_xcloud_stats_bar[data-transparent=true] {
|
||||||
|
background: none;
|
||||||
|
filter: drop-shadow(1px 0 0 #000) drop-shadow(-1px 0 0 #000) drop-shadow(0 1px 0 #000) drop-shadow(0 -1px 0 #000);
|
||||||
}
|
}
|
||||||
|
|
||||||
.better_xcloud_stats_bar label {
|
.better_xcloud_stats_bar label {
|
||||||
font-weight: bold;
|
|
||||||
margin: 0 8px 0 0;
|
margin: 0 8px 0 0;
|
||||||
font-size: 0.9rem;
|
font-family: Bahnschrift, Arial, Helvetica, sans-serif;
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: bold;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.better_xcloud_stats_bar span {
|
.better_xcloud_stats_bar span {
|
||||||
@ -530,6 +797,100 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
|
|||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
border-right: 2px solid #fff;
|
border-right: 2px solid #fff;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.better_xcloud_stats_bar span[data-grade=good] {
|
||||||
|
color: #6bffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.better_xcloud_stats_bar span[data-grade=ok] {
|
||||||
|
color: #fff16b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.better_xcloud_stats_bar span[data-grade=bad] {
|
||||||
|
color: #ff5f5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.better_xcloud_stats_bar span:first-of-type {
|
||||||
|
min-width: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.better_xcloud_stats_bar span:last-of-type {
|
||||||
|
border: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.better_xcloud_stats_settings {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
margin-right: -50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 420px;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
z-index: 1000;
|
||||||
|
background: #1a1b1e;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 16px;
|
||||||
|
font-family: "Segoe UI", Arial, Helvetica, sans-serif;
|
||||||
|
box-shadow: 0 0 6px #000;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.better_xcloud_stats_settings *:focus {
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.better_xcloud_stats_settings > b {
|
||||||
|
color: #fff;
|
||||||
|
display: block;
|
||||||
|
font-family: Bahnschrift, Arial, Helvetica, sans-serif;
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 32px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.better_xcloud_stats_settings > div {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.better_xcloud_stats_settings label {
|
||||||
|
flex: 1;
|
||||||
|
margin-bottom: 0;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.better_xcloud_stats_settings button {
|
||||||
|
padding: 8px 32px;
|
||||||
|
margin: 20px auto 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: block;
|
||||||
|
background-color: #2d3036;
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-family: Bahnschrift, Arial, Helvetica, sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
.better_xcloud_stats_settings button:hover {
|
||||||
|
background-color: #515863;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.better_xcloud_stats_settings button:focus {
|
||||||
|
background-color: #515863;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide UI elements */
|
/* Hide UI elements */
|
||||||
@ -841,48 +1202,66 @@ function injectSettingsButton($parent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CE = createElement;
|
const CE = createElement;
|
||||||
const preferredRegion = getPreferredServerRegion();
|
const PREF_PREFERRED_REGION = getPreferredServerRegion();
|
||||||
|
const PREF_LATEST_VERSION = PREFS.get(Preferences.LATEST_VERSION, null);
|
||||||
|
|
||||||
const $button = CE('button', {'class': 'better_xcloud_settings_button'}, preferredRegion);
|
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_settings_gone');
|
||||||
$settings.scrollIntoView();
|
$settings.scrollIntoView();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (PREF_LATEST_VERSION && PREF_LATEST_VERSION !== SCRIPT_VERSION) {
|
||||||
|
$button.setAttribute('data-update-available', true);
|
||||||
|
}
|
||||||
|
|
||||||
$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_settings_gone',
|
||||||
});
|
});
|
||||||
|
|
||||||
const $wrapper = CE('div', {
|
let $updateAvailable;
|
||||||
'class': 'better_xcloud_settings_wrapper',
|
const $wrapper = CE('div', {'class': 'better_xcloud_settings_wrapper'},
|
||||||
});
|
CE('div', {'class': 'better_xcloud_settings_title_wrapper'},
|
||||||
|
CE('a', {
|
||||||
|
'class': 'better_xcloud_settings_title',
|
||||||
|
'href': SCRIPT_HOME,
|
||||||
|
'target': '_blank',
|
||||||
|
}, 'Better xCloud ' + SCRIPT_VERSION),
|
||||||
|
$updateAvailable = CE('a', {
|
||||||
|
'class': 'better_xcloud_settings_update',
|
||||||
|
'href': 'https://github.com/redphx/better-xcloud/releases',
|
||||||
|
'target': '_blank',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
$container.appendChild($wrapper);
|
$container.appendChild($wrapper);
|
||||||
|
|
||||||
const $title = CE('a', {
|
if (PREF_LATEST_VERSION && PREF_LATEST_VERSION != SCRIPT_VERSION) {
|
||||||
href: SCRIPT_HOME,
|
$updateAvailable.textContent = `🌟 Version ${PREF_LATEST_VERSION} available`;
|
||||||
target: '_blank',
|
$updateAvailable.style.display = 'block';
|
||||||
}, 'Better xCloud ' + SCRIPT_VERSION);
|
}
|
||||||
$wrapper.appendChild($title);
|
|
||||||
|
|
||||||
for (let setting of Preferences.SETTINGS) {
|
for (let settingId in Preferences.SETTINGS) {
|
||||||
|
const setting = Preferences.SETTINGS[settingId];
|
||||||
if (setting.hidden) {
|
if (setting.hidden) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let $control;
|
let $control;
|
||||||
let labelAttrs = {};
|
let labelAttrs = {};
|
||||||
if (setting.id === Preferences.SERVER_REGION || setting.options) {
|
if (settingId === Preferences.SERVER_REGION || setting.options) {
|
||||||
let selectedValue;
|
let selectedValue;
|
||||||
|
|
||||||
$control = CE('select', {id: 'xcloud_setting_' + setting.id});
|
$control = CE('select', {id: 'xcloud_setting_' + settingId});
|
||||||
$control.addEventListener('change', e => {
|
$control.addEventListener('change', e => {
|
||||||
PREFS.set(setting.id, e.target.value);
|
PREFS.set(settingId, e.target.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (setting.id === Preferences.SERVER_REGION) {
|
if (settingId === Preferences.SERVER_REGION) {
|
||||||
selectedValue = preferredRegion;
|
selectedValue = PREF_PREFERRED_REGION;
|
||||||
setting.options = {};
|
setting.options = {};
|
||||||
for (let regionName in SERVER_REGIONS) {
|
for (let regionName in SERVER_REGIONS) {
|
||||||
const region = SERVER_REGIONS[regionName];
|
const region = SERVER_REGIONS[regionName];
|
||||||
@ -897,7 +1276,7 @@ function injectSettingsButton($parent) {
|
|||||||
setting.options[value] = label;
|
setting.options[value] = label;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
selectedValue = PREFS.get(setting.id);
|
selectedValue = PREFS.get(settingId);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let value in setting.options) {
|
for (let value in setting.options) {
|
||||||
@ -910,21 +1289,21 @@ function injectSettingsButton($parent) {
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
$control = CE('input', {
|
$control = CE('input', {
|
||||||
id: 'xcloud_setting_' + setting.id,
|
id: 'xcloud_setting_' + settingId,
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
'data-key': setting.id,
|
'data-key': settingId,
|
||||||
});
|
});
|
||||||
|
|
||||||
$control.addEventListener('change', e => {
|
$control.addEventListener('change', e => {
|
||||||
PREFS.set(e.target.getAttribute('data-key'), e.target.checked);
|
PREFS.set(e.target.getAttribute('data-key'), e.target.checked);
|
||||||
});
|
});
|
||||||
|
|
||||||
setting.value = PREFS.get(setting.id);
|
setting.value = PREFS.get(settingId);
|
||||||
$control.checked = setting.value;
|
$control.checked = setting.value;
|
||||||
|
|
||||||
labelAttrs = {'for': 'xcloud_setting_' + setting.id, 'tabindex': 0};
|
labelAttrs = {'for': 'xcloud_setting_' + settingId, 'tabindex': 0};
|
||||||
|
|
||||||
if (setting.id === Preferences.USE_DESKTOP_CODEC && !hasRtcSetCodecPreferencesSupport()) {
|
if (settingId === Preferences.USE_DESKTOP_CODEC && !hasRtcSetCodecPreferencesSupport()) {
|
||||||
$control.checked = false;
|
$control.checked = false;
|
||||||
$control.disabled = true;
|
$control.disabled = true;
|
||||||
$control.title = 'Your browser doesn\'t support this feature';
|
$control.title = 'Your browser doesn\'t support this feature';
|
||||||
@ -1156,7 +1535,7 @@ function patchVideoApi() {
|
|||||||
$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};
|
||||||
|
|
||||||
const stats = STREAM_WEBRTC.getStats().then(stats => {
|
STREAM_WEBRTC.getStats().then(stats => {
|
||||||
stats.forEach(stat => {
|
stats.forEach(stat => {
|
||||||
if (stat.type !== 'codec') {
|
if (stat.type !== 'codec') {
|
||||||
return;
|
return;
|
||||||
@ -1181,6 +1560,10 @@ function patchVideoApi() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (PREFS.get(Preferences.STATS_SHOW_WHEN_PLAYING)) {
|
||||||
|
StreamStats.start();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (PREF_SCREENSHOT_BUTTON_POSITION !== 'none') {
|
if (PREF_SCREENSHOT_BUTTON_POSITION !== 'none') {
|
||||||
@ -1237,7 +1620,7 @@ function patchRtcCodecs() {
|
|||||||
const newCodecs = codecs.slice();
|
const newCodecs = codecs.slice();
|
||||||
let pos = 0;
|
let pos = 0;
|
||||||
newCodecs.forEach((codec, i) => {
|
newCodecs.forEach((codec, i) => {
|
||||||
// Find high quality codecs
|
// Find high-quality codecs
|
||||||
if (codec.sdpFmtpLine && codec.sdpFmtpLine.includes('profile-level-id=4d')) {
|
if (codec.sdpFmtpLine && codec.sdpFmtpLine.includes('profile-level-id=4d')) {
|
||||||
// Move it to the top of the array
|
// Move it to the top of the array
|
||||||
newCodecs.splice(i, 1);
|
newCodecs.splice(i, 1);
|
||||||
@ -1522,6 +1905,7 @@ function hideUiOnPageChange() {
|
|||||||
STREAM_WEBRTC = null;
|
STREAM_WEBRTC = null;
|
||||||
$STREAM_VIDEO = null;
|
$STREAM_VIDEO = null;
|
||||||
StreamStats.stop();
|
StreamStats.stop();
|
||||||
|
StreamStats.hideSettingsUi();
|
||||||
document.querySelector('.better_xcloud_screenshot_button').style = '';
|
document.querySelector('.better_xcloud_screenshot_button').style = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1543,6 +1927,10 @@ if (PREFS.get(Preferences.DISABLE_BANDWIDTH_CHECKING)) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for Update
|
||||||
|
checkForUpdate();
|
||||||
|
|
||||||
|
// Monkey patches
|
||||||
patchRtcCodecs();
|
patchRtcCodecs();
|
||||||
interceptHttpRequests();
|
interceptHttpRequests();
|
||||||
patchVideoApi();
|
patchVideoApi();
|
||||||
|
Reference in New Issue
Block a user