mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 19:11:08 +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.
118 lines
4.9 KiB
TypeScript
118 lines
4.9 KiB
TypeScript
import xCloudPlayer from '../player'
|
|
import VideoComponent from './video'
|
|
import WebGPUComponent from './webgpu'
|
|
|
|
interface OverlayInterface {
|
|
debug: undefined | HTMLElement;
|
|
}
|
|
|
|
export default class Overlay {
|
|
private _player:xCloudPlayer
|
|
private _videoComponent:VideoComponent | WebGPUComponent
|
|
|
|
private _overlays:OverlayInterface = {
|
|
debug: undefined,
|
|
}
|
|
|
|
constructor(videoComponent:VideoComponent | WebGPUComponent, player:xCloudPlayer){
|
|
this._videoComponent = videoComponent
|
|
this._player = player
|
|
}
|
|
|
|
toggleDebug(){
|
|
if(this._overlays.debug === undefined){
|
|
this.createDebugOverlay()
|
|
} else {
|
|
this.destroy()
|
|
}
|
|
}
|
|
|
|
createDebugOverlay(){
|
|
this._overlays.debug = document.createElement('div')
|
|
this._overlays.debug.id = 'playerDebugOverlay'
|
|
|
|
this._overlays.debug.style.position = 'absolute'
|
|
this._overlays.debug.style.top = '0'
|
|
this._overlays.debug.style.left = '0'
|
|
this._overlays.debug.style.width = this._videoComponent.getElement()?.style.width || '100%'
|
|
this._overlays.debug.style.height = this._videoComponent.getElement()?.style.height || 'auto'
|
|
this._overlays.debug.style.background = 'linear-gradient(0deg, rgba(0,0,0, 0) 0%, rgba(0,0,0, 0) 75%, rgba(0,0,0, 0.5) 100%)'
|
|
this._overlays.debug.style.padding = '10px'
|
|
this._overlays.debug.style.pointerEvents = 'none'
|
|
|
|
document.getElementById(this._player.getElementId())?.appendChild(this._overlays.debug)
|
|
|
|
setTimeout(() => {
|
|
this.refreshDebugOverlay()
|
|
}, 500)
|
|
this.refreshDebugOverlay()
|
|
}
|
|
|
|
destroy(){
|
|
this._overlays.debug?.remove()
|
|
this._overlays.debug = undefined
|
|
}
|
|
|
|
refreshDebugOverlay(){
|
|
if(this._overlays.debug !== undefined){
|
|
this._overlays.debug.innerHTML = ''
|
|
|
|
this._overlays.debug.appendChild(this.createLabel('Local Play', this._player.getStats()._remoteIsLocal ? 'Local' : 'Remote', 'ok'))
|
|
this._overlays.debug.appendChild(this.createLabel('Resolution', this._player.getStats()._videoWidth+'x'+this._player.getStats()._videoHeight, 'ok'))
|
|
this._overlays.debug.appendChild(this.createLabel('Stream FPS', this._player.getStats()._videoFps.toString(), this._player.getStats()._videoFps >= 58 ? 'ok' : 'warning'))
|
|
|
|
// Display rendering FPS if available
|
|
const frameStats = (this._videoComponent as any).getFrameStats?.();
|
|
if (frameStats) {
|
|
this._overlays.debug.appendChild(this.createLabel('Render FPS', frameStats.renderingFps.toString(), frameStats.renderingFps >= 58 ? 'ok' : 'warning'))
|
|
this._overlays.debug.appendChild(this.createLabel('Render Delay', frameStats.renderDelayMs.toString() + ' ms', frameStats.renderDelayMs < 16 ? 'ok' : frameStats.renderDelayMs < 33 ? 'warning' : 'error'))
|
|
this._overlays.debug.appendChild(this.createLabel('Dropped Frames', frameStats.droppedFrames.toString(), frameStats.droppedFrames === 0 ? 'ok' : frameStats.droppedFrames < 30 ? 'warning' : 'error'))
|
|
}
|
|
|
|
this._overlays.debug.appendChild(this.createLabel('Connection', this._player.getStats()._remoteIsIpv6 ? 'IPv6' : 'IPv4', 'ok'))
|
|
this._overlays.debug.appendChild(this.createLabel('Video', this._player.getStats()._videoCodec, 'ok'))
|
|
const rttButtonState = (this._player.getStats()._rtt*1000 > 75) ? 'error' : (this._player.getStats()._rtt*1000 > 40) ? 'warning' : 'ok'
|
|
this._overlays.debug.appendChild(this.createLabel('RTT', (this._player.getStats()._rtt*1000).toString()+' ms', rttButtonState))
|
|
|
|
setTimeout(() => {
|
|
this.refreshDebugOverlay()
|
|
}, 500)
|
|
}
|
|
}
|
|
|
|
createLabel(text:string, value:string = '', style:string = ''){
|
|
const labelStyle = {
|
|
borderRadius: '5px',
|
|
background: 'linear-gradient(0deg, rgba(28,26,26,1) 0%, rgba(47,45,45,1) 100%)',
|
|
padding: '10px',
|
|
fontSize: '12px',
|
|
marginTop: '3px',
|
|
marginRight: '10px',
|
|
marginBottom: '2px',
|
|
textTransform: 'uppercase',
|
|
color: 'white',
|
|
display: 'inline-block',
|
|
}
|
|
|
|
const element = document.createElement('span')
|
|
|
|
const colors = {
|
|
'error': 'rgb(220, 53, 69)',
|
|
'warning': 'rgb(255, 193, 7)',
|
|
'ok': 'rgb(25, 135, 84)',
|
|
}
|
|
|
|
if(value !== ''){
|
|
element.innerHTML = `${text}: <span style="background: ${colors[style]}; border-radius: 5px; padding: 4px; padding-left: 8px; padding-right: 8px;">${value}</span>`
|
|
} else {
|
|
element.innerText = text
|
|
}
|
|
|
|
|
|
for(const key in labelStyle){
|
|
element.style[key] = labelStyle[key]
|
|
}
|
|
return element
|
|
}
|
|
|
|
} |