mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 11:01:07 +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.
69 lines
4.0 KiB
TypeScript
69 lines
4.0 KiB
TypeScript
// Performance overlay in the top-right or top-left corner, from WebRTC getStats. Levels: 1 fps; 2 + ping, resolution;
|
|
// 3 + bitrate, decode time, packet loss; 4 + codec, jitter, dropped frames, server. Horizontal or vertical.
|
|
|
|
export class StreamStats {
|
|
private element: HTMLDivElement
|
|
private timer: ReturnType<typeof setInterval> | undefined
|
|
private level = 0
|
|
private vertical = false
|
|
private left = false
|
|
private server = ''
|
|
private last = { bytes: 0, time: 0, decode: 0, frames: 0, lost: 0, received: 0 }
|
|
|
|
constructor(private pc: RTCPeerConnection) {
|
|
this.element = document.createElement('div')
|
|
Object.assign(this.element.style, {
|
|
position: 'absolute', top: '8px', right: '8px', zIndex: '3', padding: '4px 8px', borderRadius: '6px',
|
|
background: 'rgba(0,0,0,0.55)', color: '#e8e8e8', font: '12px monospace', lineHeight: '16px',
|
|
pointerEvents: 'none', display: 'none', whiteSpace: 'pre', textAlign: 'right',
|
|
})
|
|
document.body.appendChild(this.element)
|
|
}
|
|
|
|
show(level: number, vertical: boolean, server: string, left = false) {
|
|
this.level = level; this.vertical = vertical; this.server = server; this.left = left
|
|
// Top left keeps clear of the clock Xbox draws top right while its guide is open.
|
|
Object.assign(this.element.style, left ? { left: '8px', right: '', textAlign: 'left' } : { left: '', right: '8px', textAlign: 'right' })
|
|
this.element.style.display = level > 0 ? 'block' : 'none'
|
|
if (level > 0 && this.timer === undefined) { this.timer = setInterval(() => this.update(), 1000); this.update() }
|
|
if (level === 0 && this.timer !== undefined) { clearInterval(this.timer); this.timer = undefined }
|
|
}
|
|
|
|
private async update() {
|
|
const report = await this.pc.getStats()
|
|
let video: any, pair: any
|
|
const codecs = new Map<string, any>()
|
|
report.forEach((s: any) => {
|
|
if (s.type === 'inbound-rtp' && s.kind === 'video') video = s
|
|
else if (s.type === 'candidate-pair' && s.state === 'succeeded' && s.nominated) pair = s
|
|
else if (s.type === 'codec') codecs.set(s.id, s)
|
|
})
|
|
if (video === undefined) return
|
|
const seconds = this.last.time > 0 ? (video.timestamp - this.last.time) / 1000 : 0
|
|
const mbps = seconds > 0 ? (video.bytesReceived - this.last.bytes) * 8 / seconds / 1e6 : 0
|
|
const frames = (video.framesDecoded ?? 0) - this.last.frames
|
|
const decodeMs = frames > 0 ? ((video.totalDecodeTime ?? 0) - this.last.decode) * 1000 / frames : 0
|
|
const lost = (video.packetsLost ?? 0) - this.last.lost
|
|
const received = (video.packetsReceived ?? 0) - this.last.received
|
|
const loss = lost + received > 0 ? lost * 100 / (lost + received) : 0
|
|
this.last = { bytes: video.bytesReceived, time: video.timestamp, decode: video.totalDecodeTime ?? 0,
|
|
frames: video.framesDecoded ?? 0, lost: video.packetsLost ?? 0, received: video.packetsReceived ?? 0 }
|
|
|
|
const items: string[] = ['web', `${Math.round(video.framesPerSecond ?? 0)} fps`]
|
|
if (this.level >= 2) {
|
|
if (pair?.currentRoundTripTime !== undefined) items.push(`ping ${Math.round(pair.currentRoundTripTime * 1000)} ms`)
|
|
items.push(`${video.frameWidth ?? '?'}x${video.frameHeight ?? '?'}`)
|
|
}
|
|
if (this.level >= 3) items.push(`${mbps.toFixed(1)} Mb/s`, `decode ${decodeMs.toFixed(1)} ms`, `loss ${loss.toFixed(1)}%`)
|
|
if (this.level >= 4) {
|
|
const codec = codecs.get(video.codecId)
|
|
if (codec) items.push(String(codec.mimeType ?? '').replace('video/', ''))
|
|
items.push(`jitter ${Math.round((video.jitter ?? 0) * 1000)} ms`, `dropped ${video.framesDropped ?? 0}`)
|
|
if (this.server) items.push(this.server)
|
|
}
|
|
this.element.textContent = items.join(this.vertical ? '\n' : ' · ')
|
|
}
|
|
|
|
destroy() { this.show(0, this.vertical, this.server, this.left); this.element.remove() }
|
|
}
|