mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-07-05 05:41:43 +02:00
Compare commits
19 Commits
Author | SHA1 | Date | |
---|---|---|---|
e43c34ed3a | |||
28a2e32fc5 | |||
b564de249a | |||
9fa073da82 | |||
958f5410f4 | |||
15d3efdf4e | |||
645b49751d | |||
53913ae218 | |||
2569e73018 | |||
4c790ac38d | |||
11c233e14e | |||
a81cb86140 | |||
c3de245545 | |||
a7ab506f0f | |||
8ecff6adae | |||
27ec45512d | |||
3a654b99cb | |||
a009cca866 | |||
19302ea444 |
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: "[Bug]"
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Platform (please complete the following information):**
|
||||||
|
- OS: [e.g. Android]
|
||||||
|
- Browser: [e.g. chrome, firefox]
|
||||||
|
- Browser Version: [e.g. 100]
|
||||||
|
- Better xCloud Version: [e.g. 1.4]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: "[Feature]"
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
83
README.md
83
README.md
@ -6,12 +6,15 @@ Give this project a 🌟 if you like it. Thank you 🙏.
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
<img width="500" alt="Settings UI" src="https://github.com/redphx/better-xcloud/assets/96280/8fb9f0ac-85f5-4e5a-9570-5a5e119e4fc1">
|
<img width="475" alt="Settings UI" src="https://github.com/redphx/better-xcloud/assets/96280/ad687344-214d-4822-affe-21f1b1e105c8">
|
||||||
<img width="500" alt="Video Settings UI" src="https://github.com/redphx/better-xcloud/assets/96280/ed219d50-02ab-40bd-95c5-a010956d77bf">
|
<img width="475" alt="Stream HUD UI" src="https://github.com/redphx/better-xcloud/assets/96280/ff695d3a-b077-4b21-b778-beb0a1fdd6be">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Demo video:** https://youtu.be/oDr5Eddp55E
|
||||||
|
|
||||||
- **Switch region of streaming server**
|
- **Switch region of streaming server**
|
||||||
> Connect to another server instead of the default one. Check [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)).
|
> 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.
|
||||||
@ -20,14 +23,17 @@ Give this project a 🌟 if you like it. Thank you 🙏.
|
|||||||
- **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...).
|
||||||
> 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 reduce quality when the internet speed is slow
|
||||||
|
- **🔥 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
|
||||||
- **Reduce UI animations**
|
- **Reduce UI animations**
|
||||||
@ -35,9 +41,9 @@ Give this project a 🌟 if you like it. Thank you 🙏.
|
|||||||
- **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/Dimension...
|
> 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)).
|
> Not working in Hermit ([#5](https://github.com/redphx/better-xcloud/issues/5)).
|
||||||
@ -45,11 +51,13 @@ Give this project a 🌟 if you like it. Thank you 🙏.
|
|||||||
> 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 to allow analytics to help Xbox improve xCloud's experence in the future.
|
||||||
> Not working in Hermit ([#5](https://github.com/redphx/better-xcloud/issues/5)).
|
> 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**
|
||||||
|
> 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.
|
<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. It's also available for Firefox on Android.
|
1. Install [Tampermonkey extension](https://www.tampermonkey.net/) on suppported browsers.
|
||||||
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)
|
||||||
@ -65,21 +73,62 @@ To update manually, just install the script again (you won't lose your settings)
|
|||||||
❌ = not supported (mostly because of lacking Userscript/extension support)
|
❌ = not supported (mostly because of lacking Userscript/extension support)
|
||||||
➖ = unavailable
|
➖ = unavailable
|
||||||
⚠️ = see custom notes
|
⚠️ = see custom notes
|
||||||
| | Desktop | Android | iOS |
|
| | Desktop | Android/Android TV | iOS |
|
||||||
|----------------------------------------|------------------|------------------|------------------|
|
|-----------------------------------------|:-----------------|:-------------------|:----------------|
|
||||||
| Chrome/Edge/Chromium variants | ✅ | ❌ | ❌ |
|
| Chrome/Edge/Chromium variants | ✅ | ❌ | ❌ |
|
||||||
| Firefox | ✅ | ✅<sup>(1)</sup> | ❌ |
|
| Firefox | ✅ | ⚠️<sup>(1)</sup> | ❌ |
|
||||||
| Safari | ✅<sup>(2)</sup> | ➖ | ✅<sup>(3)</sup> |
|
| Safari | ✅<sup>(2)</sup> | ➖ | ✅<sup>(3)</sup> |
|
||||||
| [Hermit](https://hermit.chimbori.com) | ➖ | ⚠️<sup>(4)</sup> | ➖ |
|
| [Hermit](https://hermit.chimbori.com) | ➖ | ⚠️<sup>(4)</sup> | ➖ |
|
||||||
| Kiwi Browser | ➖ | ✅ | ➖ |
|
| [Kiwi Browser](https://kiwibrowser.com) | ➖ | ✅ | ➖ |
|
||||||
|
|
||||||
Don't see your browser in the table? If it supports Tampermonkey/Userscript then the answer is likely **"YES"**.
|
Don't see your browser in the table? If it supports Tampermonkey/Userscript then the answer is likely **"YES"**.
|
||||||
|
|
||||||
<sup>1</sup> Follow [this guide](https://support.mozilla.org/en-US/kb/find-and-install-add-ons-firefox-android) to install Tampermonkey on Firefox Android.
|
<sup>1</sup> Follow [this guide](https://support.mozilla.org/en-US/kb/find-and-install-add-ons-firefox-android) to install Tampermonkey on Firefox Android. Its Gamepad API doesn't work properly so it might not recognize your controller.
|
||||||
<sup>2, 3</sup> Requires [Userscripts app](https://apps.apple.com/us/app/userscripts/id1463298887) (free & open source).
|
<sup>2, 3</sup> Requires [Userscripts app](https://apps.apple.com/us/app/userscripts/id1463298887) (free & open source).
|
||||||
<sup>4</sup> NOT RECOMMENDED at the moment since its Userscript implementation is not working properly (see https://github.com/redphx/better-xcloud/issues/5 for full details).
|
<sup>4</sup> NOT RECOMMENDED at the moment since its Userscript implementation is not working properly (see https://github.com/redphx/better-xcloud/issues/5 for full details).
|
||||||
|
|
||||||
In general, at the moment the best Android browser to use **Better xCloud** with is **Kiwi Browser**. 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.
|
||||||
|
|
||||||
|
## Stream stats
|
||||||
|

|
||||||
|
|
||||||
|
- 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 (lower is better) |
|
||||||
|
| BR | Bitrate | The amount of data server send to your device in the last second |
|
||||||
|
| PL | Packets Lost | 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:
|
||||||
|
- Chrome/Edge/Chromium variants: `chrome://webrtc-internals`
|
||||||
|
- Firefox: `about:webrtc`
|
||||||
|
|
||||||
|
|
||||||
|
## Capture screenshot
|
||||||
|
- This feature is only available in **Better xCloud**.
|
||||||
|
- Works on both desktop & mobile, but it's designed for mobile users.
|
||||||
|
- 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.
|
||||||
|
- 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.
|
||||||
|
- 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.
|
||||||
|
2. Play a game.
|
||||||
|
3. Tap once at the bottom left/right (depend 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.
|
||||||
|
|
||||||
|
<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?**
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name Better xCloud
|
// @name Better xCloud
|
||||||
// @namespace https://github.com/redphx
|
// @namespace https://github.com/redphx
|
||||||
// @version 1.4.2
|
// @version 1.6
|
||||||
// @description Improve Xbox Cloud Gaming (xCloud) experience
|
// @description Improve Xbox Cloud Gaming (xCloud) experience
|
||||||
// @author redphx
|
// @author redphx
|
||||||
// @license MIT
|
// @license MIT
|
||||||
@ -13,16 +13,21 @@
|
|||||||
// ==/UserScript==
|
// ==/UserScript==
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const SCRIPT_VERSION = '1.4.2';
|
const SCRIPT_VERSION = '1.6';
|
||||||
const SCRIPT_HOME = 'https://github.com/redphx/better-xcloud';
|
const SCRIPT_HOME = 'https://github.com/redphx/better-xcloud';
|
||||||
|
|
||||||
const SERVER_REGIONS = {};
|
const SERVER_REGIONS = {};
|
||||||
|
var STREAM_WEBRTC;
|
||||||
|
var $STREAM_VIDEO;
|
||||||
|
var $SCREENSHOT_CANVAS;
|
||||||
|
var GAME_TITLE_ID;
|
||||||
|
|
||||||
|
class StreamBadges {
|
||||||
class StreamStatus {
|
|
||||||
static ipv6 = false;
|
static ipv6 = false;
|
||||||
static dimension = {width: 0, height: 0};
|
static resolution = null;
|
||||||
static hqCodec = false;
|
static video = null;
|
||||||
|
static audio = null;
|
||||||
|
static fps = 0;
|
||||||
static region = '';
|
static region = '';
|
||||||
|
|
||||||
static #renderBadge(name, value, color) {
|
static #renderBadge(name, value, color) {
|
||||||
@ -35,27 +40,145 @@ class StreamStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static render() {
|
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 = [
|
const BADGES = [
|
||||||
['region', StreamStatus.region, '#d7450b'],
|
['region', StreamBadges.region, '#d7450b'],
|
||||||
['server', StreamStatus.ipv6 ? 'IPv6' : 'IPv4', '#008746'],
|
['server', StreamBadges.ipv6 ? 'IPv6' : 'IPv4', '#008746'],
|
||||||
['quality', StreamStatus.hqCodec ? 'High' : 'Normal', '#007c8f'],
|
video ? ['video', video, '#007c8f'] : null,
|
||||||
['dimension', `${StreamStatus.dimension.width}x${StreamStatus.dimension.height}`, '#ff3977'],
|
audio ? ['audio', audio, '#007c8f'] : null,
|
||||||
|
StreamBadges.resolution && ['resolution', `${StreamBadges.resolution.width}x${StreamBadges.resolution.height}`, '#ff3977'],
|
||||||
];
|
];
|
||||||
|
|
||||||
const $wrapper = createElement('div', {'class': 'better_xcloud_badges'});
|
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;
|
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 {
|
class Preferences {
|
||||||
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'; }
|
||||||
static get USE_DESKTOP_CODEC() { return 'use_desktop_codec'; }
|
static get USE_DESKTOP_CODEC() { return 'use_desktop_codec'; }
|
||||||
|
|
||||||
|
static get SCREENSHOT_BUTTON_POSITION() { return 'screenshot_button_position'; }
|
||||||
static get BLOCK_TRACKING() { return 'block_tracking'; }
|
static get BLOCK_TRACKING() { return 'block_tracking'; }
|
||||||
static get BLOCK_SOCIAL_FEATURES() { return 'block_social_features'; }
|
static get BLOCK_SOCIAL_FEATURES() { return 'block_social_features'; }
|
||||||
static get DISABLE_BANDWIDTH_CHECKING() { return 'disable_bandwidth_checking'; }
|
static get DISABLE_BANDWIDTH_CHECKING() { return 'disable_bandwidth_checking'; }
|
||||||
@ -73,88 +196,71 @@ class Preferences {
|
|||||||
'id': Preferences.SERVER_REGION,
|
'id': Preferences.SERVER_REGION,
|
||||||
'label': 'Region of streaming server',
|
'label': 'Region of streaming server',
|
||||||
'default': 'default',
|
'default': 'default',
|
||||||
},
|
}, {
|
||||||
|
|
||||||
{
|
|
||||||
'id': Preferences.FORCE_1080P_STREAM,
|
'id': Preferences.FORCE_1080P_STREAM,
|
||||||
'label': 'Force 1080p stream',
|
'label': 'Force 1080p stream',
|
||||||
'default': false,
|
'default': false,
|
||||||
},
|
}, {
|
||||||
|
|
||||||
{
|
|
||||||
'id': Preferences.USE_DESKTOP_CODEC,
|
'id': Preferences.USE_DESKTOP_CODEC,
|
||||||
'label': 'Force high quality codec (if possible)',
|
'label': 'Force high quality codec (if possible)',
|
||||||
'default': false,
|
'default': false,
|
||||||
},
|
}, {
|
||||||
|
|
||||||
{
|
|
||||||
'id': Preferences.PREFER_IPV6_SERVER,
|
'id': Preferences.PREFER_IPV6_SERVER,
|
||||||
'label': 'Prefer IPv6 streaming server',
|
'label': 'Prefer IPv6 streaming server',
|
||||||
'default': false,
|
'default': false,
|
||||||
},
|
}, {
|
||||||
|
|
||||||
{
|
|
||||||
'id': Preferences.DISABLE_BANDWIDTH_CHECKING,
|
'id': Preferences.DISABLE_BANDWIDTH_CHECKING,
|
||||||
'label': 'Disable bandwidth checking',
|
'label': 'Disable bandwidth checking',
|
||||||
'default': false,
|
'default': false,
|
||||||
},
|
}, {
|
||||||
|
'id': Preferences.SCREENSHOT_BUTTON_POSITION,
|
||||||
{
|
'label': 'Screenshot button\'s position',
|
||||||
|
'default': 'bottom-left',
|
||||||
|
'options': {
|
||||||
|
'bottom-left': 'Bottom Left',
|
||||||
|
'bottom-right': 'Bottom Right',
|
||||||
|
'none': 'Disable',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
'id': Preferences.SKIP_SPLASH_VIDEO,
|
'id': Preferences.SKIP_SPLASH_VIDEO,
|
||||||
'label': 'Skip Xbox splash video',
|
'label': 'Skip Xbox splash video',
|
||||||
'default': false,
|
'default': false,
|
||||||
},
|
}, {
|
||||||
|
|
||||||
{
|
|
||||||
'id': Preferences.HIDE_DOTS_ICON,
|
'id': Preferences.HIDE_DOTS_ICON,
|
||||||
'label': 'Hide Dots icon while playing',
|
'label': 'Hide Dots icon while playing',
|
||||||
'default': false,
|
'default': false,
|
||||||
},
|
}, {
|
||||||
|
|
||||||
{
|
|
||||||
'id': Preferences.REDUCE_ANIMATIONS,
|
'id': Preferences.REDUCE_ANIMATIONS,
|
||||||
'label': 'Reduce UI animations',
|
'label': 'Reduce UI animations',
|
||||||
'default': false,
|
'default': false,
|
||||||
},
|
}, {
|
||||||
|
|
||||||
{
|
|
||||||
'id': Preferences.BLOCK_SOCIAL_FEATURES,
|
'id': Preferences.BLOCK_SOCIAL_FEATURES,
|
||||||
'label': 'Disable social features',
|
'label': 'Disable social features',
|
||||||
'default': false,
|
'default': false,
|
||||||
},
|
}, {
|
||||||
|
|
||||||
{
|
|
||||||
'id': Preferences.BLOCK_TRACKING,
|
'id': Preferences.BLOCK_TRACKING,
|
||||||
'label': 'Disable xCloud analytics',
|
'label': 'Disable xCloud analytics',
|
||||||
'default': false,
|
'default': false,
|
||||||
},
|
}, {
|
||||||
|
|
||||||
{
|
|
||||||
'id': Preferences.VIDEO_FILL_FULL_SCREEN,
|
'id': 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,
|
'id': 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,
|
'id': 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,
|
'id': Preferences.VIDEO_BRIGHTNESS,
|
||||||
'label': 'Video brightness (%)',
|
'label': 'Video brightness (%)',
|
||||||
'default': 100,
|
'default': 100,
|
||||||
@ -333,7 +439,7 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
|
|||||||
|
|
||||||
.better_xcloud_badges {
|
.better_xcloud_badges {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -35px;
|
top: 155px;
|
||||||
margin-left: 0px;
|
margin-left: 0px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
@ -345,7 +451,9 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
font-family: Bahnschrift Semibold, Arial, Helvetica, sans-serif;
|
font-family: Bahnschrift Semibold, Arial, Helvetica, sans-serif;
|
||||||
font-weight: 400;
|
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 {
|
.better_xcloud_badge .better_xcloud_badge_name {
|
||||||
@ -363,6 +471,67 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
|
|||||||
border-radius: 0 4px 4px 0;
|
border-radius: 0 4px 4px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.better_xcloud_screenshot_button {
|
||||||
|
display: none;
|
||||||
|
opacity: 0;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
padding: 5px;
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-origin: content-box;
|
||||||
|
filter: drop-shadow(0 0 2px #000000B0);
|
||||||
|
transition: opacity 0.1s ease-in-out 0s, padding 0.1s ease-in 0s;
|
||||||
|
z-index: 8888;
|
||||||
|
|
||||||
|
/* Credit: https://www.iconfinder.com/iconsets/user-interface-outline-27 */
|
||||||
|
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjQiIHdpZHRoPSIyNCIgeG1sbnM6dj0iaHR0cHM6Ly92ZWN0YS5pby9uYW5vIiBmaWxsPSIjZmZmIj48cGF0aCBkPSJNMTIgN2E1LjAyIDUuMDIgMCAwIDAtNSA1IDUuMDIgNS4wMiAwIDAgMCA1IDUgNS4wMiA1LjAyIDAgMCAwIDUtNSA1LjAyIDUuMDIgMCAwIDAtNS01em0wIDJjMS42NjkgMCAzIDEuMzMxIDMgM3MtMS4zMzEgMy0zIDMtMy0xLjMzMS0zLTMgMS4zMzEtMyAzLTN6TTYgMkMzLjgwMSAyIDIgMy44MDEgMiA2djJhMSAxIDAgMSAwIDIgMFY2YTEuOTcgMS45NyAwIDAgMSAyLTJoMmExIDEgMCAxIDAgMC0yek0zIDE1YTEgMSAwIDAgMC0xIDF2MmMwIDIuMTk5IDEuODAxIDQgNCA0aDJhMSAxIDAgMSAwIDAtMkg2YTEuOTcgMS45NyAwIDAgMS0yLTJ2LTJhMSAxIDAgMCAwLTEtMXptMTggMGExIDEgMCAwIDAtMSAxdjJhMS45NyAxLjk3IDAgMCAxLTIgMmgtMmExIDEgMCAxIDAgMCAyaDJjMi4xOTkgMCA0LTEuODAxIDQtNHYtMmExIDEgMCAwIDAtMS0xeiIvPjxwYXRoIGQ9Ik0xNiAyYTEgMSAwIDEgMCAwIDJoMmExLjk3IDEuOTcgMCAwIDEgMiAydjJhMSAxIDAgMSAwIDIgMFY2YzAtMi4xOTktMS44MDEtNC00LTR6Ii8+PC9zdmc+Cg==);
|
||||||
|
}
|
||||||
|
|
||||||
|
.better_xcloud_screenshot_button[data-showing=true] {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.better_xcloud_screenshot_button[data-capturing=true] {
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.better_xcloud_screenshot_canvas {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.better_xcloud_stats_bar {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 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 */
|
/* Hide UI elements */
|
||||||
#headerArea, #uhfSkipToMain, .uhf-footer {
|
#headerArea, #uhfSkipToMain, .uhf-footer {
|
||||||
display: none;
|
display: none;
|
||||||
@ -379,6 +548,12 @@ 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: 110px !important;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Reduce animations
|
// Reduce animations
|
||||||
@ -524,7 +699,6 @@ function interceptHttpRequests() {
|
|||||||
const PREF_PREFER_IPV6_SERVER = PREFS.get(Preferences.PREFER_IPV6_SERVER);
|
const PREF_PREFER_IPV6_SERVER = PREFS.get(Preferences.PREFER_IPV6_SERVER);
|
||||||
const PREF_FORCE_1080P_STREAM = PREFS.get(Preferences.FORCE_1080P_STREAM);
|
const PREF_FORCE_1080P_STREAM = PREFS.get(Preferences.FORCE_1080P_STREAM);
|
||||||
const PREF_USE_DESKTOP_CODEC = PREFS.get(Preferences.USE_DESKTOP_CODEC);
|
const PREF_USE_DESKTOP_CODEC = PREFS.get(Preferences.USE_DESKTOP_CODEC);
|
||||||
const HAS_CODECS_API_SUPPORT = hasRtcSetCodecPreferencesSupport();
|
|
||||||
|
|
||||||
const orgFetch = window.fetch;
|
const orgFetch = window.fetch;
|
||||||
window.fetch = async (...arg) => {
|
window.fetch = async (...arg) => {
|
||||||
@ -569,11 +743,11 @@ function interceptHttpRequests() {
|
|||||||
if (url.endsWith('/sessions/cloud/play')) {
|
if (url.endsWith('/sessions/cloud/play')) {
|
||||||
const parsedUrl = new URL(url);
|
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) {
|
for (let regionName in SERVER_REGIONS) {
|
||||||
const region = SERVER_REGIONS[regionName];
|
const region = SERVER_REGIONS[regionName];
|
||||||
if (parsedUrl.origin == region.baseUri) {
|
if (parsedUrl.origin == region.baseUri) {
|
||||||
StreamStatus.region = regionName;
|
StreamBadges.region = regionName;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -595,27 +769,6 @@ function interceptHttpRequests() {
|
|||||||
return orgFetch(...arg);
|
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
|
// ICE server candidates
|
||||||
if (PREF_PREFER_IPV6_SERVER && url.endsWith('/ice') && url.includes('/sessions/cloud/') && request.method === 'GET') {
|
if (PREF_PREFER_IPV6_SERVER && url.endsWith('/ice') && url.includes('/sessions/cloud/') && request.method === 'GET') {
|
||||||
const promise = orgFetch(...arg);
|
const promise = orgFetch(...arg);
|
||||||
@ -673,7 +826,7 @@ function createElement(elmName, props = {}) {
|
|||||||
|
|
||||||
if (argType == 'string' || argType == 'number') {
|
if (argType == 'string' || argType == 'number') {
|
||||||
$elm.innerText = arg;
|
$elm.innerText = arg;
|
||||||
} else {
|
} else if (arg) {
|
||||||
$elm.appendChild(arg);
|
$elm.appendChild(arg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -720,27 +873,41 @@ function injectSettingsButton($parent) {
|
|||||||
|
|
||||||
let $control;
|
let $control;
|
||||||
let labelAttrs = {};
|
let labelAttrs = {};
|
||||||
if (setting.id === Preferences.SERVER_REGION) {
|
if (setting.id === Preferences.SERVER_REGION || setting.options) {
|
||||||
|
let selectedValue;
|
||||||
|
|
||||||
$control = CE('select', {id: 'xcloud_setting_' + setting.id});
|
$control = CE('select', {id: 'xcloud_setting_' + setting.id});
|
||||||
$control.addEventListener('change', e => {
|
$control.addEventListener('change', e => {
|
||||||
PREFS.set(Preferences.SERVER_REGION, e.target.value);
|
PREFS.set(setting.id, e.target.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let regionName in SERVER_REGIONS) {
|
if (setting.id === Preferences.SERVER_REGION) {
|
||||||
const region = SERVER_REGIONS[regionName];
|
selectedValue = preferredRegion;
|
||||||
let value = regionName;
|
setting.options = {};
|
||||||
|
for (let regionName in SERVER_REGIONS) {
|
||||||
|
const region = SERVER_REGIONS[regionName];
|
||||||
|
let value = regionName;
|
||||||
|
|
||||||
let label = regionName;
|
let label = regionName;
|
||||||
if (region.isDefault) {
|
if (region.isDefault) {
|
||||||
label += ' (Default)';
|
label += ' (Default)';
|
||||||
value = 'default';
|
value = 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
setting.options[value] = label;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
selectedValue = PREFS.get(setting.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let value in setting.options) {
|
||||||
|
const label = setting.options[value];
|
||||||
|
|
||||||
const $option = CE('option', {value: value}, label);
|
const $option = CE('option', {value: value}, label);
|
||||||
$option.selected = regionName === preferredRegion;
|
$option.selected = value === selectedValue || label.includes(selectedValue);
|
||||||
|
|
||||||
$control.appendChild($option);
|
$control.appendChild($option);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
$control = CE('input', {
|
$control = CE('input', {
|
||||||
id: 'xcloud_setting_' + setting.id,
|
id: 'xcloud_setting_' + setting.id,
|
||||||
@ -750,16 +917,19 @@ function injectSettingsButton($parent) {
|
|||||||
|
|
||||||
$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);
|
||||||
|
|
||||||
if (setting.id == Preferences.VIDEO_FILL_FULL_SCREEN) {
|
|
||||||
updateVideoPlayerPreview();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setting.value = PREFS.get(setting.id);
|
setting.value = PREFS.get(setting.id);
|
||||||
$control.checked = setting.value;
|
$control.checked = setting.value;
|
||||||
|
|
||||||
labelAttrs = {'for': 'xcloud_setting_' + setting.id, 'tabindex': 0};
|
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'},
|
const $elm = CE('div', {'class': 'setting_row'},
|
||||||
@ -825,24 +995,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() {
|
function checkHeader() {
|
||||||
const $button = document.querySelector('#PageContent header .better_xcloud_settings_button');
|
const $button = document.querySelector('#PageContent header .better_xcloud_settings_button');
|
||||||
|
|
||||||
@ -873,6 +1025,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() {
|
function injectVideoSettingsButton() {
|
||||||
const $screen = document.querySelector('#PageContent section[class*=PureScreens]');
|
const $screen = document.querySelector('#PageContent section[class*=PureScreens]');
|
||||||
if (!$screen) {
|
if (!$screen) {
|
||||||
@ -914,29 +1079,16 @@ function injectVideoSettingsButton() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = 'better-xcloud-video-settings-btn';
|
|
||||||
let $wrapper = document.getElementById('#' + id);
|
|
||||||
if ($wrapper) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const $orgButton = node.querySelector('div > div > button');
|
const $orgButton = node.querySelector('div > div > button');
|
||||||
if (!$orgButton) {
|
if (!$orgButton) {
|
||||||
return;
|
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
|
// 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 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 $svg = $button.querySelector('svg');
|
// Create Video Settings button
|
||||||
$svg.innerHTML = SVG_ICON;
|
const $btnVideoSettings = cloneStreamMenuButton($orgButton, 'Video settings', ICON_VIDEO_SETTINGS);
|
||||||
$svg.setAttribute('viewBox', '0 0 24 24');
|
$btnVideoSettings.addEventListener('click', e => {
|
||||||
|
|
||||||
$button.addEventListener('click', e => {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
@ -947,23 +1099,38 @@ function injectVideoSettingsButton() {
|
|||||||
$parent.addEventListener('touchend', hideQuickBarFunc);
|
$parent.addEventListener('touchend', hideQuickBarFunc);
|
||||||
|
|
||||||
const $touchSurface = document.querySelector('#MultiTouchSurface');
|
const $touchSurface = document.querySelector('#MultiTouchSurface');
|
||||||
if ($touchSurface) {
|
$touchSurface && $touchSurface.addEventListener('touchstart', hideQuickBarFunc);
|
||||||
$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
|
// 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';
|
$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
|
// 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(StreamStatus.render());
|
$menu.appendChild(StreamBadges.render());
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
observer.observe($screen, {subtree: true, childList: true});
|
observer.observe($screen, {subtree: true, childList: true});
|
||||||
@ -972,6 +1139,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);
|
||||||
|
|
||||||
// Show video player when it's ready
|
// Show video player when it's ready
|
||||||
var showFunc;
|
var showFunc;
|
||||||
@ -979,9 +1147,54 @@ function patchVideoApi() {
|
|||||||
this.style.visibility = 'visible';
|
this.style.visibility = 'visible';
|
||||||
this.removeEventListener('playing', showFunc);
|
this.removeEventListener('playing', showFunc);
|
||||||
|
|
||||||
if (this.videoWidth) {
|
if (!this.videoWidth) {
|
||||||
StreamStatus.dimension = {width: this.videoWidth, height: this.videoHeight};
|
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;
|
HTMLMediaElement.prototype.orgPlay = HTMLMediaElement.prototype.play;
|
||||||
@ -1022,20 +1235,22 @@ function patchRtcCodecs() {
|
|||||||
RTCRtpTransceiver.prototype.setCodecPreferences = function(codecs) {
|
RTCRtpTransceiver.prototype.setCodecPreferences = function(codecs) {
|
||||||
// Use the same codecs as desktop
|
// Use the same codecs as desktop
|
||||||
const newCodecs = codecs.slice();
|
const newCodecs = codecs.slice();
|
||||||
|
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);
|
||||||
newCodecs.unshift(codec);
|
newCodecs.splice(pos, 0, codec);
|
||||||
|
++pos;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.orgSetCodecPreferences(newCodecs);
|
this.orgSetCodecPreferences.apply(this, [newCodecs]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
this.orgSetCodecPreferences(codecs);
|
this.orgSetCodecPreferences.apply(this, [codecs]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1212,6 +1427,75 @@ function setupVideoSettingsBar() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function setupScreenshotButton() {
|
||||||
|
$SCREENSHOT_CANVAS = createElement('canvas', {'class': 'better_xcloud_screenshot_canvas'});
|
||||||
|
document.documentElement.appendChild($SCREENSHOT_CANVAS);
|
||||||
|
|
||||||
|
const $canvasContext = $SCREENSHOT_CANVAS.getContext('2d');
|
||||||
|
|
||||||
|
const delay = 2000;
|
||||||
|
const $btn = createElement('div', {'class': 'better_xcloud_screenshot_button', 'data-showing': false});
|
||||||
|
|
||||||
|
let timeout;
|
||||||
|
const detectDbClick = e => {
|
||||||
|
if (!$STREAM_VIDEO) {
|
||||||
|
timeout = null;
|
||||||
|
$btn.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = null;
|
||||||
|
$btn.setAttribute('data-capturing', 'true');
|
||||||
|
|
||||||
|
$canvasContext.drawImage($STREAM_VIDEO, 0, 0, $SCREENSHOT_CANVAS.width, $SCREENSHOT_CANVAS.height);
|
||||||
|
$SCREENSHOT_CANVAS.toBlob(blob => {
|
||||||
|
// Download screenshot
|
||||||
|
const now = +new Date;
|
||||||
|
const $anchor = createElement('a', {
|
||||||
|
'download': `${GAME_TITLE_ID}-${now}.png`,
|
||||||
|
'href': URL.createObjectURL(blob),
|
||||||
|
});
|
||||||
|
$anchor.click();
|
||||||
|
|
||||||
|
// Free screenshot from memory
|
||||||
|
URL.revokeObjectURL($anchor.href);
|
||||||
|
$canvasContext.clearRect(0, 0, $SCREENSHOT_CANVAS.width, $SCREENSHOT_CANVAS.height);
|
||||||
|
|
||||||
|
// Hide button
|
||||||
|
$btn.setAttribute('data-showing', 'false');
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!timeout) {
|
||||||
|
$btn.setAttribute('data-capturing', 'false');
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}, 'image/png');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isShowing = $btn.getAttribute('data-showing') === 'true';
|
||||||
|
if (!isShowing) {
|
||||||
|
// Show button
|
||||||
|
$btn.setAttribute('data-showing', 'true');
|
||||||
|
$btn.setAttribute('data-capturing', 'false');
|
||||||
|
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
timeout = null;
|
||||||
|
$btn.setAttribute('data-showing', 'false');
|
||||||
|
$btn.setAttribute('data-capturing', 'false');
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$btn.addEventListener('mousedown', detectDbClick);
|
||||||
|
document.documentElement.appendChild($btn);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function patchHistoryMethod(type) {
|
function patchHistoryMethod(type) {
|
||||||
var orig = window.history[type];
|
var orig = window.history[type];
|
||||||
return function(...args) {
|
return function(...args) {
|
||||||
@ -1234,6 +1518,11 @@ function hideUiOnPageChange() {
|
|||||||
if ($quickBar) {
|
if ($quickBar) {
|
||||||
$quickBar.style.display = 'none';
|
$quickBar.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
STREAM_WEBRTC = null;
|
||||||
|
$STREAM_VIDEO = null;
|
||||||
|
StreamStats.stop();
|
||||||
|
document.querySelector('.better_xcloud_screenshot_button').style = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1255,15 +1544,15 @@ if (PREFS.get(Preferences.DISABLE_BANDWIDTH_CHECKING)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
patchRtcCodecs();
|
patchRtcCodecs();
|
||||||
|
|
||||||
interceptHttpRequests();
|
interceptHttpRequests();
|
||||||
|
|
||||||
patchVideoApi();
|
patchVideoApi();
|
||||||
|
|
||||||
// Setup UI
|
// Setup UI
|
||||||
addCss();
|
addCss();
|
||||||
updateVideoPlayerCss();
|
updateVideoPlayerCss();
|
||||||
setupVideoSettingsBar();
|
setupVideoSettingsBar();
|
||||||
|
setupScreenshotButton();
|
||||||
|
StreamStats.render();
|
||||||
|
|
||||||
// Workaround for Hermit browser
|
// Workaround for Hermit browser
|
||||||
var onLoadTriggered = false;
|
var onLoadTriggered = false;
|
||||||
@ -1275,43 +1564,13 @@ if (document.readyState === 'complete' && !onLoadTriggered) {
|
|||||||
watchHeader();
|
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.orgAddIceCandidate = RTCPeerConnection.prototype.addIceCandidate;
|
||||||
RTCPeerConnection.prototype.addIceCandidate = function(...args) {
|
RTCPeerConnection.prototype.addIceCandidate = function(...args) {
|
||||||
const candidate = args[0].candidate;
|
const candidate = args[0].candidate;
|
||||||
if (candidate && candidate.startsWith('a=candidate:1 ')) {
|
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);
|
return this.orgAddIceCandidate.apply(this, args);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user