fix: 修复自编码模式黑屏及并发问题
- 修复 MediaCodec 回调需在 configure 前设置导致的异常 - 修复解码器线程安全问题及包乱序/重复处理 - 增加关键帧前重发 SPS/PPS 以防首帧丢失导致黑屏 - 支持分辨率切换时重置解码器 - 修复 Flutter 控制端连接类型选项 UI 显示拥挤问题
This commit is contained in:
@@ -141,8 +141,9 @@ class SelfCodecDecoderPlugin :
|
||||
val st = entry.surfaceTexture()
|
||||
surfaceTexture = st
|
||||
st.setDefaultBufferSize(PLACEHOLDER_W, PLACEHOLDER_H)
|
||||
// MediaCodec 把解码帧渲染到该 SurfaceTexture。必须消费(updateTexImage)
|
||||
// 新帧,否则 SurfaceTexture 一直停留在初始(黑)帧,导致切换自编码后无画面。
|
||||
st.setOnFrameAvailableListener { texture: SurfaceTexture ->
|
||||
// 解码器将帧渲染到 Surface 后触发,这里更新纹理内容供 Flutter 显示。
|
||||
try {
|
||||
texture.updateTexImage()
|
||||
} catch (e: Exception) {
|
||||
@@ -251,8 +252,11 @@ class SelfCodecDecoderPlugin :
|
||||
}
|
||||
|
||||
override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
|
||||
val w = format.getInteger(MediaFormat.KEY_WIDTH, 0)
|
||||
val h = format.getInteger(MediaFormat.KEY_HEIGHT, 0)
|
||||
// getInteger(key, default) 需要 API 29+,用 containsKey 兼容低版本
|
||||
val w = if (format.containsKey(MediaFormat.KEY_WIDTH))
|
||||
format.getInteger(MediaFormat.KEY_WIDTH) else 0
|
||||
val h = if (format.containsKey(MediaFormat.KEY_HEIGHT))
|
||||
format.getInteger(MediaFormat.KEY_HEIGHT) else 0
|
||||
if (w > 0 && h > 0) {
|
||||
surfaceTexture?.setDefaultBufferSize(w, h)
|
||||
mainHandler.post {
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user