Skip to content
Open
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
46 changes: 38 additions & 8 deletions entry/src/main/ets/pages/StreamPage.ets
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,9 @@ struct StreamPage {
InputInterceptorService.getInstance().stop();
this.inputHandler.stopMouseInterceptor();
this.stopMouseKeepAlive();
try {
MoonBridge.setDisplayFramePacerEnabled(false);
} catch (_) {}

// 停止体感助手
GyroAssistService.getInstance().dispose();
Expand Down Expand Up @@ -1272,27 +1275,49 @@ struct StreamPage {
}

/**
* 设置 XComponent 帧率 + 获取 surfaceId + 启动串流
* 应用显示链路帧率提示。
* 120fps 串流时启动 native DisplaySoloist,并重申 XComponent / native 层帧率范围。
*/
private async launchStream(): Promise<void> {
const surfaceId = this.xComponentController.getXComponentSurfaceId();

// 设置 XComponent 期望帧率(通过 FrameNode → ArkUI_NodeHandle)
// 解决 MatePad 等设备渲染帧率被锁定在 60fps 的问题
private applyDisplayFrameRateHints(enablePacer: boolean = true): void {
const streamConfig = this.viewModel.streamConfig;
if (streamConfig && streamConfig.fps > 60) {
try {
if (enablePacer) {
MoonBridge.setDisplayFramePacerEnabled(true);
}
const frameNode = this.getUIContext().getFrameNodeById('streamSurface');
if (frameNode) {
MoonBridge.setXComponentFrameRate(frameNode, streamConfig.fps);
console.info(`XComponent 帧率已设置为 ${streamConfig.fps} fps`);
}
MoonBridge.refreshFrameRateHints();
} catch (e) {
console.warn(`设置 XComponent 帧率失败: ${e}`);
console.warn(`设置显示帧率提示失败: ${e}`);
}
} else if (enablePacer) {
try {
MoonBridge.setDisplayFramePacerEnabled(false);
} catch (_) {}
}
}

/**
* 设置 XComponent 帧率 + 获取 surfaceId + 启动串流
*/
private async launchStream(): Promise<void> {
const surfaceId = this.xComponentController.getXComponentSurfaceId();

// 设置 XComponent / DisplaySoloist 期望帧率,解决智能帧率下 120fps 回落到 60Hz 的问题
this.applyDisplayFrameRateHints(true);

await this.viewModel.startStreaming(this.computerId, this.appId, surfaceId, this.context, this.displayGuid, this.useVdd);
try {
await this.viewModel.startStreaming(this.computerId, this.appId, surfaceId, this.context, this.displayGuid, this.useVdd);
} catch (err) {
try {
MoonBridge.setDisplayFramePacerEnabled(false);
} catch (_) {}
throw err instanceof Error ? err : new Error(String(err));
}

Comment on lines 1277 to 1321

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

启动前先打开 pacer,但失败路径没有回滚。

launchStream() 在真正 startStreaming() 之前就启用了 DisplaySoloist/XComponent/native hints,而 startStreaming()catch 里只打印日志。这样只要连接或启动阶段抛错,页面停在错误态时高刷保持仍可能继续生效,直到用户离开页面才被动清理。

建议改法
   async startStreaming(): Promise<void> {
     try {
       ...
       await this.launchStream();
       this.streamStartedAtMs = Date.now();
       this.applyDisplayFrameRateHints(true);
       ...
     } catch (err) {
+      try {
+        MoonBridge.setDisplayFramePacerEnabled(false);
+      } catch (_) {}
       console.error('开始串流失败:', err);
     }
   }

Also applies to: 1381-1431

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@entry/src/main/ets/pages/StreamPage.ets` around lines 1277 - 1314,
launchStream enables DisplaySoloist/XComponent hints via
applyDisplayFrameRateHints(true) before awaiting viewModel.startStreaming, but
failures leave the pacer/hints enabled; change launchStream to wrap the await
this.viewModel.startStreaming(...) in a try/catch/finally (or at minimum catch)
and on error disable the pacer/hints by calling
applyDisplayFrameRateHints(false) (or directly call
MoonBridge.setDisplayFramePacerEnabled(false) and reset XComponent/frame hints)
so any startup exception cleans up state; keep the original logging/throw
behavior but ensure applyDisplayFrameRateHints(false) is invoked on the error
path (also apply same fix to the other launch path referenced around lines
1381-1431).

// 串流启动后调度未识别 USB 手柄检测(延迟内 GCK 完成扫描)
this.scheduleUnhandledUsbDetection();
Expand Down Expand Up @@ -1362,6 +1387,7 @@ struct StreamPage {
// 获取 surfaceId → 设置帧率 → 启动串流
await this.launchStream();
this.streamStartedAtMs = Date.now();
this.applyDisplayFrameRateHints(true);

// 串流就绪后再做 AI 服务可用性探测,避免与启动链路抢带宽
this.probeAiAvailableDeferred();
Expand Down Expand Up @@ -1458,6 +1484,9 @@ struct StreamPage {
this.inputHandler.resetState();
this.inputHandler.stopMouseInterceptor();
this.stopMouseKeepAlive();
try {
MoonBridge.setDisplayFramePacerEnabled(false);
} catch (_) {}
try {
pointer.setPointerVisibleSync(true);
} catch (_) {}
Expand Down Expand Up @@ -1856,6 +1885,7 @@ struct StreamPage {
const screenY = globalPos.y as number;
this.inputHandler.updateXComponentScreenPosition(screenX, screenY);
}
this.applyDisplayFrameRateHints(this.viewModel.isConnected);
})
}
.width('100%')
Expand Down
15 changes: 15 additions & 0 deletions entry/src/main/ets/service/streaming/MoonBridge.ets
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,21 @@ class MoonBridgeClass {
nativeLib.setXComponentFrameRate(frameNode, fps);
}

/**
* 启动/停止显示层高刷保持(native DisplaySoloist)。
* 用于 120fps 串流时持续向系统智能帧率策略请求高刷新率。
*/
setDisplayFramePacerEnabled(enabled: boolean): void {
nativeLib.setDisplayFramePacerEnabled(enabled);
}

/**
* 重新应用 NativeVSync / NativeWindow / DisplaySoloist 帧率提示。
*/
refreshFrameRateHints(): void {
nativeLib.refreshFrameRateHints();
}

// ==========================================================================
// 性能模式
// ==========================================================================
Expand Down
73 changes: 59 additions & 14 deletions nativelib/src/main/cpp/moonlight_bridge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1875,13 +1875,21 @@ typedef int32_t (*PFN_GetNodeHandleFromNapiValue)(napi_env, napi_value, void** /
typedef void* (*PFN_GetNativeXComponent)(void* /* ArkUI_NodeHandle */);
typedef int32_t (*PFN_XCSetFrameRateOld)(void* /* OH_NativeXComponent* */, XCFrameRateRange* /* range* */);
typedef int32_t (*PFN_XCSetFrameRateNew)(void* /* ArkUI_NodeHandle */, XCFrameRateRange /* range */);
typedef int32_t (*PFN_XCRegisterOnFrameCallback)(
void* /* OH_NativeXComponent* */,
void (*callback)(void* /* OH_NativeXComponent* */, uint64_t /* timestamp */, uint64_t /* targetTimestamp */));

static PFN_GetNodeHandleFromNapiValue g_pfnGetNodeHandle = nullptr;
static PFN_GetNativeXComponent g_pfnGetNativeXC = nullptr;
static PFN_XCSetFrameRateOld g_pfnXCSetFrameRateOld = nullptr;
static PFN_XCSetFrameRateNew g_pfnXCSetFrameRateNew = nullptr;
static PFN_XCRegisterOnFrameCallback g_pfnXCRegisterOnFrame = nullptr;
static bool g_xcFrameRateChecked = false;

static void XComponentOnFrameCallback(void*, uint64_t, uint64_t) {
// 空回调用于保持 XComponent/ArkUI 层有持续帧节奏提示。
}

static void CheckAndLoadXCFrameRateApis() {
if (g_xcFrameRateChecked) return;
g_xcFrameRateChecked = true;
Expand All @@ -1902,7 +1910,7 @@ static void CheckAndLoadXCFrameRateApis() {
return;
}

// 方式1 (API 20): OH_ArkUI_XComponent_SetExpectedFrameRateRange 直接通过 NodeHandle
// 方式1 (API 20): OH_ArkUI_XComponent_SetExpectedFrameRateRange - 直接通过 NodeHandle
g_pfnXCSetFrameRateNew = (PFN_XCSetFrameRateNew)dlsym(RTLD_DEFAULT,
"OH_ArkUI_XComponent_SetExpectedFrameRateRange");
if (!g_pfnXCSetFrameRateNew) {
Expand All @@ -1912,18 +1920,15 @@ static void CheckAndLoadXCFrameRateApis() {
"OH_ArkUI_XComponent_SetExpectedFrameRateRange");
}
}
if (g_pfnXCSetFrameRateNew) {
OH_LOG_INFO(LOG_APP, "XCFrameRate: API 20 OH_ArkUI_XComponent_SetExpectedFrameRateRange available");
return; // 优先方式,不需要继续查找
}

// 方式2 (API 12+11): OH_NativeXComponent_GetNativeXComponent + SetExpectedFrameRateRange
g_pfnGetNativeXC = (PFN_GetNativeXComponent)dlsym(RTLD_DEFAULT,
"OH_NativeXComponent_GetNativeXComponent");
g_pfnXCSetFrameRateOld = (PFN_XCSetFrameRateOld)dlsym(RTLD_DEFAULT,
"OH_NativeXComponent_SetExpectedFrameRateRange");
g_pfnXCRegisterOnFrame = (PFN_XCRegisterOnFrameCallback)dlsym(RTLD_DEFAULT,
"OH_NativeXComponent_RegisterOnFrameCallback");
// 回退 dlopen
if (!g_pfnGetNativeXC || !g_pfnXCSetFrameRateOld) {
if (!g_pfnGetNativeXC || !g_pfnXCSetFrameRateOld || !g_pfnXCRegisterOnFrame) {
void* aceHandle = dlopen("libace_ndk.z.so", RTLD_NOW);
if (aceHandle) {
if (!g_pfnGetNativeXC)
Expand All @@ -1932,14 +1937,23 @@ static void CheckAndLoadXCFrameRateApis() {
if (!g_pfnXCSetFrameRateOld)
g_pfnXCSetFrameRateOld = (PFN_XCSetFrameRateOld)dlsym(aceHandle,
"OH_NativeXComponent_SetExpectedFrameRateRange");
if (!g_pfnXCRegisterOnFrame)
g_pfnXCRegisterOnFrame = (PFN_XCRegisterOnFrameCallback)dlsym(aceHandle,
"OH_NativeXComponent_RegisterOnFrameCallback");
}
}

if (g_pfnXCSetFrameRateNew) {
OH_LOG_INFO(LOG_APP, "XCFrameRate: API 20 OH_ArkUI_XComponent_SetExpectedFrameRateRange available");
}
if (g_pfnGetNativeXC && g_pfnXCSetFrameRateOld) {
OH_LOG_INFO(LOG_APP, "XCFrameRate: API 12 GetNativeXComponent + API 11 SetExpectedFrameRateRange available");
} else {
OH_LOG_WARN(LOG_APP, "XCFrameRate: No XComponent frame rate API available");
}
if (g_pfnGetNativeXC && g_pfnXCRegisterOnFrame) {
OH_LOG_INFO(LOG_APP, "XCFrameRate: OH_NativeXComponent_RegisterOnFrameCallback available");
}
}

napi_value MoonBridge_SetXComponentFrameRate(napi_env env, napi_callback_info info) {
Expand Down Expand Up @@ -1971,23 +1985,35 @@ napi_value MoonBridge_SetXComponentFrameRate(napi_env env, napi_callback_info in
return GetUndefined(env);
}

void* xComp = nullptr;
if (g_pfnGetNativeXC) {
xComp = g_pfnGetNativeXC(nodeHandle);
}

if (xComp && g_pfnXCRegisterOnFrame) {
int32_t cbRet = g_pfnXCRegisterOnFrame(xComp, XComponentOnFrameCallback);
OH_LOG_INFO(LOG_APP, "XComponent onFrame callback registered for frame pacing: ret=%{public}d", cbRet);
}

// 方式1 (API 20): 直接通过 ArkUI_NodeHandle 设置
if (g_pfnXCSetFrameRateNew) {
XCFrameRateRange range = { fps, fps, fps };
int32_t xcRet = g_pfnXCSetFrameRateNew(nodeHandle, range);
OH_LOG_INFO(LOG_APP, "XComponent FrameRate set to %{public}d fps via ArkUI_NodeHandle (API 20): ret=%{public}d",
fps, xcRet);
return GetUndefined(env);
OH_LOG_INFO(LOG_APP, "XComponent FrameRateRange set to %{public}d/%{public}d/%{public}d fps via ArkUI_NodeHandle (API 20): ret=%{public}d",
fps, fps, fps, xcRet);
if (xcRet == 0) {
return GetUndefined(env);
}
OH_LOG_WARN(LOG_APP, "XComponent API 20 frame rate path failed, trying NativeXComponent fallback");
}

// 方式2 (API 12+11): NodeHandle → OH_NativeXComponent → SetExpectedFrameRateRange
if (g_pfnGetNativeXC && g_pfnXCSetFrameRateOld) {
void* xComp = g_pfnGetNativeXC(nodeHandle);
if (xComp) {
XCFrameRateRange range = { fps, fps, fps };
int32_t xcRet = g_pfnXCSetFrameRateOld(xComp, &range);
OH_LOG_INFO(LOG_APP, "XComponent FrameRate set to %{public}d fps via NativeXComponent (API 12+11): ret=%{public}d",
fps, xcRet);
OH_LOG_INFO(LOG_APP, "XComponent FrameRateRange set to %{public}d/%{public}d/%{public}d fps via NativeXComponent (API 12+11): ret=%{public}d",
fps, fps, fps, xcRet);
} else {
OH_LOG_ERROR(LOG_APP, "SetXComponentFrameRate: GetNativeXComponent returned null");
}
Expand All @@ -1996,4 +2022,23 @@ napi_value MoonBridge_SetXComponentFrameRate(napi_env env, napi_callback_info in

OH_LOG_WARN(LOG_APP, "SetXComponentFrameRate: No API path available for fps=%{public}d", fps);
return GetUndefined(env);
}
}

napi_value MoonBridge_SetDisplayFramePacerEnabled(napi_env env, napi_callback_info info) {
size_t argc = 1;
napi_value argv[1];
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);

bool enabled = false;
if (argc >= 1) {
napi_get_value_bool(env, argv[0], &enabled);
}

NativeRender::GetInstance()->SetDisplayFramePacerEnabled(enabled);
return GetUndefined(env);
}

napi_value MoonBridge_RefreshFrameRateHints(napi_env env, napi_callback_info info) {
NativeRender::GetInstance()->RefreshFrameRateHints(true);
return GetUndefined(env);
}
11 changes: 11 additions & 0 deletions nativelib/src/main/cpp/moonlight_bridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,17 @@ napi_value MoonBridge_SetBassVibrationConfig(napi_env env, napi_callback_info in
*/
napi_value MoonBridge_SetXComponentFrameRate(napi_env env, napi_callback_info info);

/**
* 启动/停止显示层高刷保持(DisplaySoloist)
* @param enabled boolean 是否启用
*/
napi_value MoonBridge_SetDisplayFramePacerEnabled(napi_env env, napi_callback_info info);

/**
* 重新应用 NativeVSync / NativeWindow / DisplaySoloist 帧率提示
*/
napi_value MoonBridge_RefreshFrameRateHints(napi_env env, napi_callback_info info);

// =============================================================================
// 常量定义
// =============================================================================
Expand Down
2 changes: 2 additions & 0 deletions nativelib/src/main/cpp/napi_init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ static napi_value Init(napi_env env, napi_value exports) {

// XComponent 帧率设置(通过 FrameNode → ArkUI_NodeHandle,无需 libraryname)
{ "setXComponentFrameRate", nullptr, MoonBridge_SetXComponentFrameRate, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "setDisplayFramePacerEnabled", nullptr, MoonBridge_SetDisplayFramePacerEnabled, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "refreshFrameRateHints", nullptr, MoonBridge_RefreshFrameRateHints, nullptr, nullptr, nullptr, napi_default, nullptr },
};

napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
Expand Down
Loading
Loading