Compare commits

...

17 Commits
v1.5 ... v1.6.1

Author SHA1 Message Date
b3b7a51979 Bump version to 1.6.1 2023-07-25 09:01:04 +07:00
831ccb31c1 Stop stretching stream stats' background to full screen 2023-07-25 09:00:48 +07:00
d87ac78e57 Add "Check for update" feature (#21) 2023-07-25 08:58:42 +07:00
67b419c37d Update README.md 2023-07-25 07:57:16 +07:00
a7b796362a Update README.md 2023-07-25 07:12:01 +07:00
889ee890f1 Update README.md 2023-07-24 11:38:29 +07:00
71a48f8afb Update README.md 2023-07-24 08:00:56 +07:00
354ecac97e Update README.md 2023-07-23 20:22:39 +07:00
45a7c28d3f Update README.md 2023-07-23 17:53:56 +07:00
e43c34ed3a Bump version to 1.6 2023-07-23 17:45:27 +07:00
28a2e32fc5 Update README.md 2023-07-23 17:44:50 +07:00
b564de249a Show stream stats (#16)
* Get video and audio info from RTCPeerConnection.getStats()

* Show stream's stats bar

* Make Stream menu icon's size smaller

* Add shadow to stream badges

* Show bitrate

* Add button to toggle Stream Stats

* Rename StreamStatus to StreamBadges

* Show '???' then currentRoundTripTime is undefined

* Remove work-around for browsers with no setCodecPreferences() support as it's not working

* Remove updateVideoPlayerPreview()

* Disable USE_DESKTOP_CODEC setting on unsupported browsers
2023-07-23 17:09:30 +07:00
9fa073da82 Update README.md 2023-07-22 15:41:14 +07:00
958f5410f4 Update README.md 2023-07-22 15:20:45 +07:00
15d3efdf4e Update README.md 2023-07-22 15:20:35 +07:00
645b49751d Update README.md 2023-07-22 15:15:16 +07:00
53913ae218 Update README.md 2023-07-22 12:23:31 +07:00
2 changed files with 410 additions and 190 deletions

View File

@ -6,9 +6,13 @@ Give this project a 🌟 if you like it. Thank you 🙏.
## 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="Video Settings UI" src="https://github.com/redphx/better-xcloud/assets/96280/ed219d50-02ab-40bd-95c5-a010956d77bf">
<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">
&nbsp;
**Demo video:** https://youtu.be/oDr5Eddp55E
- **Switch region of streaming server**
> Connect to another server instead of the default one. Check the [**FAQ** section](#faq) for some notes.
@ -20,16 +24,17 @@ Give this project a 🌟 if you like it. Thank you 🙏.
- **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.
> You should enable this feature even if you're on desktop.
> Not available for some browsers (Firefox, Safari...). Use the [changing User-Agent method](https://github.com/redphx/better-xcloud/wiki/UserAgent) instead.
> Use more bandwidth & battery.
> Comparison video with the setting ON & OFF: https://youtu.be/-9PuBJJSgR4
- **Prefer IPv6 streaming server**
> Might reduce latency
- **Disable bandwidth checking**
> xCloud won't reduce quality when the internet speed is slow
> Might reduce latency.
- **Disable bandwidth checking**
> 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**
> Save 3 seconds
> Save 3 seconds.
- **Hide Dots icon while playing**
> You can still click on it, but it doesn't block the screen anymore
- **Reduce UI animations**
@ -37,16 +42,18 @@ Give this project a 🌟 if you like it. Thank you 🙏.
- **Stretch video to full sctreen**
> Useful when you don't have a 16:9 screen
- **Adjust video filters**
> Brightness/Contrast/Saturation
> Brightness/Contrast/Saturation.
- **Display stream's statuses**
> Region/Server/Quality/Dimension...
> Region/Server/Quality/Resolution...
- **Disable social features**
> 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**
> 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**
- **🔥 Show stream stats**
> Check [Stream stats section](#stream-stats) for more info.
<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.
@ -83,67 +90,72 @@ 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).
- **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
![image](https://github.com/redphx/better-xcloud/assets/96280/dc0f4f36-8a69-4ec1-aadd-cfda2d701991)
- While playing > `...` > `Stream Stats`.
- This bar is updated every second.
| Abbr. | Full name | Explain |
|------:|:-------------------|:-----------------------------------------------------------------------------------------------------------------------------------|
| FPS | Frames per Seconds | The number of decoded frames 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) |
| BR | Bitrate | The amount of data the server sent to your device in the last second |
| 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 |
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`
- Firefox: `about:webrtc`
📝 Having this info on all the time might reduce your enjoyment, so I'd recommend only using it when having network problems.
## Capture screenshot
- This feature is only available in **Better xCloud**.
- Works on both desktop & mobile, but it's designed for mobile users.
- 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.
- Works on both desktop & mobile, but it was designed for mobile users.
- It's client-side only.
- 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 doesn't touch UI, notification bar... only the gameplay.
- Screenshot doesn't include touch UI, notification bar... only the gameplay.
- There might be a slight delay.
- ⚠️ It's not possible to map the Share/Screenshot button on your controller to this feature.
### How to capture screenshot
1. Enable this feature in setting.
1. Enable this feature in the Settings.
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.
5. Screenshot will be saved by browser.
6. You can double tap that corner to capture screenshot.
5. Screenshot will be saved by the browser.
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">
## FAQ
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).
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.
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?**
No. "Clarity Boost" feature uses an exclusive API (`Video.msVideoProcessing`) that's only available on Edge browser for desktop at the moment.
5. **Will you be able to enable the "Clarity Boost" feature on non-Edge browsers?**
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
You're no longer needed to change User-Agent since you can just use the **Force high quality stream** setting.
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.
Moved to [wiki](https://github.com/redphx/better-xcloud/wiki/UserAgent).
## Acknowledgements
- [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)
## 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.

View File

@ -1,7 +1,7 @@
// ==UserScript==
// @name Better xCloud
// @namespace https://github.com/redphx
// @version 1.5
// @version 1.6.1
// @description Improve Xbox Cloud Gaming (xCloud) experience
// @author redphx
// @license MIT
@ -13,18 +13,21 @@
// ==/UserScript==
'use strict';
const SCRIPT_VERSION = '1.5';
const SCRIPT_VERSION = '1.6.1';
const SCRIPT_HOME = 'https://github.com/redphx/better-xcloud';
const SERVER_REGIONS = {};
var STREAM_WEBRTC;
var $STREAM_VIDEO;
var $SCREENSHOT_CANVAS;
var GAME_TITLE_ID;
class StreamStatus {
class StreamBadges {
static ipv6 = false;
static resolution = {width: 0, height: 0};
static hqCodec = false;
static resolution = null;
static video = null;
static audio = null;
static fps = 0;
static region = '';
static #renderBadge(name, value, color) {
@ -37,22 +40,142 @@ class StreamStatus {
}
static render() {
let video;
if (StreamBadges.video) {
video = StreamBadges.video.codec;
if (StreamBadges.video.profile) {
let profile = StreamBadges.video.profile;
profile = profile.startsWith('4d') ? 'High' : (profile.startsWith('42') ? 'Normal' : profile);
video += ` (${profile})`;
}
}
let audio;
if (StreamBadges.audio) {
audio = StreamBadges.audio.codec;
const bitrate = StreamBadges.audio.bitrate / 1000;
audio += ` (${bitrate} kHz)`;
}
const BADGES = [
['region', StreamStatus.region, '#d7450b'],
['server', StreamStatus.ipv6 ? 'IPv6' : 'IPv4', '#008746'],
['quality', StreamStatus.hqCodec ? 'High' : 'Normal', '#007c8f'],
['resolution', `${StreamStatus.resolution.width}x${StreamStatus.resolution.height}`, '#ff3977'],
['region', StreamBadges.region, '#d7450b'],
['server', StreamBadges.ipv6 ? 'IPv6' : 'IPv4', '#008746'],
video ? ['video', video, '#007c8f'] : null,
audio ? ['audio', audio, '#007c8f'] : null,
StreamBadges.resolution && ['resolution', `${StreamBadges.resolution.width}x${StreamBadges.resolution.height}`, '#ff3977'],
];
const $wrapper = createElement('div', {'class': 'better_xcloud_badges'});
BADGES.forEach(item => $wrapper.appendChild(StreamStatus.#renderBadge(...item)));
BADGES.forEach(item => item && $wrapper.appendChild(StreamBadges.#renderBadge(...item)));
return $wrapper;
}
}
class StreamStats {
static #timeout;
static #updateInterval = 1000;
static #$container;
static #$fps;
static #$rtt;
static #$pl;
static #$fl;
static #$br;
static #lastInbound;
static start() {
StreamStats.#$container.style.display = 'block';
StreamStats.update();
}
static stop() {
StreamStats.#$container.style.display = 'none';
clearTimeout(StreamStats.#timeout);
StreamStats.#timeout = null;
StreamStats.#lastInbound = null;
}
static toggle() {
StreamStats.#isHidden() ? StreamStats.start() : StreamStats.stop();
}
static #isHidden = () => StreamStats.#$container.style.display === 'none';
static update() {
if (StreamStats.#isHidden()) {
return;
}
if (!STREAM_WEBRTC) {
StreamStats.#timeout = setTimeout(StreamStats.update, StreamStats.#updateInterval);
return;
}
STREAM_WEBRTC.getStats().then(stats => {
stats.forEach(stat => {
if (stat.type === 'inbound-rtp' && stat.kind === 'video') {
// FPS
StreamStats.#$fps.textContent = stat.framesPerSecond || 0;
// Packets Loss
const packetsLost = stat.packetsLost;
const packetsReceived = stat.packetsReceived || 1;
StreamStats.#$pl.textContent = `${packetsLost} (${(packetsLost * 100 / packetsReceived).toFixed(2)}%)`;
// Frames Dropped
const framesDropped = stat.framesDropped;
const framesReceived = stat.framesReceived || 1;
StreamStats.#$fl.textContent = `${framesDropped} (${(framesDropped * 100 / framesReceived).toFixed(2)}%)`;
// Bitrate
if (StreamStats.#lastInbound) {
const timeDiff = stat.timestamp - StreamStats.#lastInbound.timestamp;
const bitrate = 8 * (stat.bytesReceived - StreamStats.#lastInbound.bytesReceived) / timeDiff / 1000;
StreamStats.#$br.textContent = `${bitrate.toFixed(2)} Mbps`;
}
StreamStats.#lastInbound = stat;
} else if (stat.type === 'candidate-pair' && stat.state === 'succeeded') {
// Round Trip Time
const roundTripTime = typeof stat.currentRoundTripTime !== 'undefined' ? stat.currentRoundTripTime * 1000 : '???';
StreamStats.#$rtt.textContent = `${roundTripTime}ms`;
}
});
StreamStats.#timeout = setTimeout(StreamStats.update, StreamStats.#updateInterval);
});
}
static render() {
if (StreamStats.#$container) {
return;
}
const CE = createElement;
StreamStats.#$container = CE('div', {'class': 'better_xcloud_stats_bar'},
CE('label', {}, 'FPS'),
StreamStats.#$fps = CE('span', {}, 0),
CE('label', {}, 'RTT'),
StreamStats.#$rtt = CE('span', {}, '0ms'),
CE('label', {}, 'BR'),
StreamStats.#$br = CE('span', {}, '0 Mbps'),
CE('label', {}, 'PL'),
StreamStats.#$pl = CE('span', {}, '0 (0.00%)'),
CE('label', {}, 'FL'),
StreamStats.#$fl = CE('span', {}, '0 (0.00%)'));
document.documentElement.appendChild(StreamStats.#$container);
}
}
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 PREFER_IPV6_SERVER() { return 'prefer_ipv6_server'; }
static get FORCE_1080P_STREAM() { return 'force_1080p_stream'; }
@ -205,6 +328,26 @@ class 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() {
let css = `
.better_xcloud_settings_button {
@ -221,6 +364,10 @@ function addCss() {
background-color: #515863;
}
.better_xcloud_settings_button[data-update-available]::after {
content: ' 🌟';
}
.better_xcloud_settings {
background-color: #151515;
user-select: none;
@ -242,7 +389,11 @@ function addCss() {
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-size: 20px;
text-decoration: none;
@ -250,18 +401,37 @@ function addCss() {
display: block;
margin-bottom: 8px;
color: #5dc21e;
flex: 1;
}
@media (hover: hover) {
.better_xcloud_settings_wrapper a:hover {
.better_xcloud_settings_wrapper a.better_xcloud_settings_title:hover {
color: #83f73a;
}
}
.better_xcloud_settings_wrapper a:focus {
.better_xcloud_settings_wrapper a.better_xcloud_settings_title:focus {
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 {
display: flex;
margin-bottom: 8px;
@ -319,7 +489,7 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
.better_xcloud_badges {
position: absolute;
bottom: -35px;
top: 155px;
margin-left: 0px;
user-select: none;
}
@ -331,7 +501,9 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
color: #fff;
font-family: Bahnschrift Semibold, Arial, Helvetica, sans-serif;
font-weight: 400;
margin-right: 8px;
margin: 0 8px 8px 0;
box-shadow: 0px 0px 6px #000;
border-radius: 4px;
}
.better_xcloud_badge .better_xcloud_badge_name {
@ -380,6 +552,35 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
display: none;
}
.better_xcloud_stats_bar {
display: none;
position: absolute;
top: 0;
left: 0;
opacity: 0.8;
background-color: #000;
color: #fff;
font-family: Consolas, "Courier New", Courier, monospace;
font-size: 0.9rem;
padding-left: 8px;
z-index: 1000;
}
.better_xcloud_stats_bar label {
font-weight: bold;
margin: 0 8px 0 0;
font-size: 0.9rem;
}
.better_xcloud_stats_bar span {
min-width: 60px;
display: inline-block;
text-align: right;
padding-right: 8px;
margin-right: 8px;
border-right: 2px solid #fff;
}
/* Hide UI elements */
#headerArea, #uhfSkipToMain, .uhf-footer {
display: none;
@ -396,6 +597,12 @@ div[class*=NotFocusedDialog] {
#game-stream video {
visibility: hidden;
}
/* Adjust Stream menu icon's size */
button[class*=MenuItem-module__container] {
min-width: auto !important;
width: 110px !important;
}
`;
// Reduce animations
@ -541,7 +748,6 @@ function interceptHttpRequests() {
const PREF_PREFER_IPV6_SERVER = PREFS.get(Preferences.PREFER_IPV6_SERVER);
const PREF_FORCE_1080P_STREAM = PREFS.get(Preferences.FORCE_1080P_STREAM);
const PREF_USE_DESKTOP_CODEC = PREFS.get(Preferences.USE_DESKTOP_CODEC);
const HAS_CODECS_API_SUPPORT = hasRtcSetCodecPreferencesSupport();
const orgFetch = window.fetch;
window.fetch = async (...arg) => {
@ -586,11 +792,11 @@ function interceptHttpRequests() {
if (url.endsWith('/sessions/cloud/play')) {
const parsedUrl = new URL(url);
StreamStatus.region = parsedUrl.host.split('.', 1)[0];
StreamBadges.region = parsedUrl.host.split('.', 1)[0];
for (let regionName in SERVER_REGIONS) {
const region = SERVER_REGIONS[regionName];
if (parsedUrl.origin == region.baseUri) {
StreamStatus.region = regionName;
StreamBadges.region = regionName;
break;
}
}
@ -612,27 +818,6 @@ function interceptHttpRequests() {
return orgFetch(...arg);
}
// Work-around for browsers with no setCodecPreferences() support
if (PREF_USE_DESKTOP_CODEC && !HAS_CODECS_API_SUPPORT && url.endsWith('/sdp') && url.includes('/sessions/cloud/') && request.method === 'GET') {
const promise = orgFetch(...arg);
return promise.then(response => {
return response.clone().text().then(text => {
if (!text.length) {
return response;
}
const obj = JSON.parse(text);
obj.exchangeResponse = obj.exchangeResponse.replaceAll('profile-level-id=42', 'profile-level-id=4d');
response.json = () => Promise.resolve(obj);
response.text = () => Promise.resolve(JSON.stringify(obj));
return response;
});
});
}
// ICE server candidates
if (PREF_PREFER_IPV6_SERVER && url.endsWith('/ice') && url.includes('/sessions/cloud/') && request.method === 'GET') {
const promise = orgFetch(...arg);
@ -690,7 +875,7 @@ function createElement(elmName, props = {}) {
if (argType == 'string' || argType == 'number') {
$elm.innerText = arg;
} else {
} else if (arg) {
$elm.appendChild(arg);
}
}
@ -705,30 +890,47 @@ function injectSettingsButton($parent) {
}
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 => {
const $settings = document.querySelector('.better_xcloud_settings');
$settings.classList.toggle('better_xcloud_settings_gone');
$settings.scrollIntoView();
});
if (PREF_LATEST_VERSION && PREF_LATEST_VERSION !== SCRIPT_VERSION) {
$button.setAttribute('data-update-available', true);
}
$parent.appendChild($button);
const $container = CE('div', {
'class': 'better_xcloud_settings better_xcloud_settings_gone',
});
const $wrapper = CE('div', {
'class': 'better_xcloud_settings_wrapper',
});
let $updateAvailable;
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);
const $title = CE('a', {
href: SCRIPT_HOME,
target: '_blank',
}, 'Better xCloud ' + SCRIPT_VERSION);
$wrapper.appendChild($title);
if (PREF_LATEST_VERSION && PREF_LATEST_VERSION != SCRIPT_VERSION) {
$updateAvailable.textContent = `🌟 Version ${PREF_LATEST_VERSION} available`;
$updateAvailable.style.display = 'block';
}
for (let setting of Preferences.SETTINGS) {
if (setting.hidden) {
@ -746,7 +948,7 @@ function injectSettingsButton($parent) {
});
if (setting.id === Preferences.SERVER_REGION) {
selectedValue = preferredRegion;
selectedValue = PREF_PREFERRED_REGION;
setting.options = {};
for (let regionName in SERVER_REGIONS) {
const region = SERVER_REGIONS[regionName];
@ -781,16 +983,19 @@ function injectSettingsButton($parent) {
$control.addEventListener('change', e => {
PREFS.set(e.target.getAttribute('data-key'), e.target.checked);
if (setting.id == Preferences.VIDEO_FILL_FULL_SCREEN) {
updateVideoPlayerPreview();
}
});
setting.value = PREFS.get(setting.id);
$control.checked = setting.value;
labelAttrs = {'for': 'xcloud_setting_' + setting.id, 'tabindex': 0};
if (setting.id === Preferences.USE_DESKTOP_CODEC && !hasRtcSetCodecPreferencesSupport()) {
$control.checked = false;
$control.disabled = true;
$control.title = 'Your browser doesn\'t support this feature';
$control.style.cursor = 'help';
}
}
const $elm = CE('div', {'class': 'setting_row'},
@ -856,24 +1061,6 @@ function updateVideoPlayerCss() {
}
function updateVideoPlayerPreview() {
const $screen = document.querySelector('.better_xcloud_settings_preview_screen');
$screen.style.display = 'block';
const filters = getVideoPlayerFilterStyle();
const $video = document.querySelector('.better_xcloud_settings_preview_video');
$video.style.filter = filters;
if (PREFS.get(Preferences.VIDEO_FILL_FULL_SCREEN)) {
$video.style.height = 'auto';
} else {
$video.style.height = '100%';
}
updateVideoPlayerCss();
}
function checkHeader() {
const $button = document.querySelector('#PageContent header .better_xcloud_settings_button');
@ -904,6 +1091,19 @@ function watchHeader() {
}
function cloneStreamMenuButton($orgButton, label, svg_icon) {
const $button = $orgButton.cloneNode(true);
$button.setAttribute('aria-label', label);
$button.querySelector('div[class*=label]').textContent = label;
const $svg = $button.querySelector('svg');
$svg.innerHTML = svg_icon;
$svg.setAttribute('viewBox', '0 0 24 24');
return $button;
}
function injectVideoSettingsButton() {
const $screen = document.querySelector('#PageContent section[class*=PureScreens]');
if (!$screen) {
@ -945,29 +1145,16 @@ function injectVideoSettingsButton() {
return;
}
const id = 'better-xcloud-video-settings-btn';
let $wrapper = document.getElementById('#' + id);
if ($wrapper) {
return;
}
const $orgButton = node.querySelector('div > div > button');
if (!$orgButton) {
return;
}
// Clone other button
const $button = $orgButton.cloneNode(true);
$button.setAttribute('aria-label', 'Video settings');
$button.querySelector('div[class*=label]').textContent = 'Video settings';
// Credit: https://www.iconfinder.com/iconsets/user-interface-outline-27
const SVG_ICON = '<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 $svg = $button.querySelector('svg');
$svg.innerHTML = SVG_ICON;
$svg.setAttribute('viewBox', '0 0 24 24');
$button.addEventListener('click', e => {
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
const $btnVideoSettings = cloneStreamMenuButton($orgButton, 'Video settings', ICON_VIDEO_SETTINGS);
$btnVideoSettings.addEventListener('click', e => {
e.preventDefault();
e.stopPropagation();
@ -978,23 +1165,38 @@ function injectVideoSettingsButton() {
$parent.addEventListener('touchend', hideQuickBarFunc);
const $touchSurface = document.querySelector('#MultiTouchSurface');
if ($touchSurface) {
$touchSurface.addEventListener('touchstart', hideQuickBarFunc);
}
$touchSurface && $touchSurface.addEventListener('touchstart', hideQuickBarFunc);
});
$orgButton.parentElement.insertBefore($button, $orgButton.parentElement.firstChild);
// Add button at the beginning
$orgButton.parentElement.insertBefore($btnVideoSettings, $orgButton.parentElement.firstChild);
// Hide Quick bar when closing HUD
document.querySelector('button[class*=StreamMenu-module__backButton]').addEventListener('click', e => {
const $btnCloseHud = document.querySelector('button[class*=StreamMenu-module__backButton]');
$btnCloseHud.addEventListener('click', e => {
$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
const $btnStreamStats = cloneStreamMenuButton($orgButton, 'Stream stats', ICON_STREAM_STATS);
$btnStreamStats.addEventListener('click', e => {
e.preventDefault();
e.stopPropagation();
// Close HUD
$btnCloseHud.click();
// Toggle Stream Stats
StreamStats.toggle();
});
// Insert after Video Settings button
$orgButton.parentElement.insertBefore($btnStreamStats, $btnVideoSettings.nextSibling);
// Render stream badges
const $menu = document.querySelector('div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module]');
$menu.appendChild(StreamStatus.render());
$menu.appendChild(StreamBadges.render());
});
});
});
observer.observe($screen, {subtree: true, childList: true});
@ -1011,25 +1213,54 @@ function patchVideoApi() {
this.style.visibility = 'visible';
this.removeEventListener('playing', showFunc);
if (this.videoWidth) {
$STREAM_VIDEO = this;
$SCREENSHOT_CANVAS.width = this.videoWidth;
$SCREENSHOT_CANVAS.height = this.videoHeight;
StreamStatus.resolution = {width: this.videoWidth, height: this.videoHeight};
if (PREF_SCREENSHOT_BUTTON_POSITION !== 'none') {
const $btn = document.querySelector('.better_xcloud_screenshot_button');
$btn.style.display = 'block';
if (PREF_SCREENSHOT_BUTTON_POSITION === 'bottom-right') {
$btn.style.right = '0';
} else {
$btn.style.left = '0';
}
}
GAME_TITLE_ID = /\/launch\/([^/]+)/.exec(window.location.pathname)[1];
if (!this.videoWidth) {
return;
}
$STREAM_VIDEO = this;
$SCREENSHOT_CANVAS.width = this.videoWidth;
$SCREENSHOT_CANVAS.height = this.videoHeight;
StreamBadges.resolution = {width: this.videoWidth, height: this.videoHeight};
const stats = STREAM_WEBRTC.getStats().then(stats => {
stats.forEach(stat => {
if (stat.type !== 'codec') {
return;
}
const mimeType = stat.mimeType.split('/');
if (mimeType[0] === 'video') {
const video = {
codec: mimeType[1],
};
if (video.codec === 'H264') {
const match = /profile-level-id=([0-9a-f]{6})/.exec(stat.sdpFmtpLine);
video.profile = match ? match[1] : null;
}
StreamBadges.video = video;
} else if (!StreamBadges.audio && mimeType[0] === 'audio') {
StreamBadges.audio = {
codec: mimeType[1],
bitrate: stat.clockRate,
};
}
});
});
if (PREF_SCREENSHOT_BUTTON_POSITION !== 'none') {
const $btn = document.querySelector('.better_xcloud_screenshot_button');
$btn.style.display = 'block';
if (PREF_SCREENSHOT_BUTTON_POSITION === 'bottom-right') {
$btn.style.right = '0';
} else {
$btn.style.left = '0';
}
}
GAME_TITLE_ID = /\/launch\/([^/]+)/.exec(window.location.pathname)[1];
}
HTMLMediaElement.prototype.orgPlay = HTMLMediaElement.prototype.play;
@ -1354,7 +1585,9 @@ function hideUiOnPageChange() {
$quickBar.style.display = 'none';
}
STREAM_WEBRTC = null;
$STREAM_VIDEO = null;
StreamStats.stop();
document.querySelector('.better_xcloud_screenshot_button').style = '';
}
@ -1376,6 +1609,10 @@ if (PREFS.get(Preferences.DISABLE_BANDWIDTH_CHECKING)) {
});
}
// Check for Update
checkForUpdate();
// Monkey patches
patchRtcCodecs();
interceptHttpRequests();
patchVideoApi();
@ -1385,6 +1622,7 @@ addCss();
updateVideoPlayerCss();
setupVideoSettingsBar();
setupScreenshotButton();
StreamStats.render();
// Workaround for Hermit browser
var onLoadTriggered = false;
@ -1396,43 +1634,13 @@ if (document.readyState === 'complete' && !onLoadTriggered) {
watchHeader();
}
RTCPeerConnection.prototype.orgSetRemoteDescription = RTCPeerConnection.prototype.setRemoteDescription;
RTCPeerConnection.prototype.setRemoteDescription = function(...args) {
StreamStatus.hqCodec = false;
const sdpDesc = args[0];
if (sdpDesc.sdp) {
const sdp = sdpDesc.sdp;
let lineIndex = 0;
let endPos = 0;
let line;
while (lineIndex > -1) {
lineIndex = sdp.indexOf('a=fmtp:', endPos);
if (lineIndex === -1) {
break;
}
endPos = sdp.indexOf('\n', lineIndex);
line = sdp.substring(lineIndex, endPos);
if (line.includes('profile-level-id')) {
StreamStatus.hqCodec = line.includes('profile-level-id=4d');
break;
}
}
}
return this.orgSetRemoteDescription.apply(this, args);
}
RTCPeerConnection.prototype.orgAddIceCandidate = RTCPeerConnection.prototype.addIceCandidate;
RTCPeerConnection.prototype.addIceCandidate = function(...args) {
const candidate = args[0].candidate;
if (candidate && candidate.startsWith('a=candidate:1 ')) {
StreamStatus.ipv6 = candidate.substring(20).includes(':');
StreamBadges.ipv6 = candidate.substring(20).includes(':');
}
STREAM_WEBRTC = this;
return this.orgAddIceCandidate.apply(this, args);
}