Compare commits

..

15 Commits

Author SHA1 Message Date
redphx
3c4248c1c7 Update README.md 2023-07-21 16:51:40 +07:00
redphx
de834fcb80 Bump version to 1.4.2 2023-07-21 16:49:28 +07:00
redphx
baf2c2a35d Fix "Force high quality codec" feature not working with Kiwi Browser 2023-07-21 16:42:57 +07:00
redphx
522830a47d Update README.md 2023-07-21 15:16:57 +07:00
redphx
7207646379 Bump version to 1.4.1 2023-07-21 09:02:37 +07:00
redphx
597c150c77 Update README.md 2023-07-21 09:02:10 +07:00
redphx
e7980c186d Add Kiwi Browser support (#11)
* Catch exception when trying to force desktop codec profile on Kiwi Browser

* Improve setCodecPreferences(), avoid crashing

* Improve codec status detection

* Fix codec detection
2023-07-21 08:57:05 +07:00
redphx
0c38b54c38 Update README.md 2023-07-20 21:06:49 +07:00
redphx
1929834c98 Update README.md 2023-07-20 18:50:57 +07:00
redphx
6e80ea08e8 Bump version to 1.4 2023-07-20 18:40:27 +07:00
redphx
a5caafa93a Stream badges & 1080p stream (#10)
* Get stream statuses

* Render badges

* Add "Force 1080p stream" feature (#9)
2023-07-20 18:38:58 +07:00
redphx
f8134c3e5a Update README.md 2023-07-19 13:22:48 +07:00
redphx
89ea55774b Make Settings UI can be interacted using controller 2023-07-19 09:03:36 +07:00
redphx
2836eeb6ed Update README.md 2023-07-17 20:10:51 +07:00
redphx
876b090ad2 Update README.md 2023-07-17 19:54:44 +07:00
2 changed files with 248 additions and 46 deletions

View File

@@ -6,17 +6,24 @@ Give this project a 🌟 if you like it. Thank you 🙏.
## Features
<img width="500" alt="Settings UI" src="https://github.com/redphx/better-xcloud/assets/96280/2f5b81f7-f739-4f8e-bb30-7b404fa35628">
<img width="500" alt="Video Settings UI" src="https://github.com/redphx/better-xcloud/assets/96280/130aa870-6938-4604-9e23-45e217b800cc">
<img width="500" alt="Settings UI" src="https://github.com/redphx/better-xcloud/assets/96280/8fb9f0ac-85f5-4e5a-9570-5a5e119e4fc1">
<img width="500" alt="Video Settings UI" src="https://github.com/redphx/better-xcloud/assets/96280/ed219d50-02ab-40bd-95c5-a010956d77bf">
- **Switch region of streaming server**
> Connect to another server instead of the default one. Check [FAQ section](#faq) for some notes.
> Not working in Hermit ([#5](https://github.com/redphx/better-xcloud/issues/5)).
- **Force 1080p stream**
> 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.
> Not working in Hermit ([#5](https://github.com/redphx/better-xcloud/issues/5)).
- **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.
> Use more bandwidth & battery.
> Comparison video with the setting ON & OFF: https://youtu.be/-9PuBJJSgR4
- **Prefer IPv6 streaming server**
> Might reduce latency
- **Force high quality stream**
> Force xCloud to use the best streaming codec profile (same as desktop & TV). You don't have to change User-Agent anymore.
> Use more bandwitdh & battery.
> Comparison video with the setting ON & OFF: https://youtu.be/-9PuBJJSgR4
- **Disable bandwidth checking**
> xCloud won't reduce quality when the internet speed is slow
- **Skip Xbox splash video**
@@ -29,12 +36,18 @@ Give this project a 🌟 if you like it. Thank you 🙏.
> Useful when you don't have a 16:9 screen
- **Adjust video filters**
> Brightness/Contrast/Saturation
- **Display stream's statuses**
> Region/Server/Quality/Dimension...
- **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.
> Not working in Hermit ([#5](https://github.com/redphx/better-xcloud/issues/5)).
- **Hide footer and other UI elements**
<sup>(\*)</sup> By default (for compatibility reasons) xCloud only uses high quality codec profile when you use Tizen TV or Chrome/Edge/Chromium browser on Chrome/MacOS. Enable this setting will give you the best experience no matter what platform & browser you're on.
## How to use
1. Install [Tampermonkey extension](https://www.tampermonkey.net/) on suppported browsers. It's also available for Firefox on Android.
2. Install **Better xCloud**:
@@ -50,18 +63,23 @@ To update manually, just install the script again (you won't lose your settings)
✅ = confirmed to be working
❓ = not yet tested
❌ = not supported (mostly because of lacking Userscript/extension support)
= unavailable
⚠️ = see custom notes
| | Desktop | Android | iOS |
|----------------------------------------|------------------|------------------|-----------------|
|----------------------------------------|------------------|------------------|------------------|
| Chrome/Edge/Chromium variants | ✅ | ❌ | ❌ |
| Firefox | ✅ | ✅ | ❌ |
| Safari | ✅<sup>(1)</sup> | | ✅<sup>(2)</sup> |
| [Hermit](https://hermit.chimbori.com) | | ⚠️<sup>(3)</sup> | |
| Firefox | ✅ | ✅<sup>(1)</sup> | ❌ |
| Safari | ✅<sup>(2)</sup> | | ✅<sup>(3)</sup> |
| [Hermit](https://hermit.chimbori.com) | | ⚠️<sup>(4)</sup> | |
| Kiwi Browser | | ✅ | |
Don't see your browser in the table? If it supports Tampermonkey/Userscript then the answer is likely **"YES"**.
<sup>1, 2</sup> Requires [Userscripts app](https://apps.apple.com/us/app/userscripts/id1463298887).
<sup>3</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). It's still my favorite app to play xCloud on because it's lightweight, supports Userscript (premium features, only $1.99) without having to install anything else. I built **Better xCloud** just so I could use it with Hermit.
<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>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).
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).
## FAQ
1. **Will I get banned for using this?**

View File

@@ -1,7 +1,7 @@
// ==UserScript==
// @name Better xCloud
// @namespace https://github.com/redphx
// @version 1.3.2
// @version 1.4.2
// @description Improve Xbox Cloud Gaming (xCloud) experience
// @author redphx
// @license MIT
@@ -13,15 +13,47 @@
// ==/UserScript==
'use strict';
const SCRIPT_VERSION = '1.3.2';
const SCRIPT_VERSION = '1.4.2';
const SCRIPT_HOME = 'https://github.com/redphx/better-xcloud';
const SERVER_REGIONS = {};
class StreamStatus {
static ipv6 = false;
static dimension = {width: 0, height: 0};
static hqCodec = false;
static region = '';
static #renderBadge(name, value, color) {
const CE = createElement;
const $badge = CE('div', {'class': 'better_xcloud_badge'},
CE('span', {'class': 'better_xcloud_badge_name'}, name),
CE('span', {'class': 'better_xcloud_badge_value', 'style': `background-color: ${color}`}, value));
return $badge;
}
static render() {
const BADGES = [
['region', StreamStatus.region, '#d7450b'],
['server', StreamStatus.ipv6 ? 'IPv6' : 'IPv4', '#008746'],
['quality', StreamStatus.hqCodec ? 'High' : 'Normal', '#007c8f'],
['dimension', `${StreamStatus.dimension.width}x${StreamStatus.dimension.height}`, '#ff3977'],
];
const $wrapper = createElement('div', {'class': 'better_xcloud_badges'});
BADGES.forEach(item => $wrapper.appendChild(StreamStatus.#renderBadge(...item)));
return $wrapper;
}
}
class Preferences {
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'; }
static get USE_DESKTOP_CODEC() { return 'use_desktop_codec'; }
static get BLOCK_TRACKING() { return 'block_tracking'; }
@@ -44,14 +76,20 @@ class Preferences {
},
{
'id': Preferences.PREFER_IPV6_SERVER,
'label': 'Prefer IPv6 streaming server',
'id': Preferences.FORCE_1080P_STREAM,
'label': 'Force 1080p stream',
'default': false,
},
{
'id': Preferences.USE_DESKTOP_CODEC,
'label': 'Force high quality stream',
'label': 'Force high quality codec (if possible)',
'default': false,
},
{
'id': Preferences.PREFER_IPV6_SERVER,
'label': 'Prefer IPv6 streaming server',
'default': false,
},
@@ -193,7 +231,7 @@ function addCss() {
padding: 8px;
}
.better_xcloud_settings_button:hover, .better_xlcoud_settings_button:focus {
.better_xcloud_settings_button:hover, .better_xcloud_settings_button:focus {
background-color: #515863;
}
@@ -225,11 +263,23 @@ function addCss() {
font-weight: bold;
display: block;
margin-bottom: 8px;
color: #5dc21e;
}
@media (hover: hover) {
.better_xcloud_settings_wrapper a:hover {
color: #83f73a;
}
}
.better_xcloud_settings_wrapper a:focus {
color: #83f73a;
}
.better_xcloud_settings_wrapper .setting_row {
display: flex;
margin-bottom: 8px;
padding: 2px 4px;
}
.better_xcloud_settings_wrapper .setting_row label {
@@ -238,6 +288,12 @@ function addCss() {
margin-bottom: 0;
}
@media not (hover: hover) {
.better_xcloud_settings_wrapper .setting_row:focus-within {
background-color: #242424;
}
}
.better_xcloud_settings_wrapper .setting_row input {
align-self: center;
}
@@ -263,10 +319,50 @@ function addCss() {
}
}
.better_xcloud_settings_wrapper .setting_button:focus {
background-color: #00753c;
}
.better_xcloud_settings_wrapper .setting_button:active {
background-color: #00753c;
}
div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
overflow: visible;
}
.better_xcloud_badges {
position: absolute;
bottom: -35px;
margin-left: 0px;
user-select: none;
}
.better_xcloud_badge {
border: none;
display: inline-block;
line-height: 24px;
color: #fff;
font-family: Bahnschrift Semibold, Arial, Helvetica, sans-serif;
font-weight: 400;
margin-right: 8px;
}
.better_xcloud_badge .better_xcloud_badge_name {
background-color: #2d3036;
display: inline-block;
padding: 2px 8px;
border-radius: 4px 0 0 4px;
text-transform: uppercase;
}
.better_xcloud_badge .better_xcloud_badge_value {
background-color: grey;
display: inline-block;
padding: 2px 8px;
border-radius: 0 4px 4px 0;
}
/* Hide UI elements */
#headerArea, #uhfSkipToMain, .uhf-footer {
display: none;
@@ -340,7 +436,7 @@ function getPreferredServerRegion() {
function updateIceCandidates(candidates) {
const pattern = new RegExp(/a=candidate:(?<order>\d+) (?<num>\d+) UDP (?<priority>\d+) (?<ip>[^\s]+) (?<the_rest>.*)/);
const pattern = new RegExp(/a=candidate:(?<foundation>\d+) (?<component>\d+) UDP (?<priority>\d+) (?<ip>[^\s]+) (?<the_rest>.*)/);
const lst = [];
for (let item of candidates) {
@@ -355,19 +451,19 @@ function updateIceCandidates(candidates) {
lst.sort((a, b) => (a.ip.includes(':') || a.ip > b.ip) ? -1 : 1);
const newCandidates = [];
let order = 1;
let foundation = 1;
lst.forEach(item => {
item.order = order;
item.priority = (order == 1) ? 100 : 1;
item.foundation = foundation;
item.priority = (foundation == 1) ? 100 : 1;
newCandidates.push({
'candidate': `a=candidate:${item.order} 1 UDP ${item.priority} ${item.ip} ${item.the_rest}`,
'candidate': `a=candidate:${item.foundation} 1 UDP ${item.priority} ${item.ip} ${item.the_rest}`,
'messageType': 'iceCandidate',
'sdpMLineIndex': '0',
'sdpMid': '0',
});
++order;
++foundation;
});
newCandidates.push({
@@ -426,6 +522,7 @@ 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();
@@ -468,6 +565,36 @@ function interceptHttpRequests() {
});
}
// Get region
if (url.endsWith('/sessions/cloud/play')) {
const parsedUrl = new URL(url);
StreamStatus.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;
break;
}
}
// Force 1080p stream
if (PREF_FORCE_1080P_STREAM) {
// Intercept "osName" value
const clone = request.clone();
const body = await clone.json();
body.settings.osName = 'windows';
const newRequest = new Request(request, {
body: JSON.stringify(body),
});
arg[0] = newRequest;
}
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);
@@ -592,6 +719,7 @@ function injectSettingsButton($parent) {
}
let $control;
let labelAttrs = {};
if (setting.id === Preferences.SERVER_REGION) {
$control = CE('select', {id: 'xcloud_setting_' + setting.id});
$control.addEventListener('change', e => {
@@ -630,17 +758,19 @@ function injectSettingsButton($parent) {
setting.value = PREFS.get(setting.id);
$control.checked = setting.value;
labelAttrs = {'for': 'xcloud_setting_' + setting.id, 'tabindex': 0};
}
const $elm = CE('div', {'class': 'setting_row'},
CE('label', {'for': 'xcloud_setting_' + setting.id}, setting.label),
CE('label', labelAttrs, setting.label),
$control
);
$wrapper.appendChild($elm);
}
const $reloadBtn = CE('button', {'class': 'setting_button'}, 'Reload page to reflect changes');
const $reloadBtn = CE('button', {'class': 'setting_button', 'tabindex': 0}, 'Reload page to reflect changes');
$reloadBtn.addEventListener('click', e => window.location.reload());
$wrapper.appendChild($reloadBtn);
@@ -813,9 +943,6 @@ function injectVideoSettingsButton() {
// Show Quick settings bar
$quickBar.style.display = 'flex';
// Close HUD
document.querySelector('button[class*=StreamMenu-module__backButton]').click();
$parent.addEventListener('click', hideQuickBarFunc);
$parent.addEventListener('touchend', hideQuickBarFunc);
@@ -826,6 +953,15 @@ function injectVideoSettingsButton() {
});
$orgButton.parentElement.insertBefore($button, $orgButton.parentElement.firstChild);
// Hide Quick bar when closing HUD
document.querySelector('button[class*=StreamMenu-module__backButton]').addEventListener('click', e => {
$quickBar.style.display = 'none';
});
// Render stream badges
const $menu = document.querySelector('div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module]');
$menu.appendChild(StreamStatus.render());
});
});
@@ -842,6 +978,10 @@ function patchVideoApi() {
showFunc = function() {
this.style.visibility = 'visible';
this.removeEventListener('playing', showFunc);
if (this.videoWidth) {
StreamStatus.dimension = {width: this.videoWidth, height: this.videoHeight};
}
}
HTMLMediaElement.prototype.orgPlay = HTMLMediaElement.prototype.play;
@@ -881,20 +1021,23 @@ function patchRtcCodecs() {
RTCRtpTransceiver.prototype.orgSetCodecPreferences = RTCRtpTransceiver.prototype.setCodecPreferences;
RTCRtpTransceiver.prototype.setCodecPreferences = function(codecs) {
// Use the same codecs as desktop
codecs = [
{
'clockRate': 90000,
'mimeType': 'video/H264',
'sdpFmtpLine': 'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d001f',
},
{
'clockRate': 90000,
'mimeType': 'video/H264',
'sdpFmtpLine': 'level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=4d001f',
const newCodecs = codecs.slice();
newCodecs.forEach((codec, i) => {
// Find high quality codecs
if (codec.sdpFmtpLine && codec.sdpFmtpLine.includes('profile-level-id=4d')) {
// Move it to the top of the array
newCodecs.splice(i, 1);
newCodecs.unshift(codec);
}
].concat(codecs);
});
try {
this.orgSetCodecPreferences(newCodecs);
} catch (e) {
console.log(e);
this.orgSetCodecPreferences(codecs);
}
}
}
@@ -1131,3 +1274,44 @@ window.onload = () => {
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(':');
}
return this.orgAddIceCandidate.apply(this, args);
}