fix: 修复自编码模式黑屏及并发问题

- 修复 MediaCodec 回调需在 configure 前设置导致的异常
- 修复解码器线程安全问题及包乱序/重复处理
- 增加关键帧前重发 SPS/PPS 以防首帧丢失导致黑屏
- 支持分辨率切换时重置解码器
- 修复 Flutter 控制端连接类型选项 UI 显示拥挤问题
This commit is contained in:
2026-07-24 09:31:58 +08:00
parent f8048e1331
commit 778c37b8db
9 changed files with 374 additions and 107 deletions

View File

@@ -27,6 +27,9 @@ class RemoteController {
/// WebRTC 连接建立(可开始远程控制)。
void Function()? onConnectionEstablished;
/// 连接失败。
void Function(String error)? onConnectionFailed;
/// 连接断开。
void Function()? onDisconnected;
@@ -48,6 +51,9 @@ class RemoteController {
/// 自编码解码纹理已就绪textureId >= 0
void Function(int textureId)? onSelfCodecReady;
/// 自编码解码器已释放(连接断开时)。
void Function()? onSelfCodecLost;
/// 被控端上报当前生效的串流模式。
void Function(int mode)? onStreamModeReport;
@@ -99,17 +105,20 @@ class RemoteController {
onConnectionEstablished?.call();
_startStats();
};
_webRtc!.onConnectionFailed = (error) => onConnectionFailed?.call(error);
_webRtc!.onDisconnected = () {
onDisconnected?.call();
};
_webRtc!.onIceDisconnected = (message) => onIceDisconnected?.call(message);
_webRtc!.onRemoteStream = (renderer) => onRemoteStream?.call(renderer);
_webRtc!.onSelfCodecReady = (id) => onSelfCodecReady?.call(id);
_webRtc!.onSelfCodecLost = () => onSelfCodecLost?.call();
_webRtc!.onStreamModeReport = (mode) => onStreamModeReport?.call(mode);
_webRtc!.onResolutionReported = (w, h) => onResolutionReported?.call(w, h);
_webRtc!.onSelfCodecNotSupported = () => onSelfCodecNotSupported?.call();
_webRtc!.initialize().catchError((e) {
onStatusChanged?.call('状态: 连接失败 - $e');
onConnectionFailed?.call(e.toString());
});
}

View File

@@ -126,6 +126,69 @@ class _ControllerHomeState extends State<ControllerHome> {
String selectedType = 'NONE';
final valueController = TextEditingController();
// 纵向选项列表,避免分段控件在窄弹窗中把中文选项挤压成两行。
Widget buildOption(
String value,
String title,
String desc,
void Function(void Function()) setDialogState,
) {
final selected = selectedType == value;
return GestureDetector(
onTap: () => setDialogState(() => selectedType = value),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
border: Border.all(
color: selected
? CupertinoColors.activeBlue
: CupertinoColors.systemGrey4,
),
borderRadius: BorderRadius.circular(10),
color: selected
? CupertinoColors.activeBlue.withValues(alpha: 0.06)
: null,
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
desc,
style: const TextStyle(
fontSize: 12,
color: CupertinoColors.systemGrey,
),
),
],
),
),
const SizedBox(width: 8),
Icon(
selected
? CupertinoIcons.check_mark_circled_solid
: CupertinoIcons.circle,
color: selected
? CupertinoColors.activeBlue
: CupertinoColors.systemGrey,
),
],
),
),
);
}
await showCupertinoDialog<void>(
context: context,
builder: (ctx) => StatefulBuilder(
@@ -134,37 +197,37 @@ class _ControllerHomeState extends State<ControllerHome> {
content: Column(
children: [
const SizedBox(height: 12),
CupertinoSegmentedControl<String>(
children: const {
'NONE': Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: Text('免密连接'),
),
'CODE': Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: Text('动态验证码'),
),
'PASSWORD': Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: Text('固定密码'),
),
},
groupValue: selectedType,
onValueChanged: (v) => setDialogState(() => selectedType = v),
buildOption(
'NONE',
'免密连接',
'被控端弹出手动确认框,无需验证码或密码',
setDialogState,
),
buildOption(
'CODE',
'动态验证码',
'使用一次性动态验证码鉴权',
setDialogState,
),
buildOption(
'PASSWORD',
'固定密码',
'使用固定连接密码鉴权',
setDialogState,
),
const SizedBox(height: 4),
CupertinoTextField(
controller: valueController,
enabled: selectedType != 'NONE',
placeholder: selectedType == 'NONE'
? '免密连接,无需填写'
: selectedType == 'PASSWORD'
? '请输入固定密码'
: '请输入动态验证码',
obscureText: true,
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
),
const SizedBox(height: 12),
if (selectedType == 'NONE')
const Text(
'免密连接:被控端将弹出手动确认对话框,无需输入验证码或密码。',
style: TextStyle(fontSize: 13, color: CupertinoColors.systemGrey),
)
else
CupertinoTextField(
controller: valueController,
placeholder: selectedType == 'PASSWORD' ? '请输入固定密码' : '请输入动态验证码',
obscureText: true,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
),
],
),
actions: [
@@ -245,6 +308,14 @@ class _ControllerHomeState extends State<ControllerHome> {
});
}
};
_controller!.onSelfCodecLost = () {
if (mounted) {
setState(() {
_selfCodecReady = false;
_selfCodecTextureId = null;
});
}
};
_controller!.onStreamModeReport = (mode) {
if (mounted) setState(() => _streamMode = mode);
};

View File

@@ -217,7 +217,13 @@ class WebRtcController {
switch (msg.action) {
case Action.REPORT_STREAM_MODE:
_currentStreamMode = msg.streamMode;
_selfCodecDecoder?.setEnabled(msg.streamMode == streamModeSelfCodec);
if (msg.streamMode == streamModeSelfCodec) {
_ensureSelfCodecDecoder().then((_) {
_selfCodecDecoder?.setEnabled(true);
});
} else {
_selfCodecDecoder?.setEnabled(false);
}
onStreamModeReport?.call(msg.streamMode);
break;
case Action.REPORT_RESOLUTION: