mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-12 03:21: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.
142 lines
4.5 KiB
TypeScript
142 lines
4.5 KiB
TypeScript
import Channel from './channel'
|
|
import InputPacket, { MetadataFrame, ReportTypes, InputFrame as GamepadFrame, MouseFrame, KeyboardFrame, PointerFrame } from './input/packet'
|
|
import InputQueue from './input/queue'
|
|
|
|
import Gamepad from '../input/gamepad'
|
|
|
|
export interface VibrationFrame {
|
|
gamepadIndex: number;
|
|
leftMotorPercent: number;
|
|
rightMotorPercent: number;
|
|
leftTriggerMotorPercent: number;
|
|
rightTriggerMotorPercent: number;
|
|
durationMs: number;
|
|
delayMs: number;
|
|
repeat: number;
|
|
}
|
|
|
|
export default class InputChannel extends Channel {
|
|
private _serverVideoWidth = 0
|
|
private _serverVideoHeight = 0
|
|
|
|
private _inputQueue = new InputQueue(this.getPlayer())
|
|
|
|
getChannelName() {
|
|
return 'input'
|
|
}
|
|
|
|
getChannelConfig() {
|
|
return {
|
|
ordered: true,
|
|
protocol: '1.0',
|
|
}
|
|
}
|
|
|
|
getServerVideoWidth(){
|
|
return this._serverVideoWidth
|
|
}
|
|
|
|
getServerVideoHeight(){
|
|
return this._serverVideoHeight
|
|
}
|
|
|
|
start(){
|
|
const Packet = new InputPacket(0)
|
|
Packet.setMetadata(navigator.maxTouchPoints > 1 ? navigator.maxTouchPoints : 1)
|
|
this.send(Packet.toBuffer())
|
|
|
|
console.log('[InputChannel] Sent metadata:', Packet, Packet.toBuffer())
|
|
|
|
setTimeout(this.gamepadStateLoop.bind(this), 16)
|
|
}
|
|
|
|
onMessage(event: MessageEvent<any>) {
|
|
const dataView = new DataView(event.data)
|
|
const reportType = dataView.getUint8(0)
|
|
|
|
console.log('[InputChannel] received:', dataView)
|
|
|
|
if(reportType === ReportTypes.Vibration){
|
|
console.log('Received a vibration report')
|
|
dataView.getUint8(2) // rumbleType: 0 = FourMotorRumble
|
|
const gamepadIndex = dataView.getUint8(3)
|
|
|
|
const gamepad = this.getPlayer()._channels.control.getGamepadHandler(gamepadIndex)
|
|
|
|
if(gamepad !== undefined){
|
|
const leftMotorPercent = dataView.getUint8(4) / 100
|
|
const rightMotorPercent = dataView.getUint8(5) / 100
|
|
const leftTriggerMotorPercent = dataView.getUint8(6) / 100
|
|
const rightTriggerMotorPercent = dataView.getUint8(7) / 100
|
|
const durationMs = dataView.getUint16(8, true)
|
|
const delayMs = dataView.getUint16(10, true)
|
|
const repeat = dataView.getUint8(12)
|
|
|
|
const rumble:VibrationFrame = {
|
|
gamepadIndex,
|
|
leftMotorPercent,
|
|
rightMotorPercent,
|
|
leftTriggerMotorPercent,
|
|
rightTriggerMotorPercent,
|
|
durationMs,
|
|
delayMs,
|
|
repeat,
|
|
}
|
|
|
|
gamepad.handleVibration(rumble)
|
|
} else {
|
|
console.log('[InputChannel] Received a vibration report but no gamepad handler is available')
|
|
}
|
|
|
|
} else if(reportType === ReportTypes.ServerMetadata){
|
|
console.log('[InputChannel] Received server video dimensions:', dataView.getUint32(2, true), dataView.getUint32(6, true))
|
|
this._serverVideoHeight = dataView.getUint32(2, true)
|
|
this._serverVideoWidth = dataView.getUint32(6, true)
|
|
}
|
|
|
|
}
|
|
|
|
gamepadStateLoop(){
|
|
const gamepadHandlers = this.getPlayer()._channels.control.getGamepadHandlers()
|
|
// console.log('[InputChannel] gamepadStateLoop() called', gamepadHandlers)
|
|
const gamepadFrames:Array<GamepadFrame> = []
|
|
for(const gamepad in gamepadHandlers){
|
|
if(gamepadHandlers[gamepad] instanceof Gamepad){
|
|
const frame = (gamepadHandlers[gamepad] as Gamepad).getGamepadState()
|
|
if(frame !== undefined){
|
|
gamepadFrames.push(frame)
|
|
}
|
|
}
|
|
}
|
|
|
|
if(gamepadFrames.length > 0){
|
|
this.queueGamepadFrames(gamepadFrames)
|
|
}
|
|
|
|
setTimeout(this.gamepadStateLoop.bind(this), 16)
|
|
}
|
|
|
|
queueMetadataFrame(data:MetadataFrame){
|
|
return this._inputQueue.queueMetadataFrame(data)
|
|
}
|
|
|
|
queueGamepadFrames(frames:Array<GamepadFrame>, forceSend = false){
|
|
this._inputQueue.queueGamepadFrames(frames, forceSend)
|
|
}
|
|
|
|
queueMouseFrame(data:MouseFrame){
|
|
this._inputQueue.queueMouseFrame(data)
|
|
}
|
|
|
|
queueKeyboardFrame(data:KeyboardFrame){
|
|
this._inputQueue.queueKeyboardFrame(data)
|
|
}
|
|
|
|
queuePointerFrame(data:PointerFrame){
|
|
this._inputQueue.queuePointerFrame(data)
|
|
}
|
|
|
|
destroy() {
|
|
// console.log('DebugChannel destroy() called')
|
|
}
|
|
} |