mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 02:51:10 +00:00
Xbox Cloud Gaming: stream from the store and Games tab, native player with DroidDeck frame generation Stream Xbox games with Xbox Cloud Gaming (Game Pass) from DroidDeck, outside the Linux session. - Backend: `xodus-cli cloud-token` (tools/xbox/droiddeck-cloud-token.patch) gets the streaming tokens; they stay in memory. - Web player (default): the Greenlight player page (tools/xcloud-player, MIT) in a WebView, plus Microsoft's own xbox.com/play player. In-stream menu with screen effects (sharpen, colour, deband, FXAA, CRT, NTSC, Toon, fps cap), stream quality, performance overlay (level, layout, top right / top left) and rumble. - Native player (opt-in, Android 10+, Player engine: Native): Google's WebRTC (io.github.webrtc-sdk 125.6422.07, BSD) with the Greenlight protocol ported to Kotlin, in its own process (:xcloudnative) with its own instance of DroidDeck's compositor. Decoded frames reach the compositor as AHardwareBuffers (new stream mode, VK_ANDROID_external_memory_android_ hardware_buffer import), so DDFG 2x and LSFG 2x/3x/4x run on them. Live frame generation, frame-rate target (presets and a 10-165 fps slider, even pacing), zero-copy picture, small network buffer, fast decoder, instant controller input, overlay with engine and on-screen fps, background end, restart and web fallback. It takes the main process's tokens through XboxCloudTokenService; they are fetched ahead while a cloud game is shown. - The compositor requests the dma-buf import extensions only where the driver has them (the system driver has none). - Catalog: cloud availability, Game Pass plan CFQ7TTC0K6L8 counted, library filter All / Owned / Game Pass / Cloud, Stream and Add to Games on the store page, stream-only Games-tab entries (Stream + Download / Play + Stream). - Xbox games get VC++ 2015-2022 and the ARM64 runtime registry entries (vcredist2022_arm64, built in until the catalog has it) before each launch. - Controller focus reaches pickers opened from a game page (install location, uninstall); Add / Remove from Games keep the focus. Credits: Greenlight by UnknownSKL (Jim Kroon), MIT, player and protocol; Better xCloud for the resolution / OS mapping; MGamer404's Bannerlator #627 for the ARM64 runtime approach. Ported from the412banner's Bannerlator.
107 lines
3.3 KiB
TypeScript
107 lines
3.3 KiB
TypeScript
import xCloudPlayer from '../player'
|
|
|
|
export default class Stats {
|
|
private _player: xCloudPlayer
|
|
private _peerConnection:RTCPeerConnection
|
|
|
|
_videoCodec:string = ''
|
|
_audioCodec:string = ''
|
|
|
|
_videoWidth:number = 0
|
|
_videoHeight:number = 0
|
|
_videoFps:number = 0
|
|
_rtt:number = 0.0
|
|
|
|
_remoteHost:string = ''
|
|
_remotePort:number = 0
|
|
_remoteIsLocal = false
|
|
_remoteIsIpv6 = false
|
|
_activeRemoteCandidate:string = ''
|
|
|
|
constructor(player:xCloudPlayer) {
|
|
this._player = player
|
|
this._peerConnection = player._peerConnection
|
|
|
|
setInterval(this.loop.bind(this), 1000)
|
|
}
|
|
|
|
loop(){
|
|
this._peerConnection.getStats().then((stats) => {
|
|
stats.forEach((report) => {
|
|
// @TODO: Implement chat stats using type outbound-rtp
|
|
|
|
if(report.type === 'inbound-rtp'){
|
|
this.readInboundRtp(report)
|
|
|
|
} else if(report.type === 'candidate-pair' && report.packetsReceived !== undefined && report.packetsReceived > 0){
|
|
this.readCandidatePair(report)
|
|
|
|
} else if(report.type === 'remote-candidate'){
|
|
this.readRemoteCandidate(report)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
readInboundRtp(report:RTCInboundRtpStreamStats){
|
|
if(report.kind === 'video' && report.codecId && report.packetsReceived !== undefined && report.packetsReceived > 0){
|
|
|
|
if(report.codecId.includes('profile-level-id=4d')){
|
|
this._videoCodec = 'H264 (High)'
|
|
|
|
} else if(report.codecId.includes('profile-level-id=42e')) {
|
|
this._videoCodec = 'H264 (Normal)'
|
|
|
|
} else if(report.codecId.includes('profile-level-id=420')) {
|
|
this._videoCodec = 'H264 (Low)'
|
|
|
|
} else {
|
|
this._videoCodec = 'Other ('+report.codecId+')'
|
|
}
|
|
|
|
if(report.frameWidth){ this._videoWidth = report.frameWidth }
|
|
if(report.frameHeight){ this._videoHeight = report.frameHeight }
|
|
if(report.framesPerSecond){ this._videoFps = report.framesPerSecond }
|
|
// report.jitter
|
|
|
|
} else if(report.kind === 'audio' && report.codecId && report.packetsReceived !== undefined && report.packetsReceived > 0){
|
|
this._audioCodec = report.codecId
|
|
|
|
// report.jitter
|
|
}
|
|
}
|
|
|
|
readCandidatePair(report){
|
|
this._activeRemoteCandidate = report.remoteCandidateId
|
|
this._rtt = report.currentRoundTripTime
|
|
}
|
|
|
|
readRemoteCandidate(report){
|
|
if(report.id !== this._activeRemoteCandidate) {return}
|
|
|
|
if(this.isPrivateIP(report.address)){
|
|
this._remoteIsLocal = true
|
|
} else {
|
|
this._remoteIsLocal = false
|
|
}
|
|
|
|
if(this.isIpv6(report.address)){
|
|
this._remoteIsIpv6 = true
|
|
} else {
|
|
this._remoteIsIpv6 = false
|
|
}
|
|
|
|
this._remoteHost = report.address
|
|
this._remotePort = report.port
|
|
}
|
|
|
|
isPrivateIP(ip) {
|
|
const parts = ip.split('.')
|
|
return parts[0] === '10' || (parts[0] === '172' && (parseInt(parts[1], 10) >= 16 && parseInt(parts[1], 10) <= 31)) || (parts[0] === '192' && parts[1] === '168')
|
|
}
|
|
|
|
isIpv6(ip) {
|
|
const parts = ip.split(':')
|
|
return parts.length > 3 ? true : false
|
|
}
|
|
} |