import 'package:flutter/material.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:uuid/uuid.dart'; import 'config/ice_servers.dart'; import 'controller/remote_controller.dart'; import 'utils/control_commands.dart'; import 'widgets/remote_touch_view.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'WebRTC 控制端', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), useMaterial3: true, ), home: const ControllerHome(), ); } } class ControllerHome extends StatefulWidget { const ControllerHome({super.key}); @override State createState() => _ControllerHomeState(); } class _ControllerHomeState extends State { final _serverUrlController = TextEditingController(text: kDefaultSignalServer); final _deviceIdController = TextEditingController(); final _targetController = TextEditingController(text: '981964879'); RemoteController? _controller; RTCVideoRenderer? _renderer; /// 用于在任意位置(含回调中)弹出提示。 final GlobalKey _scaffoldKey = GlobalKey(); bool _connected = false; String _status = '状态: 已停止'; String _stats = ''; double _videoAspect = 16 / 9; @override void initState() { super.initState(); _deviceIdController.text = 'controller_${const Uuid().v4().substring(0, 8)}'; } @override void dispose() { _controller?.disconnect(); _serverUrlController.dispose(); _deviceIdController.dispose(); _targetController.dispose(); super.dispose(); } void _setStatus(String status) => setState(() => _status = status); Future _connect() async { final serverUrl = _serverUrlController.text.trim(); final deviceId = _deviceIdController.text.trim(); final target = _targetController.text.trim(); if (serverUrl.isEmpty || deviceId.isEmpty || target.isEmpty) { _setStatus('请填写所有字段'); return; } _setStatus('状态: 正在连接信令服务器...'); _controller = RemoteController( serverUrl: serverUrl, deviceId: deviceId, targetDeviceId: target, ); _controller!.onStatusChanged = _setStatus; _controller!.onConnectionEstablished = () { setState(() { _connected = true; _status = '状态: 已连接 - 远程控制中'; }); }; _controller!.onDisconnected = () { setState(() => _connected = false); _setStatus('状态: 远端已断开'); }; _controller!.onIceDisconnected = (message) { _onIceDisconnected(message); }; _controller!.onTargetOffline = (message) { _onTargetOffline(message); }; _controller!.onRemoteStream = (renderer) { setState(() => _renderer = renderer); renderer.addListener(_onRendererUpdate); }; _controller!.onStats = (stats) => setState(() => _stats = stats); _controller!.connect(); } void _onRendererUpdate() { final w = _renderer?.value.width ?? 0; final h = _renderer?.value.height ?? 0; if (w > 0 && h > 0 && mounted) { setState(() => _videoAspect = w / h); } } /// ICE 断开:提示用户并自动返回连接设置面板。 Future _onIceDisconnected(String message) async { final scaffoldCtx = _scaffoldKey.currentState?.context; if (mounted && scaffoldCtx != null) { ScaffoldMessenger.of(scaffoldCtx).showSnackBar( SnackBar( content: Text(message), duration: const Duration(seconds: 3), ), ); } await _disconnect(); } /// 目标被控端不在线:提示用户并复位到连接设置面板。 Future _onTargetOffline(String message) async { final scaffoldCtx = _scaffoldKey.currentState?.context; if (mounted && scaffoldCtx != null) { ScaffoldMessenger.of(scaffoldCtx).showSnackBar( SnackBar( content: Text(message), duration: const Duration(seconds: 3), ), ); } setState(() => _connected = false); } Future _disconnect() async { await _controller?.disconnect(); _controller = null; _renderer?.removeListener(_onRendererUpdate); setState(() { _connected = false; _renderer = null; _status = '状态: 已停止'; _stats = ''; }); } @override Widget build(BuildContext context) { return Scaffold( key: _scaffoldKey, appBar: AppBar(title: const Text('WebRTC 控制端')), body: _connected ? _buildControlPanel() : _buildSetupPanel(), ); } Widget _buildSetupPanel() { return SingleChildScrollView( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'WebRTC 控制端', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), ), const SizedBox(height: 24), const Text('信令服务器地址:', style: TextStyle(fontSize: 14)), TextField( controller: _serverUrlController, decoration: const InputDecoration( hintText: 'ws://175.178.213.60:8088/ws/signal', ), ), const SizedBox(height: 16), const Text('本机设备ID:', style: TextStyle(fontSize: 14)), TextField( controller: _deviceIdController, decoration: const InputDecoration(hintText: '设备ID'), ), const SizedBox(height: 16), const Text('目标被控设备ID:', style: TextStyle(fontSize: 14)), TextField( controller: _targetController, decoration: const InputDecoration(hintText: '被控端设备ID'), ), const SizedBox(height: 24), Text( _status, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), const SizedBox(height: 24), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: _connect, child: const Text('连接被控设备'), ), ), ], ), ); } Widget _buildControlPanel() { return Stack( children: [ Container(color: Colors.black), if (_renderer != null) Center( child: AspectRatio( aspectRatio: _videoAspect, child: Stack( fit: StackFit.expand, children: [ RTCVideoView( _renderer!, objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain, ), RemoteTouchView( onTouch: (x, y) => _controller ?.sendControlCommand(ControlCommands.touch(x, y)), onSwipe: (x1, y1, x2, y2, d) => _controller ?.sendControlCommand( ControlCommands.swipe(x1, y1, x2, y2, d)), onKey: (k, a) => _controller ?.sendControlCommand(ControlCommands.key(k, a)), onLongPress: (x, y) => _controller ?.sendControlCommand(ControlCommands.longPress(x, y)), ), ], ), ), ), Positioned( top: 0, left: 0, child: Container( color: Colors.black54, padding: const EdgeInsets.all(8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _status, style: const TextStyle(color: Colors.white), ), Text( _stats, style: const TextStyle(color: Colors.white, fontSize: 12), ), ], ), ), ), Positioned( bottom: 16, right: 16, child: FloatingActionButton( onPressed: _disconnect, tooltip: '断开连接', child: const Icon(Icons.close), ), ), ], ); } }