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
4 changes: 2 additions & 2 deletions AppScope/app.json5
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
67 changes: 67 additions & 0 deletions entry/src/main/ets/components/StreamConnectingTips.ets
Original file line number Diff line number Diff line change
@@ -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];
}
6 changes: 5 additions & 1 deletion entry/src/main/ets/pages/AppListPageV2.ets
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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
}
});
}
Expand Down
101 changes: 87 additions & 14 deletions entry/src/main/ets/pages/StreamPage.ets
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -86,6 +87,7 @@ interface StreamPageParams {
cmdList?: string; // JSON 字符串格式的超级指令列表
displayGuid?: string; // 指定的显示器 GUID
useVdd?: boolean; // 是否使用虚拟显示器
posterPath?: string; // 应用本地海报路径,连接中遮罩背景
}

/** StreamPage 对 CustomKeyManager 的宿主回调实现 */
Expand Down Expand Up @@ -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%';
Expand Down Expand Up @@ -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 {
Expand All @@ -266,6 +275,8 @@ struct StreamPage {
}
}
}
// 每次进入随机选一条小课堂提示
this.connectingTip = pickRandomConnectingTip();

// 自动加载游戏关联的自定义按键配置
if (this.appId > 0) {
Expand Down Expand Up @@ -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
Expand Down
47 changes: 36 additions & 11 deletions entry/src/main/ets/service/streaming/StreamingSession.ets
Original file line number Diff line number Diff line change
Expand Up @@ -1266,28 +1266,53 @@ 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)) {
// 瞬态错误后立即检查是否已被用户取消
if (this.userInitiatedStop) {
return;
}
console.warn(`StreamingSession: startConnection 返回瞬态错误 ${result},1.5s 后重试一次`);
if (!this.userInitiatedStop) {
this.nativeModule.stopConnection();
}
await new Promise<void>(resolve => setTimeout(resolve, 1500));
// 重试前再次检查是否已被用户取消
if (this.userInitiatedStop) {
return;
}
result = callStart();
if (result === 0) {
console.info('StreamingSession: 重试成功');
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (result !== 0) {
throw new Error(`连接失败,错误码: ${result}`);
}
Expand Down
Loading
Loading