From cd3fe4ce122103dea5cf1df91d852d80998086c5 Mon Sep 17 00:00:00 2001 From: qiin2333 <414382190@qq.com> Date: Thu, 14 May 2026 16:28:31 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(stream):=20=E4=BC=98=E5=8C=96=E8=BF=9E?= =?UTF-8?q?=E6=8E=A5=E4=B8=AD=E2=86=92=E9=A6=96=E5=B8=A7=E8=A7=86=E8=A7=89?= =?UTF-8?q?=E8=BF=87=E6=B8=A1=20+=20RTSP=20=E7=9E=AC=E6=80=81=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E8=87=AA=E5=8A=A8=E9=87=8D=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ConnectingOverlay 推迟到首帧解码后才退场,并以 320ms 淡出+轻微放大与首帧出现对齐 - 遮罩使用应用海报作模糊背景,不再纯黑等待 - 等待期随机展示一条小课堂提示 - RTSP ANNOUNCE 阶段瞬态 socket 错误(errno 4/32/104/110)自动重试一次 - bump version to 1.0.0.771 --- AppScope/app.json5 | 4 +- .../ets/components/StreamConnectingTips.ets | 67 ++++++++++++ entry/src/main/ets/pages/AppListPageV2.ets | 6 +- entry/src/main/ets/pages/StreamPage.ets | 101 +++++++++++++++--- .../service/streaming/StreamingSession.ets | 37 +++++-- .../main/ets/viewmodel/StreamViewModel.ets | 62 +++++++---- entry/src/main/resources/rawfile/CHANGELOG.md | 11 ++ 7 files changed, 241 insertions(+), 47 deletions(-) create mode 100644 entry/src/main/ets/components/StreamConnectingTips.ets diff --git a/AppScope/app.json5 b/AppScope/app.json5 index 6339bf8..2425dfb 100644 --- a/AppScope/app.json5 +++ b/AppScope/app.json5 @@ -2,8 +2,8 @@ "app": { "bundleName": "com.alkaidlab.sdream", "vendor": "Moonlight", - "versionCode": 1000770, - "versionName": "1.0.0.770", + "versionCode": 1000771, + "versionName": "1.0.0.771", "icon": "$media:layered_icon", "label": "$string:app_name", "bundleType": "app", diff --git a/entry/src/main/ets/components/StreamConnectingTips.ets b/entry/src/main/ets/components/StreamConnectingTips.ets new file mode 100644 index 0000000..01125dd --- /dev/null +++ b/entry/src/main/ets/components/StreamConnectingTips.ets @@ -0,0 +1,67 @@ +/** + * 串流连接中显示的小课堂提示。 + * 内容偏向真实存在但少有人知的隐藏技巧/小彩蛋,从历次 CHANGELOG 挖掘而来。 + * 目标:让用户每次连接都能学到一个新东西,而不是看到常见能力的复读。 + * + * 在 StreamPage 的 ConnectingOverlay 中随机展示一条,让等待 launchApp / 首帧 + * 的几秒钟(HTTPS + TLS + RTSP + 解码器初始化)不至于纯黑等待。 + */ +export const STREAM_CONNECTING_TIPS: readonly string[] = [ + // —— 隐藏手势 / 双击 / 长按彩蛋 —— + '🐚 双击 ESC 可以呼出游戏菜单,不想用可以在设置里换成 F1/F12/~ 哦~', + '🐚 长按 Start 键也能呼出游戏菜单,不用伸手摸屏幕~', + '🐚 性能覆盖层双击切换横/竖布局,长按锁定后画面事件可以穿透过去~', + '🐚 自定义按键的双击绑定可以单独设置一个动作,摇杆的 L3/R3 就是这么做的~', + '🐚 双指捏合可以缩放画面最高 10x,单指还能继续平移~', + '🐚 触控板模式支持双指滚动、长按拖拽、双击拖拽全套手势~', + + // —— 振动黑魔法(差异化卖点) —— + '🐚 没有手柄也能感受振动反馈:开启音频振动,手机马达跟着低频/鼓点震起来~', + '🐚 音频振动有"音乐/节奏"模式,听歌时手机会跟节拍跳~', + '🐚 USB 手柄支持立体声空间感振动:声源偏左低频马达增强,偏右高频马达增强~', + '🐚 自定义按键支持 HD Haptic 触感分级,按下去的回馈也能定制~', + + // —— 画质冷知识 —— + '🐚 鸿蒙的 HDR Vivid 比 HDR10 强:色准、亮度映射、暗部细节都不在一个量级~', + '🐚 XEngine 是华为 GPU 加速的硬件超分,开了不掉帧还更清晰~', + '🐚 超分辨率"自动"模式会优先 XEngine,不支持时回退 FSR 1~', + '🐚 SDR→HDR 映像增强可以让普通 SDR 画面焕发 HDR 光彩~', + '🐚 OLED 暗部校正可以消除低亮度模式切换的闪烁(MatePad Pro 12.2 适配)~', + '🐚 画面位置支持 9 宫格 + 水平/垂直百分比偏移,刘海挡画面终结~', + + // —— 输入小技巧 —— + '🐚 没接鼠标?摇杆可以模拟鼠标,桌面和策略游戏照样操作~', + '🐚 陀螺仪体感瞄准是真的能用,FPS 微调比纯摇杆精准多了~', + '🐚 DualSense / DualShock4 USB 连接时可以把触摸板当鼠标用~', + '🐚 Xbox / PS 手柄自动启用 USB 高速模式,理论可达 1000Hz 轮询~', + '🐚 Joy-Con / Pro Controller 也能用,按键映射已经内置好~', + '🐚 蓝牙手柄的 Home 键已防误触,再也不会被弹出去了~', + + // —— 界面彩蛋 / 高级技巧 —— + '🐚 应用列表的背景图会跟着你长按选中的应用自动切换,是当前应用的封面哦~', + '🐚 应用列表右上角图标可以切换小图标列表模式,长列表更省空间~', + '🐚 主题有巧克力深色和 Gura 鲨鱼蓝浅色两套,设置里切换试试~', + '🐚 IME 输入面板右上角 🔒/🔓 可以锁定快捷条,避免误触收起~', + '🐚 自定义按键支持多档配置档案,不同游戏自动加载上次用的那套~', + '🐚 自定义按键支持智能配色:12 套预设 + 屏幕取色一键匹配壁纸~', + '🐚 自定义按键的蜂窝六边形群组可以一键生成 7 枚六边形按键~', + '🐚 自定义按键编辑模式有 PPT 风格的对齐吸附线,拖拽就能对齐~', + '🐚 自定义按键二维码分享用 v3 压缩算法,最多 30 个按键也能塞进一张码~', + '🐚 每个应用可以配置专属的"超级指令",启动时自动按顺序执行~', + + // —— 网络 / 连接小技巧 —— + '🐚 网络测试后可以锁定首选地址,跨网络场景下连接更可靠~', + '🐚 智能码率(ABR)会实时监控丢包延迟自动调节,弱网时主动请求关键帧加速恢复~', + '🐚 串流中可以动态调节码率,不用退出重连就能找到网络最佳值~', + '🐚 Foundation Sunshine 服务端能解锁更多增强:HDR Vivid、振动反馈、剪贴板同步、LLM 决策中心~', + + // —— 安全 / 备份 / 战报 —— + '🐚 设置自动备份会加密保存到本地(HUKS + AES-256-GCM),换设备也能秒恢复~', + '🐚 串流结束有「杂鱼串流战报」可以看,还能生成 PNG 分享图~' +]; + +/** 随机选一条提示语 */ +export function pickRandomConnectingTip(): string { + const idx = Math.floor(Math.random() * STREAM_CONNECTING_TIPS.length); + return STREAM_CONNECTING_TIPS[idx]; +} diff --git a/entry/src/main/ets/pages/AppListPageV2.ets b/entry/src/main/ets/pages/AppListPageV2.ets index 5960cb2..498cd08 100644 --- a/entry/src/main/ets/pages/AppListPageV2.ets +++ b/entry/src/main/ets/pages/AppListPageV2.ets @@ -1160,6 +1160,9 @@ struct AppListPageV2 { private doLaunchApp(app: ObservableApp): void { const displayGuid = this.getSelectedDisplayGuid(); + // 把当前 app 的本地海报路径传给 StreamPage,作为 ConnectingOverlay 的背景, + // 让等待 launchApp / 首帧的几秒钟不再是纯黑屏。 + const posterPath = (app.localIconPath && app.iconLoaded) ? app.localIconPath : ''; router.pushUrl({ url: 'pages/StreamPage', params: { @@ -1169,7 +1172,8 @@ struct AppListPageV2 { cmdList: app.cmdList.length > 0 ? JSON.stringify(app.cmdList) : undefined, resume: app.isRunning, displayGuid: displayGuid, - useVdd: this.useVirtualDisplay + useVdd: this.useVirtualDisplay, + posterPath: posterPath } }); } diff --git a/entry/src/main/ets/pages/StreamPage.ets b/entry/src/main/ets/pages/StreamPage.ets index acb2925..680aa7e 100644 --- a/entry/src/main/ets/pages/StreamPage.ets +++ b/entry/src/main/ets/pages/StreamPage.ets @@ -27,6 +27,7 @@ import { StreamViewModel, TouchMode } from '../viewmodel/StreamViewModel'; import { TouchInputHandler, TouchInputMode } from '../service/input/TouchInputHandler'; import { StreamWindowManager } from '../service/streaming/StreamWindowManager'; import { ShortcutManager, Shortcut } from '../components/ShortcutManager'; +import { pickRandomConnectingTip } from '../components/StreamConnectingTips'; import { ShortcutEditorDialog, ShortcutEditData } from '../components/dialogs/ShortcutEditorDialog'; import { GamepadManager, GamepadState, MoonlightControllerType, MouseEmulationCallback } from '../service/input/GamepadManager'; import { GyroAssistService } from '../service/input/GyroAssistService'; @@ -86,6 +87,7 @@ interface StreamPageParams { cmdList?: string; // JSON 字符串格式的超级指令列表 displayGuid?: string; // 指定的显示器 GUID useVdd?: boolean; // 是否使用虚拟显示器 + posterPath?: string; // 应用本地海报路径,连接中遮罩背景 } /** StreamPage 对 CustomKeyManager 的宿主回调实现 */ @@ -145,6 +147,11 @@ struct StreamPage { // 网络质量警告(通过 AppStorage 跨 NAPI 边界可靠同步) @StorageLink('connectionPoor') connectionPoor: boolean = false; + + // 连接中遮罩使用的应用海报(由 AppListPageV2 传入),让等待 launchApp/首帧时不再是纯黑屏 + @State posterPath: string = ''; + // 连接中遮罩随机展示的小课堂提示 + @State connectingTip: string = ''; // UI 状态 @State xComponentWidth: Length = '100%'; @@ -257,6 +264,8 @@ struct StreamPage { this.appName = params.appName || ''; this.displayGuid = params.displayGuid || ''; this.useVdd = params.useVdd || false; + // 连接中遮罩背景(应用海报),让等待首帧时有视觉锚点 + this.posterPath = params.posterPath || ''; // 解析超级指令列表 if (params.cmdList) { try { @@ -266,6 +275,8 @@ struct StreamPage { } } } + // 每次进入随机选一条小课堂提示 + this.connectingTip = pickRandomConnectingTip(); // 自动加载游戏关联的自定义按键配置 if (this.appId > 0) { @@ -2167,23 +2178,85 @@ struct StreamPage { @Builder ConnectingOverlay() { - Column() { - LoadingProgress() - .width(48) - .height(48) - .color(Color.White) - - Text(this.viewModel.connectionStageText || '正在连接...') - .fontSize(14) - .fontColor('#B0FFFFFF') - .margin({ top: 16 }) - .textAlign(TextAlign.Center) - .transition(TransitionEffect.OPACITY.animation({ duration: 200 })) + Stack() { + // 1) 海报背景层:有海报时用 app 封面,模糊 + 暗化;无海报时回退到纯黑 + if (this.posterPath.length > 0) { + Image(`file://${this.posterPath}`) + .width('100%') + .height('100%') + .objectFit(ImageFit.Cover) + .blur(40) + .opacity(0.55) + } + // 2) 渐变暗化层(顶部和底部更暗,居中信息可读性更高) + Column() + .width('100%') + .height('100%') + .linearGradient({ + angle: 180, + colors: [ + ['rgba(0,0,0,0.85)', 0.0], + ['rgba(0,0,0,0.55)', 0.5], + ['rgba(0,0,0,0.92)', 1.0] + ] + }) + + // 3) 信息层 + Column() { + // 应用名(如果有) + if (this.appName.length > 0) { + Text(this.appName) + .fontSize(20) + .fontWeight(FontWeight.Medium) + .fontColor('#FFFFFFFF') + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + .margin({ bottom: 32 }) + .constraintSize({ maxWidth: 360 }) + .textAlign(TextAlign.Center) + } + + LoadingProgress() + .width(48) + .height(48) + .color(Color.White) + + Text(this.viewModel.connectionStageText || '正在连接...') + .fontSize(14) + .fontColor('#E0FFFFFF') + .margin({ top: 16 }) + .textAlign(TextAlign.Center) + .transition(TransitionEffect.OPACITY.animation({ duration: 200 })) + + // 小课堂提示(仅在有 tip 时显示,控制最大宽度) + if (this.connectingTip.length > 0) { + Text(this.connectingTip) + .fontSize(12) + .fontColor('#99FFFFFF') + .margin({ top: 28 }) + .padding({ left: 28, right: 28 }) + .textAlign(TextAlign.Center) + .maxLines(3) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + .constraintSize({ maxWidth: 460 }) + .lineHeight(18) + } + } + .width('100%') + .height('100%') + .justifyContent(FlexAlign.Center) + .alignItems(HorizontalAlign.Center) } .width('100%') .height('100%') - .backgroundColor('#CC000000') - .justifyContent(FlexAlign.Center) + // 淡出 + 轻微放大消失,让"已连接 → 首帧"之间不再瞬间黑切。 + // 进入时用瞬时显现(appear: identity),离开时 320ms ease-out 淡到 0 + scale 1.04 + .transition(TransitionEffect.asymmetric( + TransitionEffect.IDENTITY, + TransitionEffect.OPACITY.combine( + TransitionEffect.scale({ x: 1.04, y: 1.04 }) + ).animation({ duration: 320, curve: Curve.EaseOut }) + )) } @Builder diff --git a/entry/src/main/ets/service/streaming/StreamingSession.ets b/entry/src/main/ets/service/streaming/StreamingSession.ets index c1c998a..24117e5 100644 --- a/entry/src/main/ets/service/streaming/StreamingSession.ets +++ b/entry/src/main/ets/service/streaming/StreamingSession.ets @@ -1266,28 +1266,43 @@ export class StreamingSession { console.info(`解码器: buffers=${this.config.decoderBufferCount}, sync=${this.config.enableSyncDecode}, ` + `vsync=${this.config.enableVsync}, vrr=${this.config.enableVrr}`); - const result = this.nativeModule.startConnection( + const callStart = (): number => this.nativeModule.startConnection( this.connectionAddress, this.serverAppVersion, this.serverGfeVersion, this.rtspSessionUrl, this.serverCodecModeSupport, - this.config.width, this.config.height, this.config.fps, - this.config.bitrate, + this.config!.width, this.config!.height, this.config!.fps, + this.config!.bitrate, 1392, // packetSize (will be capped by moonlight-common-c for remote) 2, // streamingRemotely: STREAM_CFG_AUTO - let moonlight-common-c auto-detect - this.config.audioConfig, + this.config!.audioConfig, supportedVideoFormats, - this.config.clientRefreshRateX100, - this.riKey, riAesIv, + this.config!.clientRefreshRateX100, + this.riKey!, riAesIv, 0x01 | 0x02, // videoCapabilities: DIRECT_SUBMIT | RFI_AVC - this.config.hdr ? 2 : 1, // colorSpace: REC_2020 / REC_709 - this.config.colorRange, - this.config.hdr ? this.config.hdrMode : HdrMode.SDR, - this.config.microphoneEnabled, - this.config.controlOnly // controlOnly + this.config!.hdr ? 2 : 1, // colorSpace: REC_2020 / REC_709 + this.config!.colorRange, + this.config!.hdr ? this.config!.hdrMode : HdrMode.SDR, + this.config!.microphoneEnabled, + this.config!.controlOnly ); + let result = callStart(); + // RTSP/ENet 瞬态错误自动重试 1 次:errno 4=EINTR、110=ETIMEDOUT、104=ECONNRESET、32=EPIPE + // 这类错误通常是 ENet service 循环被打断或网络抖动,host 端不需要重新 launchApp, + // 只需 stopConnection 释放 native 资源后等待短暂窗口再重试即可恢复。 + const TRANSIENT_ERRORS = [4, 32, 104, 110]; + if (result !== 0 && TRANSIENT_ERRORS.includes(result)) { + console.warn(`StreamingSession: startConnection 返回瞬态错误 ${result},1.5s 后重试一次`); + this.nativeModule.stopConnection(); + await new Promise(resolve => setTimeout(resolve, 1500)); + result = callStart(); + if (result === 0) { + console.info('StreamingSession: 重试成功'); + } + } + if (result !== 0) { throw new Error(`连接失败,错误码: ${result}`); } diff --git a/entry/src/main/ets/viewmodel/StreamViewModel.ets b/entry/src/main/ets/viewmodel/StreamViewModel.ets index f43e5bb..3034adb 100644 --- a/entry/src/main/ets/viewmodel/StreamViewModel.ets +++ b/entry/src/main/ets/viewmodel/StreamViewModel.ets @@ -164,7 +164,11 @@ export class StreamViewModel { private resolutionChangedCallback: ((width: number, height: number) => void) | null = null; // 连接状态回调(用于端口诊断等) private connectionStatusCallback: ((connectionStatus: number) => void) | null = null; - + + // drStart 到首帧上屏还有 ~150-300ms:轮询 framesDecoded,上屏后才切 CONNECTED, + // 让 ConnectingOverlay 淡出与首帧出现对齐。 + private firstFrameTimer: number = -1; + // 便捷属性 get isConnecting(): boolean { return this.connectionState === StreamConnectionState.CONNECTING; @@ -313,6 +317,7 @@ export class StreamViewModel { // 设置连接终止回调 this.streamingSession.setConnectionTerminatedCallback((errorCode: number) => { console.info(`StreamViewModel: 连接终止回调 errorCode=${errorCode}`); + this.stopFirstFrameWatcher(); this.connectionState = StreamConnectionState.DISCONNECTED; if (this.connectionTerminatedCallback) { this.connectionTerminatedCallback(errorCode); @@ -354,21 +359,26 @@ export class StreamViewModel { this.connectionStageText = stageName; }); - // 视频解码器启动回调:drStart 在 LiStartConnection 内部触发, - // 比 session.start() resolve 更早。提前切到 CONNECTED 让 UI 立即隐藏 loading, - // 缩短"已连接 → 首帧"之间的视觉空窗。 - // 捕获当前 session 实例,防止旧会话延迟回调污染新会话或把 ERROR/DISCONNECTED 改回 CONNECTED。 - const currentSession = this.streamingSession; + // drStart 后轮询 framesDecoded,首帧解出才切 CONNECTED,避免 overlay 提前淡出露出黑屏。 + const session = this.streamingSession; this.streamingSession.setDecoderStartedCallback(() => { - if (this.streamingSession !== currentSession) { - return; - } - if (this.connectionState === StreamConnectionState.CONNECTING) { - console.info('StreamViewModel: drStart 触发,提前进入 CONNECTED 状态'); - this.connectionProgress = 1.0; - this.connectionStageText = '已连接'; - this.connectionState = StreamConnectionState.CONNECTED; - } + if (this.streamingSession !== session || this.connectionState !== StreamConnectionState.CONNECTING) return; + this.connectionStageText = '等待首帧...'; + const deadline = Date.now() + 1500; + this.stopFirstFrameWatcher(); + this.firstFrameTimer = setInterval(() => { + if (this.streamingSession !== session || this.connectionState !== StreamConnectionState.CONNECTING) { + this.stopFirstFrameWatcher(); + return; + } + const decoded = session.getStats()?.decodedFrames ?? 0; + if (decoded > 0 || Date.now() >= deadline) { + this.connectionProgress = 1.0; + this.connectionStageText = '已连接'; + this.connectionState = StreamConnectionState.CONNECTED; + this.stopFirstFrameWatcher(); + } + }, 50); }); // 更新进度:获取服务器信息 @@ -391,9 +401,12 @@ export class StreamViewModel { } } - this.connectionProgress = 1.0; - this.connectionStageText = '已连接'; - this.connectionState = StreamConnectionState.CONNECTED; + // 不在这里强制切 CONNECTED:start() resolve 常于 drStart 之后,但首帧可能还没上屏。 + // 让 firstFrameWatcher 控制 CONNECTED 的跳变时点,有效消除 loading → 首帧 之间的视觉跳跃。 + // 如果 watcher 已经完成(极快的路径),这里状态已经是 CONNECTED,冗余赋值也安全。 + if (this.connectionState !== StreamConnectionState.CONNECTING) { + this.connectionProgress = 1.0; + } // 查询实际生效的超分引擎 this.activeUpscaleMode = this.streamingSession?.getActiveUpscaleMode() ?? 0; @@ -407,6 +420,7 @@ export class StreamViewModel { } } catch (err) { + this.stopFirstFrameWatcher(); this.connectionState = StreamConnectionState.ERROR; const error = err as Error; this.errorMessage = `连接失败: ${error.message}`; @@ -420,7 +434,10 @@ export class StreamViewModel { async stopStreaming(): Promise { // 重置旋转状态 this.resetRotationState(); - + + // 停止首帧轮询(如果还在运行) + this.stopFirstFrameWatcher(); + // 清除网络质量警告 AppStorage.setOrCreate('connectionPoor', false); @@ -432,6 +449,13 @@ export class StreamViewModel { this.connectionState = StreamConnectionState.DISCONNECTED; } + + private stopFirstFrameWatcher(): void { + if (this.firstFrameTimer !== -1) { + clearInterval(this.firstFrameTimer); + this.firstFrameTimer = -1; + } + } /** * 发送输入 diff --git a/entry/src/main/resources/rawfile/CHANGELOG.md b/entry/src/main/resources/rawfile/CHANGELOG.md index be3157d..817bfeb 100644 --- a/entry/src/main/resources/rawfile/CHANGELOG.md +++ b/entry/src/main/resources/rawfile/CHANGELOG.md @@ -25,6 +25,17 @@ - 最新版本放在最前面 --> +## [1.0.0.771] - 2026-05-14 +连接体验优化:首帧对齐 + RTSP 瞬态重试 + 连接中提示 + +### 优化 +- **连接中 → 首帧动画过渡**:原本加载遮罩在解码器启动的那一刻就瞬间消失,会露出尚未填充画面的黑底 ≡ 现在推迟到“首帧真正解码完成”才切换,且遮罩以 320ms 淡出+轻微放大的形式退场,与首帧出现严丝合缝。 +- **连接中遮罩重做**:使用当前应用的海报作为背景(模糊 + 渐变暗化),填充等待 launchApp / 首帧的几秒钟,不再是纯黑屏。 +- **连接中随机小课堂提示**:随机展示一条隐藏手势 / 手柄快捷键 / 隐藏能力提示,让等待时间有趣且可能学到新东西。 + +### 修复 +- **RTSP 握手瞬态错误自动重试**:偏远程/弱网场景下 ANNOUNCE 阶段偶发 errno 4/32/104/110(底层 socket 被中断或超时)导致“错误码 4”,现在会自动重试一次,用户无感。 + ## [1.0.0.770] - 2026-05-12 剪贴板与主机双向同步(图文) From 170a3e3246adaf5d2c85fd712b7cafb86772deb1 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 08:43:19 +0000 Subject: [PATCH 2/2] fix: apply CodeRabbit auto-fixes Fixed 2 file(s) based on 2 unresolved review comments. Co-authored-by: CodeRabbit --- .../main/ets/service/streaming/StreamingSession.ets | 12 +++++++++++- entry/src/main/resources/rawfile/CHANGELOG.md | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/entry/src/main/ets/service/streaming/StreamingSession.ets b/entry/src/main/ets/service/streaming/StreamingSession.ets index 24117e5..3f246b6 100644 --- a/entry/src/main/ets/service/streaming/StreamingSession.ets +++ b/entry/src/main/ets/service/streaming/StreamingSession.ets @@ -1294,9 +1294,19 @@ export class StreamingSession { // 只需 stopConnection 释放 native 资源后等待短暂窗口再重试即可恢复。 const TRANSIENT_ERRORS = [4, 32, 104, 110]; if (result !== 0 && TRANSIENT_ERRORS.includes(result)) { + // 瞬态错误后立即检查是否已被用户取消 + if (this.userInitiatedStop) { + return; + } console.warn(`StreamingSession: startConnection 返回瞬态错误 ${result},1.5s 后重试一次`); - this.nativeModule.stopConnection(); + if (!this.userInitiatedStop) { + this.nativeModule.stopConnection(); + } await new Promise(resolve => setTimeout(resolve, 1500)); + // 重试前再次检查是否已被用户取消 + if (this.userInitiatedStop) { + return; + } result = callStart(); if (result === 0) { console.info('StreamingSession: 重试成功'); diff --git a/entry/src/main/resources/rawfile/CHANGELOG.md b/entry/src/main/resources/rawfile/CHANGELOG.md index 817bfeb..8a45ce6 100644 --- a/entry/src/main/resources/rawfile/CHANGELOG.md +++ b/entry/src/main/resources/rawfile/CHANGELOG.md @@ -29,7 +29,7 @@ 连接体验优化:首帧对齐 + RTSP 瞬态重试 + 连接中提示 ### 优化 -- **连接中 → 首帧动画过渡**:原本加载遮罩在解码器启动的那一刻就瞬间消失,会露出尚未填充画面的黑底 ≡ 现在推迟到“首帧真正解码完成”才切换,且遮罩以 320ms 淡出+轻微放大的形式退场,与首帧出现严丝合缝。 +- **连接中 → 首帧动画过渡**:原本加载遮罩在解码器启动的那一刻就瞬间消失,会露出尚未填充画面的黑底 → 现在推迟到"首帧真正解码完成"才切换,且遮罩以 320ms 淡出+轻微放大的形式退场,与首帧出现严丝合缝。 - **连接中遮罩重做**:使用当前应用的海报作为背景(模糊 + 渐变暗化),填充等待 launchApp / 首帧的几秒钟,不再是纯黑屏。 - **连接中随机小课堂提示**:随机展示一条隐藏手势 / 手柄快捷键 / 隐藏能力提示,让等待时间有趣且可能学到新东西。