Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 32 additions & 5 deletions entry/src/main/ets/pages/StreamPage.ets
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ struct StreamPage {
// 使用 DisplaySync 直接向系统合成器请求高刷新率,
// 解决无触摸时 ArkUI .onMouse 降至 ~30Hz 的问题
private mouseDisplaySync: displaySync.DisplaySync | null = null;
private mouseInputSuspended: boolean = false;

// ── 解耦的处理器/管理器 ──
private inputHandler: StreamInputHandler = new StreamInputHandler();
Expand Down Expand Up @@ -328,6 +329,15 @@ struct StreamPage {
// 初始化窗口管理器
this.windowManager = new StreamWindowManager(this.context);
this.windowManager.setOnWindowSizeChanged(() => this.recalculateXComponentSize());
this.windowManager.setOnWindowFocusChanged((focused: boolean) => {
if (!this.viewModel.isConnected) return;
if (!focused) {
this.inputHandler.stopMouseInterceptor();
this.stopMouseKeepAlive();
} else if (!this.mouseInputSuspended) {
this.resumeMouseInterceptor();
}
});

// 注册 Network Boost 网络场景监听(弱信号/拥塞 提示)
this.netSceneListener = (scene: SystemNetScene): void => {
Expand Down Expand Up @@ -843,7 +853,7 @@ struct StreamPage {
private buildGameMenuCallbacks(): GameMenuCallbacks {
return {
onDismiss: () => {
this.resumeMouseInterceptor();
this.handleGameMenuDismissed();
},
onDisconnect: () => {
this.gameMenuDialogController?.close();
Expand Down Expand Up @@ -962,13 +972,21 @@ struct StreamPage {
};
}

private handleGameMenuDismissed(): void {
this.gameMenuDialogController = null;
if (!this.mouseInputSuspended) return;
this.mouseInputSuspended = false;
this.resumeMouseInterceptor();
}

/**
* 显示串流菜单
*/
showStreamMenu(): void {
if (!this.menuManager) return;

// 暂停鼠标全速轮询(Monitor 暂停 + DisplaySync 停止)
this.mouseInputSuspended = true;
this.inputHandler.stopMouseInterceptor();
this.stopMouseKeepAlive();

Expand All @@ -990,7 +1008,11 @@ struct StreamPage {
}),
alignment: DialogAlignment.CenterEnd,
customStyle: true,
backgroundColor: Color.Transparent
backgroundColor: Color.Transparent,
onWillDismiss: (action: DismissDialogAction): void => {
action.dismiss();
this.handleGameMenuDismissed();
}
});
this.gameMenuDialogController.open();
}
Expand Down Expand Up @@ -1117,10 +1139,13 @@ struct StreamPage {
* 恢复鼠标全速轮询(对话框关闭后调用)
*/
private resumeMouseInterceptor(): void {
if (!this.viewModel.isConnected) return;
if (!this.viewModel.isConnected || this.mouseInputSuspended) return;
this.stopMouseKeepAlive();
const monitorOk = this.inputHandler.startMouseInterceptor(
this.getXComponentWidthVp(),
this.getXComponentHeightVp()
this.getXComponentHeightVp(),
this.windowManager?.windowId ?? -1,
this.windowManager?.isFullscreen ?? false
);
if (!monitorOk) {
this.startMouseKeepAlive();
Expand Down Expand Up @@ -1455,7 +1480,9 @@ struct StreamPage {
// 启动鼠标全速轮询:优先 C++ Monitor,不可用时 DisplaySync 兜底
const monitorOk = this.inputHandler.startMouseInterceptor(
this.getXComponentWidthVp(),
this.getXComponentHeightVp()
this.getXComponentHeightVp(),
this.windowManager?.windowId ?? -1,
this.windowManager?.isFullscreen ?? false
);
if (!monitorOk) {
this.startMouseKeepAlive();
Expand Down
45 changes: 41 additions & 4 deletions entry/src/main/ets/service/input/InputInterceptorService.ets
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ interface MouseInterceptorNative {
screenWidth: number, screenHeight: number,
relativeMode: boolean): void;
isMouseInterceptorActive(): boolean;
isCursorLockAvailable(): boolean;
lockCursor(windowId: number, followMovement: boolean): number;
unlockCursor(windowId: number): number;
}

/**
Expand Down Expand Up @@ -149,6 +152,7 @@ export class MouseInterceptorService {
private static instance: MouseInterceptorService;
private native: MouseInterceptorNative | null;
private active: boolean = false;
private lockedWindowId: number = -1;

private constructor() {
this.native = (nativeLib as Record<string, Object>)['MouseInterceptor'] as MouseInterceptorNative;
Expand Down Expand Up @@ -207,10 +211,13 @@ export class MouseInterceptorService {
* 停止鼠标拦截器
*/
stop(): void {
if (!this.active || !this.native) return;
this.native.removeMouseInterceptor();
this.active = false;
console.info('MouseInterceptor: 鼠标拦截器已停止');
if (!this.native) return;
if (this.active) {
this.native.removeMouseInterceptor();
this.active = false;
console.info('MouseInterceptor: 鼠标拦截器已停止');
}
this.unlockCursor();
}

isActive(): boolean {
Expand All @@ -219,4 +226,34 @@ export class MouseInterceptorService {
}
return false;
}

isCursorLockAvailable(): boolean {
return this.native?.isCursorLockAvailable() ?? false;
}

lockCursor(windowId: number, followMovement: boolean): number {
if (!this.native || windowId < 0) return 801;
if (this.lockedWindowId >= 0 && this.lockedWindowId !== windowId) {
this.unlockCursor();
}
const result = this.native.lockCursor(windowId, followMovement);
if (result === 0) {
this.lockedWindowId = windowId;
console.info(`MouseInterceptor: 光标锁定成功 (windowId=${windowId}, followMovement=${followMovement})`);
}
return result;
}

unlockCursor(): number {
if (!this.native || this.lockedWindowId < 0) return 0;
const windowId = this.lockedWindowId;
const result = this.native.unlockCursor(windowId);
if (result === 0) {
this.lockedWindowId = -1;
console.info(`MouseInterceptor: 光标已释放 (windowId=${windowId})`);
} else {
console.warn(`MouseInterceptor: 光标释放失败 (windowId=${windowId}, error=${result})`);
}
return result;
}
}
32 changes: 25 additions & 7 deletions entry/src/main/ets/service/streaming/StreamInputHandler.ets
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,16 @@ export class StreamInputHandler {
*
* @param viewWidth 视图宽度 (vp)
* @param viewHeight 视图高度 (vp)
* @param windowId 主窗口 ID
* @param isFullscreen 是否处于全屏模式
* @returns true 表示 C++ Monitor 已启动(不需要 keepalive),
* false 表示 Monitor 不可用(调用方应启用 keepalive 动画来保持 ArkUI 帧率)
* false 表示应启用 keepalive,由 ArkUI 处理鼠标事件
*/
startMouseInterceptor(viewWidth: number, viewHeight: number): boolean {
startMouseInterceptor(viewWidth: number, viewHeight: number,
windowId: number, isFullscreen: boolean): boolean {
const mouseInterceptor = MouseInterceptorService.getInstance();
mouseInterceptor.stop();
this.mouseInterceptorActive = false;

// 保存 XComponent 实际尺寸,供 ArkUI 回退路径绝对模式使用
this.viewWidthVp = viewWidth;
Expand All @@ -187,6 +192,21 @@ export class StreamInputHandler {
// 读取游戏鼠标模式设置
const relativeMode = this.viewModel?.inputSettings?.gameMouseMode ?? false;

// API 22+ 全屏窗口使用系统光标锁定。相对模式锁定后坐标保持不变,
// 必须由 ArkUI rawDelta 处理移动,不能再用 C++ Monitor 计算坐标差。
let cursorLocked = false;
if (isFullscreen && windowId >= 0 && mouseInterceptor.isCursorLockAvailable()) {
const lockResult = mouseInterceptor.lockCursor(windowId, !relativeMode);
cursorLocked = lockResult === 0;
if (!cursorLocked) {
console.info(`StreamInputHandler: 光标锁定不可用 (${lockResult}),保留原有鼠标处理`);
}
}
if (relativeMode && cursorLocked) {
console.info('StreamInputHandler: 游戏鼠标模式已锁定光标,使用 ArkUI rawDelta');
return false;
}

// 初始使用屏幕尺寸(全屏时完全正确),窗口模式下异步获取实际窗口矩形更新
mouseInterceptor.configure(0, 0, screenWidthPx, screenHeightPx,
screenWidthPx, screenHeightPx, relativeMode);
Expand Down Expand Up @@ -255,7 +275,6 @@ export class StreamInputHandler {
* 停止鼠标高速轮询
*/
stopMouseInterceptor(): void {
if (!this.mouseInterceptorActive) return;
MouseInterceptorService.getInstance().stop();
this.mouseInterceptorActive = false;
}
Expand Down Expand Up @@ -285,10 +304,9 @@ export class StreamInputHandler {
if (event.action === MouseAction.Move) {
const relativeMode = this.viewModel?.inputSettings?.gameMouseMode ?? false;
if (relativeMode) {
// 相对模式(Monitor 不可用时回退):使用 rawDelta 发送相对位移
// rawDelta 为 vp 单位,转 px 后 ×1.5 补偿 ArkUI 事件频率损失
const dx = vp2px(event.rawDeltaX ?? 0) * 1.5;
const dy = vp2px(event.rawDeltaY ?? 0) * 1.5;
// rawDelta 是鼠标硬件上报的原始位移,不是 vp/px,直接转发给远端。
const dx = event.rawDeltaX ?? 0;
const dy = event.rawDeltaY ?? 0;
if (dx !== 0 || dy !== 0) {
MoonBridge.sendMouseMove(Math.round(dx), Math.round(dy));
}
Expand Down
40 changes: 40 additions & 0 deletions entry/src/main/ets/service/streaming/StreamWindowManager.ets
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,20 @@ export class StreamWindowManager {
private windowSizeChangeHandler: ((size: window.Size) => void) | null = null;
/** windowSizeChange 防抖定时器 */
private windowSizeDebounceTimer: number = -1;
/** 主窗口 ID,供 Native WindowManager API 使用 */
private mainWindowId: number = -1;
/** 主窗口焦点变化回调 */
private onWindowFocusChanged: ((focused: boolean) => void) | null = null;
private windowEventHandler: ((event: window.WindowEventType) => void) | null = null;

/** 查询当前是否全屏 */
get isFullscreen(): boolean {
return this._isFullscreen;
}

get windowId(): number {
return this.mainWindowId;
}

constructor(context: common.UIAbilityContext) {
this.context = context;
Expand All @@ -65,6 +74,10 @@ export class StreamWindowManager {
setOnWindowSizeChanged(callback: (() => void) | null): void {
this.onWindowSizeChanged = callback;
}

setOnWindowFocusChanged(callback: ((focused: boolean) => void) | null): void {
this.onWindowFocusChanged = callback;
}

/**
* 设置串流分辨率
Expand Down Expand Up @@ -109,6 +122,8 @@ export class StreamWindowManager {
async configureWindow(): Promise<void> {
try {
const win = await window.getLastWindow(this.context);
this.mainWindowId = win.getWindowProperties().id;
this.registerWindowEventListener(win);
win.setWindowKeepScreenOn(true).catch(() => {
// TODO: Implement error handling.
});
Expand All @@ -133,6 +148,7 @@ export class StreamWindowManager {
try {
const win = await window.getLastWindow(this.context);
this.unregisterWindowSizeListener(win);
this.unregisterWindowEventListener(win);
win.setWindowKeepScreenOn(false);
await this.exitImmersiveFullscreen(win);

Expand All @@ -146,6 +162,7 @@ export class StreamWindowManager {
win.setWindowBrightness(-1);

this._isFullscreen = false;
this.mainWindowId = -1;
console.info('StreamWindowManager: 已恢复窗口设置');
} catch (err) {
console.error('恢复窗口设置失败:', err);
Expand Down Expand Up @@ -216,6 +233,29 @@ export class StreamWindowManager {
}
}

private registerWindowEventListener(win: window.Window): void {
this.unregisterWindowEventListener(win);
this.windowEventHandler = (event: window.WindowEventType): void => {
if (event === window.WindowEventType.WINDOW_ACTIVE) {
this.onWindowFocusChanged?.(true);
} else if (event === window.WindowEventType.WINDOW_INACTIVE ||
event === window.WindowEventType.WINDOW_HIDDEN) {
this.onWindowFocusChanged?.(false);
}
};
try {
win.on('windowEvent', this.windowEventHandler);
} catch (_e) {}
}

private unregisterWindowEventListener(win: window.Window): void {
if (!this.windowEventHandler) return;
try {
win.off('windowEvent', this.windowEventHandler);
} catch (_e) {}
this.windowEventHandler = null;
}

/**
* 防抖通知布局重算。窗口 recover/maximize、用户拖拽和系统布局 settle
* 都从这里汇总,避免页面层用固定延时猜窗口何时稳定。
Expand Down
3 changes: 3 additions & 0 deletions entry/src/main/module.json5
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@
"abilities": ["EntryAbility"],
"when": "always"
}
},
{
"name": "ohos.permission.LOCK_WINDOW_CURSOR"
}
]
}
Expand Down
Loading
Loading