refactor: 重构设置页面并修复屏幕共享黑屏问题

将安全验证和输入设置从主界面迁移至独立的 SettingsActivity,并允许用户手动选择模拟点击方式。
修复 Android 10+ 上 MediaProjection Token 传递失效导致黑屏的问题,通过静态变量保存授权结果并增加捕获延时与心跳保活机制。
优化多端视频编解码器协商,统一在 Offer SDP 中将 VP8/VP9 优先排列,H264 仅作兜底,提升跨端解码兼容性。
This commit is contained in:
TongTongStudio
2026-07-24 17:55:44 +08:00
parent 778c37b8db
commit cdf3d5da46
14 changed files with 652 additions and 175 deletions

View File

@@ -13,6 +13,8 @@ watch(
(stream) => {
if (videoRef.value && stream) {
videoRef.value.srcObject = stream;
// 部分浏览器在 srcObject 设置后不会自动播放(尤其是无用户手势时),需显式 play() 兜底
videoRef.value.play().catch(() => {});
hasStream.value = true;
} else {
hasStream.value = false;

View File

@@ -75,7 +75,18 @@ export class WebRtcController {
this.onIceState && this.onIceState(s);
};
this.pc.ontrack = (e) => {
if (e.streams && e.streams[0]) this.onStream && this.onStream(e.streams[0]);
// 某些浏览器/协商场景下 event.streams 可能为空,需回退到用 track 自行构造 MediaStream
// 否则 store.remoteStream 始终为空 -> 视频元素拿不到流 -> 黑屏(用户会误以为无法控制)。
let stream = e.streams && e.streams[0];
if (!stream) {
stream = new MediaStream([e.track]);
}
this.onStream && this.onStream(stream);
e.track.onunmute = () => {
if (e.track.readyState === 'live') {
this.onStream && this.onStream(stream);
}
};
};
this.pc.ondatachannel = (e) => this.setupDataChannel(e.channel);
@@ -87,12 +98,59 @@ export class WebRtcController {
this.setupDataChannel(dc);
const offer = await this.pc.createOffer();
// 与 WebRTCControlled 的 optimizeSdp 保持一致:将 m=video 行的编解码器
// 重排为 VP8/VP9 优先、H264 兜底,确保各控制端(浏览器/桌面/Chromium/Linux
// 都能稳定解码,避免 H264 High Profile 无法解码而黑屏。
offer.sdp = this.preferVideoCodecs(offer.sdp);
await this.pc.setLocalDescription(offer);
this.signaling.sendOffer(this.pc.localDescription.sdp, this.targetDeviceId, authType, authValue);
this.signaling.sendOffer(offer.sdp, this.targetDeviceId, authType, authValue);
this._startStats();
}
/**
* 重排 Offer SDP 中 m=video 行的视频编解码器顺序VP8/VP9 优先H264 兜底。
* 与 Android 被控端 WebRtcClient.optimizeSdp 的编解码优先级保持一致,
* 保证协商出的编码格式控制端一定可解码(编码/解码一致性)。
* @param {string} sdp 原始 SDP
* @returns {string} 重排后的 SDP
*/
preferVideoCodecs(sdp) {
const lines = sdp.split('\r\n');
const result = [];
// 先收集各类型视频负载的 payload type
const vp8 = [];
const vp9 = [];
const h264 = [];
for (const line of lines) {
const t = line.trim();
if (t.startsWith('a=rtpmap:')) {
const payload = t.split(':')[1].split(' ')[0];
if (t.includes('VP8/90000')) vp8.push(payload);
else if (t.includes('VP9/90000')) vp9.push(payload);
else if (t.includes('H264/90000')) h264.push(payload);
}
}
const ordered = [...vp8, ...vp9, ...h264];
if (ordered.length === 0) return sdp;
for (const line of lines) {
const t = line.trim();
if (t.startsWith('m=video')) {
const parts = t.split(' ');
if (parts.length > 3) {
const head = [parts[0], parts[1], parts[2]].join(' ');
const rest = parts.slice(3).filter((p) => !ordered.includes(p));
result.push([head, ...ordered, ...rest].join(' '));
continue;
}
}
result.push(line);
}
return result.join('\r\n');
}
setupDataChannel(dc) {
this.dataChannel = dc;
dc.onopen = () => this.onDataChannelState && this.onDataChannelState(true);
@@ -119,6 +177,11 @@ export class WebRtcController {
if (!this.pc) return;
try {
await this.pc.setRemoteDescription({ type: 'answer', sdp });
// 诊断:确认视频收发方向,便于区分“编解码器问题”与“连接方向问题”
const vt = this.pc.getTransceivers
? this.pc.getTransceivers().find((t) => t.receiver && t.receiver.track && t.receiver.track.kind === 'video')
: null;
console.info('[协商完成] video transceiver currentDirection=', vt ? vt.currentDirection : 'n/a');
} catch (e) {
this.onError && this.onError('设置远端描述(ANSWER)失败: ' + (e?.message || e));
throw e;