feat: 增加信令自动重连保活与控制端视频录制
- 信令客户端增加心跳保活与指数退避自动重连机制 - 服务端优化传输异常日志级别,客户端断开不再报错 - 被控端信令断开时保持服务运行并自动重连 - Flutter 控制端增加远程视频录制功能,兼容标准 WebRTC 模式切换 - 各端连接按钮增加“连接中”状态防重复点击
This commit is contained in:
@@ -9,6 +9,7 @@ import 'controller/remote_controller.dart';
|
||||
import 'utils/control_commands.dart';
|
||||
import 'utils/device_utils.dart';
|
||||
import 'webrtc/self_codec_decoder.dart';
|
||||
import 'webrtc/video_recorder.dart';
|
||||
import 'widgets/remote_touch_view.dart';
|
||||
|
||||
void main() {
|
||||
@@ -52,6 +53,8 @@ class _ControllerHomeState extends State<ControllerHome> {
|
||||
RTCVideoRenderer? _renderer;
|
||||
|
||||
bool _connected = false;
|
||||
/// 是否正在连接信令服务器 / 建立 WebRTC(连接过程中禁用“连接”按钮)。
|
||||
bool _connecting = false;
|
||||
String _status = '状态: 已停止';
|
||||
String _stats = '';
|
||||
double _videoAspect = 16 / 9;
|
||||
@@ -62,6 +65,13 @@ class _ControllerHomeState extends State<ControllerHome> {
|
||||
bool _selfCodecReady = false;
|
||||
bool _selfCodecSupported = true;
|
||||
|
||||
/// 远程视频录制器(基于 flutter_webrtc 的 MediaRecorder)。
|
||||
final VideoRecorder _videoRecorder = VideoRecorder();
|
||||
bool _recording = false;
|
||||
String _recordStatus = '';
|
||||
/// 自编码模式下请求录制时,先切回 WebRTC 标准模式、待就绪后再开始录制。
|
||||
bool _pendingRecordStart = false;
|
||||
|
||||
/// 分辨率预设:width 为长边像素;0 表示被控端原生分辨率;
|
||||
/// height 传 0(由被控端按屏幕宽高比计算),fps 传 0(沿用当前帧率)。
|
||||
int _selectedResolution = 0;
|
||||
@@ -88,6 +98,7 @@ class _ControllerHomeState extends State<ControllerHome> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_videoRecorder.dispose();
|
||||
_controller?.disconnect();
|
||||
_serverUrlController.dispose();
|
||||
_deviceIdController.dispose();
|
||||
@@ -266,6 +277,7 @@ class _ControllerHomeState extends State<ControllerHome> {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _connecting = true);
|
||||
_setStatus('状态: 正在连接信令服务器...');
|
||||
|
||||
_controller = RemoteController(
|
||||
@@ -279,11 +291,19 @@ class _ControllerHomeState extends State<ControllerHome> {
|
||||
_controller!.onConnectionEstablished = () {
|
||||
setState(() {
|
||||
_connected = true;
|
||||
_connecting = false;
|
||||
_status = '状态: 已连接 - 远程控制中';
|
||||
});
|
||||
};
|
||||
_controller!.onConnectionFailed = (error) {
|
||||
if (mounted) setState(() => _connecting = false);
|
||||
_showAlert('连接失败:$error');
|
||||
};
|
||||
_controller!.onDisconnected = () {
|
||||
setState(() => _connected = false);
|
||||
setState(() {
|
||||
_connected = false;
|
||||
_connecting = false;
|
||||
});
|
||||
_setStatus('状态: 远端已断开');
|
||||
};
|
||||
_controller!.onIceDisconnected = (message) {
|
||||
@@ -298,6 +318,10 @@ class _ControllerHomeState extends State<ControllerHome> {
|
||||
_controller!.onRemoteStream = (renderer) {
|
||||
setState(() => _renderer = renderer);
|
||||
renderer.addListener(_onRendererUpdate);
|
||||
if (_pendingRecordStart) {
|
||||
_pendingRecordStart = false;
|
||||
_startRecording();
|
||||
}
|
||||
};
|
||||
_controller!.onStats = (stats) => setState(() => _stats = stats);
|
||||
_controller!.onSelfCodecReady = (textureId) {
|
||||
@@ -318,6 +342,10 @@ class _ControllerHomeState extends State<ControllerHome> {
|
||||
};
|
||||
_controller!.onStreamModeReport = (mode) {
|
||||
if (mounted) setState(() => _streamMode = mode);
|
||||
if (mode == SelfCodecDecoder.streamModeWebRtc && _pendingRecordStart) {
|
||||
_pendingRecordStart = false;
|
||||
_startRecording();
|
||||
}
|
||||
};
|
||||
_controller!.onResolutionReported = (w, h) {
|
||||
if (w > 0 && h > 0 && mounted) {
|
||||
@@ -363,13 +391,19 @@ class _ControllerHomeState extends State<ControllerHome> {
|
||||
/// 目标被控端不在线:提示用户并复位到连接设置面板。
|
||||
Future<void> _onTargetOffline(String message) async {
|
||||
_showAlert(message);
|
||||
setState(() => _connected = false);
|
||||
setState(() {
|
||||
_connected = false;
|
||||
_connecting = false;
|
||||
});
|
||||
}
|
||||
|
||||
/// 被控端拒绝连接请求:提示用户并复位到连接设置面板。
|
||||
Future<void> _onConnectionRejected(String message) async {
|
||||
_showAlert(message);
|
||||
setState(() => _connected = false);
|
||||
setState(() {
|
||||
_connected = false;
|
||||
_connecting = false;
|
||||
});
|
||||
}
|
||||
|
||||
/// 弹出分辨率选择菜单(iOS 风格 ActionSheet)。
|
||||
@@ -426,13 +460,70 @@ class _ControllerHomeState extends State<ControllerHome> {
|
||||
_controller?.sendStreamMode(next);
|
||||
}
|
||||
|
||||
/// 切换远程视频录制:开始 / 停止。
|
||||
Future<void> _toggleRecord() async {
|
||||
if (_recording) {
|
||||
await _stopRecording();
|
||||
return;
|
||||
}
|
||||
if (_renderer == null) {
|
||||
_showAlert('尚未连接或没有视频画面,无法录制');
|
||||
return;
|
||||
}
|
||||
if (_streamMode == SelfCodecDecoder.streamModeSelfCodec) {
|
||||
// 自编码模式下没有 WebRTC 视频轨道,先切回标准模式再开始录制。
|
||||
_pendingRecordStart = true;
|
||||
setState(() => _recordStatus = '正在切回标准模式以开始录制...');
|
||||
_controller?.sendStreamMode(SelfCodecDecoder.streamModeWebRtc);
|
||||
return;
|
||||
}
|
||||
await _startRecording();
|
||||
}
|
||||
|
||||
/// 开始录制当前远端视频轨道。
|
||||
Future<void> _startRecording() async {
|
||||
if (_recording || !mounted) return;
|
||||
final stream = _renderer?.srcObject;
|
||||
if (stream == null) {
|
||||
_showAlert('尚未接收到视频画面,无法录制');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final ok = await _videoRecorder.start(stream);
|
||||
if (ok && mounted) {
|
||||
setState(() {
|
||||
_recording = true;
|
||||
_recordStatus = '录制中...';
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() => _recordStatus = '');
|
||||
_showAlert('开始录制失败:$e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 停止录制并提示保存位置。
|
||||
Future<void> _stopRecording() async {
|
||||
final path = await _videoRecorder.stop();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_recording = false;
|
||||
_recordStatus = path != null ? '已保存:$path' : '录制已停止';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _disconnect() async {
|
||||
await _videoRecorder.dispose();
|
||||
await _controller?.disconnect();
|
||||
_controller = null;
|
||||
_renderer?.removeListener(_onRendererUpdate);
|
||||
_lastResizedAspect = 0;
|
||||
setState(() {
|
||||
_connected = false;
|
||||
_connecting = false;
|
||||
_renderer = null;
|
||||
_status = '状态: 已停止';
|
||||
_stats = '';
|
||||
@@ -440,6 +531,9 @@ class _ControllerHomeState extends State<ControllerHome> {
|
||||
_selfCodecTextureId = null;
|
||||
_selfCodecReady = false;
|
||||
_selfCodecSupported = true;
|
||||
_recording = false;
|
||||
_recordStatus = '';
|
||||
_pendingRecordStart = false;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -501,8 +595,8 @@ class _ControllerHomeState extends State<ControllerHome> {
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: CupertinoButton.filled(
|
||||
onPressed: _showAuthDialog,
|
||||
child: const Text('连接被控设备'),
|
||||
onPressed: _connecting ? null : _showAuthDialog,
|
||||
child: Text(_connecting ? '正在连接...' : '连接被控设备'),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -569,6 +663,33 @@ class _ControllerHomeState extends State<ControllerHome> {
|
||||
],
|
||||
),
|
||||
),
|
||||
// 远程视频录制开关
|
||||
CupertinoButton(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
onPressed: _toggleRecord,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
_recording
|
||||
? CupertinoIcons.stop_circle
|
||||
: CupertinoIcons.video_camera,
|
||||
color: _recording
|
||||
? CupertinoColors.destructiveRed
|
||||
: CupertinoColors.white,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
_recording ? '停止' : '录制',
|
||||
style: const TextStyle(
|
||||
color: CupertinoColors.white,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 分隔线
|
||||
Container(
|
||||
width: 1,
|
||||
@@ -668,6 +789,14 @@ class _ControllerHomeState extends State<ControllerHome> {
|
||||
_stats,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 12),
|
||||
),
|
||||
if (_recordStatus.isNotEmpty)
|
||||
Text(
|
||||
_recordStatus,
|
||||
style: const TextStyle(
|
||||
color: CupertinoColors.systemOrange,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user