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

@@ -1,5 +1,5 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
org.gradle.java.home=C\:/Program Files/Java/jdk-21
#org.gradle.java.home=C\:/Program Files/Java/jdk-21
kotlin.daemon.jvm.options=-Xmx4G
kotlin.compiler.execution.strategy=daemon
android.useAndroidX=true

View File

@@ -128,15 +128,82 @@ class WebRtcController {
'optional': [],
};
final offer = await _pc!.createOffer(constraints);
await _pc!.setLocalDescription(offer);
_sendOffer(offer.sdp!, authType: authType, authValue: authValue);
// 与 WebRTCControlled 的 optimizeSdp 保持一致Offer 的 m=video 行优先 VP8/VP9
// H264 兜底,确保各控制端(桌面/浏览器/Chromium/Linux都能稳定解码
// 即编码端(被控端)与解码端(控制端)协商出一致的、可解码的视频格式。
final reorderedSdp = _preferVideoCodecs(offer.sdp!);
final offerWithPref = RTCSessionDescription(reorderedSdp, offer.type);
await _pc!.setLocalDescription(offerWithPref);
_sendOffer(reorderedSdp, authType: authType, authValue: authValue);
}
/// 重排 Offer SDP 中 `m=video` 行的视频编解码器顺序VP8/VP9 优先H264 兜底。
///
/// 与 Android 被控端 [WebRtcClient.optimizeSdp] 的编解码优先级保持一致,
/// 保证协商出的编码格式控制端一定可解码(编码/解码一致性)。
String _preferVideoCodecs(String sdp) {
final lines = sdp.split('\r\n');
final vp8 = <String>[];
final vp9 = <String>[];
final h264 = <String>[];
for (final line in lines) {
final t = line.trim();
if (t.startsWith('a=rtpmap:')) {
final payload = t.split(':')[1].split(' ')[0];
if (t.contains('VP8/90000')) {
vp8.add(payload);
} else if (t.contains('VP9/90000')) {
vp9.add(payload);
} else if (t.contains('H264/90000')) {
h264.add(payload);
}
}
}
final ordered = <String>[...vp8, ...vp9, ...h264];
if (ordered.isEmpty) return sdp;
final result = <String>[];
for (final line in lines) {
final t = line.trim();
if (t.startsWith('m=video')) {
final parts = t.split(' ');
if (parts.length > 3) {
final head = '${parts[0]} ${parts[1]} ${parts[2]}';
final rest = parts
.skip(3)
.where((p) => !ordered.contains(p))
.toList();
result.add([head, ...ordered, ...rest].join(' '));
continue;
}
}
result.add(line);
}
return result.join('\r\n');
}
void _onTrack(RTCTrackEvent event) {
if (event.track.kind == 'video' && event.streams.isNotEmpty) {
renderer.srcObject = event.streams[0];
onRemoteStream?.call(renderer);
if (event.track.kind != 'video') return;
_bindRemoteVideo(event);
}
/// 绑定远端视频轨道到渲染器。
///
/// 与 Web 端 WebRtcController.ontrack 的兜底逻辑保持一致:
/// 某些平台/协商场景下Unified Plan + recvonly`event.streams` 可能为空,
/// 此时必须用 `event.track` 自行构造 MediaStream否则 renderer.srcObject
/// 为空 -> 控制端拿不到画面(黑屏/一直显示“等待画面”),但控制通道不受影响。
Future<void> _bindRemoteVideo(RTCTrackEvent event) async {
MediaStream stream;
if (event.streams.isNotEmpty) {
stream = event.streams[0];
} else {
// event.streams 为空:用 track 自行构造 MediaStream。
stream = await createLocalMediaStream('remoteVideo');
await stream.addTrack(event.track);
}
renderer.srcObject = stream;
onRemoteStream?.call(renderer);
}
void _onDataChannel(RTCDataChannel channel) {