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.
143 lines
5.2 KiB
TypeScript
143 lines
5.2 KiB
TypeScript
import xCloudPlayer from '../player'
|
|
|
|
export default class MouseKeyboard {
|
|
private _player: xCloudPlayer | undefined
|
|
private _index: number
|
|
|
|
private _listener = {
|
|
keyDown: this.onKeyDown.bind(this),
|
|
keyUp: this.onKeyUp.bind(this),
|
|
pointerMove: this.onPointerMove.bind(this),
|
|
pointerClick: this.onPointerClick.bind(this),
|
|
pointerScroll: this.onPointerScroll.bind(this),
|
|
lockMouseKeyboard: this.lockMouseKeyboard.bind(this),
|
|
}
|
|
|
|
|
|
constructor(index:number){
|
|
this._index = index
|
|
}
|
|
|
|
attach(xCloudPlayer:xCloudPlayer){
|
|
this._player = xCloudPlayer
|
|
|
|
this._player._channels.control.sendGamepadState(this._index, true, this)
|
|
|
|
// Mod the video UI to capture MKB input
|
|
if(this._player.getVideoElement() === undefined){
|
|
console.log('[MKB] VideoComponent is not attached. this._player._videoComponent is:', this._player.getVideoElement())
|
|
return
|
|
} else {
|
|
this._player.getVideoElement()?.addEventListener('pointermove', this._listener.pointerMove)
|
|
this._player.getVideoElement()?.addEventListener('pointerdown', this._listener.pointerClick)
|
|
this._player.getVideoElement()?.addEventListener('pointerup', this._listener.pointerClick)
|
|
this._player.getVideoElement()?.addEventListener('wheel', this._listener.pointerScroll)
|
|
this._player.getVideoElement()?.addEventListener('click', this._listener.lockMouseKeyboard)
|
|
|
|
window.addEventListener('keydown', this._listener.keyDown)
|
|
window.addEventListener('keyup', this._listener.keyUp)
|
|
|
|
this._player.getVideoElement()?.addEventListener('contextmenu', event => {
|
|
event.preventDefault()
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
detach(){
|
|
if(this._player === undefined){
|
|
console.log('[MKB] Player is not attached. this._player is:', this._player)
|
|
return
|
|
}
|
|
|
|
this._player._channels.control.sendGamepadState(this._index, false)
|
|
|
|
window.removeEventListener('keydown', this._listener.keyDown)
|
|
window.removeEventListener('keyup', this._listener.keyUp)
|
|
|
|
this._player.getVideoElement()?.removeEventListener('pointermove', this._listener.pointerMove)
|
|
this._player.getVideoElement()?.removeEventListener('pointerdown', this._listener.pointerClick)
|
|
this._player.getVideoElement()?.removeEventListener('pointerup', this._listener.pointerClick)
|
|
this._player.getVideoElement()?.removeEventListener('wheel', this._listener.pointerScroll)
|
|
this._player.getVideoElement()?.removeEventListener('click', this._listener.lockMouseKeyboard)
|
|
}
|
|
|
|
onPointerMove(event:PointerEvent){
|
|
this._player?._channels.input.queueMouseFrame({
|
|
X: event.movementX*2,
|
|
Y: event.movementY*2,
|
|
WheelX: 0,
|
|
WheelY: 0,
|
|
Buttons: event.buttons,
|
|
Relative: 0, // 0 = Relative, 1 = Absolute
|
|
})
|
|
}
|
|
|
|
onPointerClick(event:PointerEvent){
|
|
// console.log('Pointer clicked:', event)
|
|
this._player?._channels.input.queueMouseFrame({
|
|
X: event.movementX*2,
|
|
Y: event.movementY*2,
|
|
WheelX: 0,
|
|
WheelY: 0,
|
|
Buttons: event.buttons,
|
|
Relative: 0, // 0 = Relative, 1 = Absolute
|
|
})
|
|
}
|
|
|
|
onPointerScroll(event:WheelEvent){
|
|
console.log('[MKB] Pointer scrolled:', event)
|
|
}
|
|
|
|
onKeyDown(event:KeyboardEvent){
|
|
this._player?._channels.input.queueKeyboardFrame({
|
|
key: event.key,
|
|
keyCode: event.keyCode,
|
|
pressed: true,
|
|
})
|
|
event.preventDefault()
|
|
}
|
|
|
|
onKeyUp(event:KeyboardEvent){
|
|
this._player?._channels.input.queueKeyboardFrame({
|
|
key: event.key,
|
|
keyCode: event.keyCode,
|
|
pressed: false,
|
|
})
|
|
}
|
|
|
|
lockMouseKeyboard(){
|
|
if(this._player === undefined || this._player.getVideoElement() === undefined){
|
|
console.log('[MKB] Player or VideoComponent is not attached. Failed to lock Mouse & Keyboard because there is nothing to lock for.')
|
|
return
|
|
}
|
|
|
|
// @disable ts-expect-error requestPointerLock() is not defined to consume arguments
|
|
const promise = this._player?.getVideoElement()?.requestPointerLock({
|
|
unadjustedMovement: true,
|
|
}) as Promise<undefined> | undefined
|
|
|
|
if(promise === undefined){
|
|
console.log('[MKB] Failed to lock Mouse & Keyboard because the requestPointerLock() is undefined')
|
|
return
|
|
}
|
|
|
|
if ('keyboard' in navigator && 'lock' in (navigator.keyboard as any)) {
|
|
document.body.requestFullscreen().then(() => {
|
|
(navigator as any).keyboard.lock()
|
|
})
|
|
} else {
|
|
console.log('[MKB] Keyboard lock is not supported. Error:', 'NotSupportedError')
|
|
return
|
|
}
|
|
|
|
promise.then(() => {
|
|
console.log('[MKB] pointer is locked')
|
|
}).catch((error) => {
|
|
if (error.name === 'NotSupportedError') {
|
|
console.log('[MKB] Mouse pointer lock is not supported. Error:', 'NotSupportedError')
|
|
}
|
|
})
|
|
}
|
|
|
|
} |