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
41 changes: 35 additions & 6 deletions entry/src/main/ets/service/streaming/StreamingSession.ets
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { ComputerManager } from '../ComputerManager';
import { ComputerInfo, selectBestAddress } from '../../model/ComputerInfo';
import { NvHttp, LaunchConfig } from './NvHttp';
import { CryptoUtil } from '../../utils/CryptoUtil';
import { parseAddressAndPort } from '../../utils/NetHelper';
import { parseAddressAndPort, parseRtspSessionHost } from '../../utils/NetHelper';
import { common, abilityAccessCtrl, bundleManager, Permissions } from '@kit.AbilityKit';
import { display } from '@kit.ArkUI';
import { GamepadManager } from '../input/GamepadManager';
Expand Down Expand Up @@ -43,7 +43,8 @@ import {
VK_MENU,
VK_LWIN,
KEY_ACTION_DOWN,
KEY_ACTION_UP
KEY_ACTION_UP,
STAGE_NAME_RESOLUTION
} from './MoonBridge';
import nativeLib from 'libmoonlight_nativelib.so';

Expand Down Expand Up @@ -387,6 +388,7 @@ export class StreamingSession {
private userInitiatedStop: boolean = false;
private isStartingConnection: boolean = false;
private connectionStartPromise: Promise<number> | null = null;
private lastFailedStage: number = 0;
private computer: ComputerInfo | null = null;
/** 本次串流最终选中的连接地址(手动/锁定/轮询胜出地址),保证 HTTP 与原生串流阶段一致 */
private connectionAddress: string = '';
Expand Down Expand Up @@ -1206,6 +1208,7 @@ export class StreamingSession {
stageComplete: (stage: number): void => { console.info(`连接阶段完成: ${stage}`); },
stageFailed: (stage: number, errorCode: number): void => {
const name = STAGE_NAMES[stage] || `阶段 ${stage}`;
this.lastFailedStage = stage;
console.error(`连接阶段失败: stage=${stage} (${name}), error=${errorCode}`);
},
connectionStarted: (): void => {
Expand Down Expand Up @@ -1437,11 +1440,13 @@ export class StreamingSession {
`vsync=${this.config.enableVsync}, hostPaced=${this.config.enableHostPacedPresentation ?? false}, ` +
`vrr=${this.config.enableVrr}`);

const callStart = async (): Promise<number> => {
const callStart = async (address: string): Promise<number> => {
this.isStartingConnection = true;
this.lastFailedStage = 0;
try {
console.info(`StreamingSession: native connection address=${address}`);
const pendingStart: Promise<number> = this.nativeModule.startConnectionAsync(
this.connectionAddress,
address,
this.serverAppVersion,
this.serverGfeVersion,
this.rtspSessionUrl,
Expand Down Expand Up @@ -1469,7 +1474,31 @@ export class StreamingSession {
}
};

let result = await callStart();
let activeConnectionAddress = this.connectionAddress;
let result = await callStart(activeConnectionAddress);

// getaddrinfo() may fail after /launch succeeds when the selected hostname
// is stale or temporarily unavailable. In that specific stage, retry with
// the concrete host Sunshine just advertised in sessionUrl0. Do not use
// this address unconditionally: behind IPv4 NAT, sessionUrl0 may contain a
// private host while the selected WAN address is the correct route.
const rtspSessionHost = parseRtspSessionHost(this.rtspSessionUrl);
if (result === -2 &&
this.lastFailedStage === STAGE_NAME_RESOLUTION &&
rtspSessionHost &&
rtspSessionHost !== activeConnectionAddress) {
if (this.userInitiatedStop) {
return;
}
console.warn(`StreamingSession: 名称解析失败,改用 sessionUrl0 主机 ${rtspSessionHost} 重试`);
this.nativeModule.stopConnection();
activeConnectionAddress = rtspSessionHost;
result = await callStart(activeConnectionAddress);
if (result === 0) {
console.info('StreamingSession: sessionUrl0 主机回退成功');
}
}

// RTSP/ENet 瞬态错误自动重试 1 次:errno 4=EINTR、110=ETIMEDOUT、104=ECONNRESET、32=EPIPE
// 这类错误通常是 ENet service 循环被打断或网络抖动,host 端不需要重新 launchApp,
// 只需 stopConnection 释放 native 资源后等待短暂窗口再重试即可恢复。
Expand All @@ -1488,7 +1517,7 @@ export class StreamingSession {
if (this.userInitiatedStop) {
return;
}
result = await callStart();
result = await callStart(activeConnectionAddress);
if (result === 0) {
console.info('StreamingSession: 重试成功');
}
Expand Down
16 changes: 16 additions & 0 deletions entry/src/main/ets/utils/NetHelper.ets
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,22 @@ export function parseAddressAndPort(input: string): AddressPort {
return result;
}

/**
* Extract the host advertised by Sunshine in sessionUrl0.
*
* The native Moonlight core still resolves SERVER_INFORMATION.address for
* the RTSP socket. This host is therefore a fallback for cases where the
* selected hostname can no longer be resolved after /launch or /resume.
*/
export function parseRtspSessionHost(sessionUrl: string): string {
const match = /^(?:rtsp|rtspenc):\/\/([^/?#]+)/i.exec(sessionUrl.trim());
if (!match || match.length < 2) {
return '';
}

return parseAddressAndPort(match[1]).host;
}

/**
* 格式化地址用于 URL:IPv6 地址需要加方括号
*
Expand Down
4 changes: 4 additions & 0 deletions entry/src/main/ets/utils/NetworkErrorClassifierSelfCheck.ets
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
isPrivateIPv4,
isPublicAddress,
parseAddressAndPort,
parseRtspSessionHost,
} from './NetHelper';
import { selectBestAddress } from '../model/ComputerInfo';

Expand Down Expand Up @@ -216,6 +217,9 @@ export class NetworkErrorClassifierSelfCheck {
{ name: 'formatAddressForUrl IPv4', actual: formatAddressForUrl('192.168.1.10'), expected: '192.168.1.10', reason: 'IPv4 不加方括号' },
{ name: 'formatAddressForUrl IPv6', actual: formatAddressForUrl('fe80::1'), expected: '[fe80::1]', reason: 'URL 中 IPv6 必须加方括号' },
{ name: 'formatAddressForUrl bracket IPv6', actual: formatAddressForUrl('[::1]'), expected: '[::1]', reason: '已有方括号不重复添加' },
{ name: 'parseRtspSessionHost IPv4', actual: parseRtspSessionHost('rtsp://192.168.1.10:48010'), expected: '192.168.1.10', reason: '提取 Sunshine 返回的 IPv4 RTSP 主机' },
{ name: 'parseRtspSessionHost IPv6', actual: parseRtspSessionHost('rtspenc://[240e:1234::10]:48010'), expected: '240e:1234::10', reason: '提取 Sunshine 返回的带方括号 IPv6 RTSP 主机' },
{ name: 'parseRtspSessionHost rejects HTTP', actual: parseRtspSessionHost('https://sunshine.example.com:47984'), expected: '', reason: '只接受 RTSP 会话 URL' },
];

for (const c of typeCases) {
Expand Down
Loading