refactor: 重构设置页面并修复屏幕共享黑屏问题
将安全验证和输入设置从主界面迁移至独立的 SettingsActivity,并允许用户手动选择模拟点击方式。 修复 Android 10+ 上 MediaProjection Token 传递失效导致黑屏的问题,通过静态变量保存授权结果并增加捕获延时与心跳保活机制。 优化多端视频编解码器协商,统一在 Offer SDP 中将 VP8/VP9 优先排列,H264 仅作兜底,提升跨端解码兼容性。
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user