import 'dart:io'; import 'dart:convert'; import 'dart:async'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_joystick/flutter_joystick.dart'; import 'package:marscar_controller/main.dart'; import 'package:media_kit/media_kit.dart'; import 'package:media_kit_video/media_kit_video.dart'; import 'package:provider/provider.dart'; class PlayerPage extends StatefulWidget { const PlayerPage({super.key}); @override State createState() => _PlayerPageState(); } class _PlayerPageState extends State { late final player = Player(); late final controller = VideoController(player); bool finishConnection = false; final int callBackPeriod = 20; RawDatagramSocket? _udpSocket; String _pingResult = '正在连接图传...'; Socket? _timestampSocket; StreamSubscription? _socketSubscription; Timer? _latencyTestTimer; String _controlLatencyResult = '正在连接控制...'; Timer? _udpSendTimer; @override void initState() { super.initState(); SystemChrome.setPreferredOrientations([ DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight ]); SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); GlobalSettingState globalSettingState = context.read(); _initializeUdpSocket(); _latencyTestTimer = Timer.periodic(const Duration(seconds: 1), (timer) { if (_udpSocket == null || globalSettingState.carIP.isEmpty) return; final sendTime = DateTime.now().millisecondsSinceEpoch; final data = utf8.encode('TESTTIME=$sendTime'); try { _udpSocket!.send(data, InternetAddress(globalSettingState.carIP), 80); } catch (e) { if (mounted) { setState(() { _controlLatencyResult = '控制延迟测试失败'; }); } } }); _udpSendTimer = Timer.periodic(Duration(milliseconds: callBackPeriod), (timer) { if (_udpSocket == null || globalSettingState.carIP.isEmpty) return; List data = utf8.encode('LX=$lx&LY=$ly&RX=$rx&RY=$ry&FORWARD=$forward&BACKWARD=$backward&LEFT=$left&RIGHT=$right&UP=$up&DOWN=$down&PAW=$paw'); try { _udpSocket!.send(data, InternetAddress(globalSettingState.carIP), 80); if (reset == 1) { List data = utf8.encode('RESET=1'); _udpSocket!.send(data, InternetAddress(globalSettingState.carIP), 80); setState(() { reset = 0; }); } } catch (e) { _showErrorSnackBar('控制失败: $e'); timer.cancel(); } }); if (player.platform is NativePlayer) { final props = { 'profile': 'low-latency', }; for (final entry in props.entries) { (player.platform as NativePlayer).setProperty(entry.key, entry.value); } } player.open(Media('rtsp://${globalSettingState.cameraIP}:8554/live')); _connectToTimestampServer(globalSettingState.cameraIP); } void _initializeUdpSocket() async { try { _udpSocket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0); _udpSocket?.listen((RawSocketEvent event) { if (event == RawSocketEvent.read) { Datagram? datagram = _udpSocket!.receive(); if (datagram == null) return; final message = utf8.decode(datagram.data); if (message.startsWith('TESTTIME=')) { final sentTimestampStr = message.substring(9); final sentTimestamp = int.tryParse(sentTimestampStr); if (sentTimestamp != null) { final latency = DateTime.now().millisecondsSinceEpoch - sentTimestamp; if (mounted) { setState(() { _controlLatencyResult = '控制延迟: $latency ms'; }); } } } } }); } catch (e) { _showErrorSnackBar('控制模块初始化失败'); } } void _showErrorSnackBar(String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), backgroundColor: Colors.red, ), ); } double lx = 0.0, ly = 0.0, rx = 0.0, ry = 0.0, paw = 0.0; int forward = 0, backward = 0, left = 0, right = 0, up = 0, down = 0, reset = 0; bool hasSendZeroPack = false; void _connectToTimestampServer(String ip) async { if (ip.isEmpty) { if (!mounted) return; setState(() { _pingResult = 'IP地址未设置'; }); return; } try { if (!mounted) return; setState(() { _pingResult = '正在连接图传服务...'; }); _timestampSocket = await Socket.connect(ip, 8555, timeout: const Duration(seconds: 5)); _socketSubscription = _timestampSocket?.listen( (data) { setState(() { finishConnection = true; }); final message = utf8.decode(data).trim(); final serverTimestampStr = message.split('\n').last; if (serverTimestampStr.isNotEmpty) { final serverTimestamp = int.tryParse(serverTimestampStr); if (serverTimestamp != null) { final clientTimestamp = DateTime.now().millisecondsSinceEpoch; final latency = clientTimestamp - serverTimestamp; if (mounted) { setState(() { _pingResult = '图传延迟: ${latency.abs()} ms'; }); } } } }, onError: (error) { if (!mounted) return; setState(() { _pingResult = '图传出错'; finishConnection = true; }); _showErrorSnackBar('图传出错'); _timestampSocket?.destroy(); }, onDone: () { if (!mounted) return; setState(() { _pingResult = '图传已断开'; finishConnection = true; }); _showErrorSnackBar('图传已断开'); _timestampSocket?.destroy(); }, cancelOnError: true, ); } catch (e) { setState(() { _pingResult = '连接图传失败'; finishConnection = true; }); _showErrorSnackBar('连接图传失败'); } } @override void dispose() { SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, ]); player.dispose(); _socketSubscription?.cancel(); _timestampSocket?.destroy(); _udpSendTimer?.cancel(); _udpSocket?.close(); _latencyTestTimer?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Theme.of(context).primaryColor, body: Center( child: Stack( children: [ SizedBox( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height, child: Video( controller: controller, controls: (state) => const SizedBox.shrink(), ), ), if(finishConnection) Positioned( top: 8, left: 64, child: ElevatedButton( onPressed: () { GlobalSettingState globalSettingState = context.read(); globalSettingState.setCameraIP(''); }, style: ButtonStyle( backgroundColor: WidgetStateProperty.all(Color.fromARGB(60, 255, 255, 255)), iconColor: WidgetStateProperty.all(Color.fromARGB(200, 255, 255, 255)), ), child: Icon(Icons.arrow_back), ), ), Positioned( top: 8, right: 64, child: Container( padding: EdgeInsets.all(8.0), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8.0), ), child: Text( '$_pingResult\n$_controlLatencyResult', style: TextStyle(color: Colors.white, fontSize: 16), ), ), ), Positioned( left: 64, bottom: 32, child: Joystick( period: Duration(milliseconds: callBackPeriod), stick: CircleAvatar( radius: 20, backgroundColor: Color.fromARGB(200, 255, 255, 255), ), base: JoystickSquareBase( size: 160, decoration: JoystickBaseDecoration( color: Color.fromARGB(60, 200, 200, 200), ) ), stickOffsetCalculator: const RectangleStickOffsetCalculator(), listener: (details) { setState(() { lx = details.x * 127 + 127.5; ly = details.y * 127 + 127.5; }); }, ), ), Positioned( right: 128, bottom: 180, child: Joystick( period: Duration(milliseconds: callBackPeriod), stick: CircleAvatar( radius: 20, backgroundColor: Color.fromARGB(200, 255, 255, 255), ), base: Container( width: 160, height: 160, decoration: BoxDecoration( color: Color.fromARGB(60, 200, 200, 200), shape: BoxShape.circle, border: BoxBorder.all( color: Theme.of(context).colorScheme.outline, width: 1 ) ), ), listener: (details) { if(details.x == 0 && details.y == 0) { left = right = forward = backward = 0; } else { var angle = -atan2(details.y, details.x), pi_18 = pi / 18.0; setState(() { left = angle < - pi_18 * 12 || angle > pi_18 * 12 ? 1 : 0; right = angle > - pi_18 * 6 && angle < pi_18 * 6 ? 1 : 0; forward = angle > pi_18 * 3 && angle < pi_18 * 15 ? 1 : 0; backward = angle > - pi_18 * 15 && angle < - pi_18 * 3 ? 1 : 0; }); } }, ), ), Positioned( bottom: 180, right: 64, child: Joystick( period: Duration(milliseconds: callBackPeriod), mode: JoystickMode.vertical, stick: CircleAvatar( radius: 20, backgroundColor: Color.fromARGB(200, 255, 255, 255), ), base: Container( width: 32, height: 160, decoration: BoxDecoration( color: Color.fromARGB(60, 255, 255, 255), borderRadius: const BorderRadius.all( Radius.circular(20) ), border: BoxBorder.all( color: Theme.of(context).colorScheme.outline, width: 1 ) ) ), listener: (details) { setState(() { up = details.y < 0 ? 1: 0; down = details.y > 0 ? 1: 0; }); }, ), ), Positioned( bottom: 100, right: 64, child: Joystick( period: Duration(milliseconds: callBackPeriod), mode: JoystickMode.horizontal, stick: CircleAvatar( radius: 20, backgroundColor: Color.fromARGB(200, 255, 255, 255), ), base: Container( width: 160, height: 32, decoration: BoxDecoration( color: Color.fromARGB(60, 255, 255, 255), borderRadius: const BorderRadius.all( Radius.circular(20) ), border: BoxBorder.all( color: Theme.of(context).colorScheme.outline, width: 1 ) ) ), listener: (details) { setState(() { ry = details.x * 127 + 127.5; }); }, ), ), Positioned( bottom: 40, right: 64, child: Joystick( period: Duration(milliseconds: callBackPeriod), mode: JoystickMode.horizontal, stick: CircleAvatar( radius: 20, backgroundColor: Color.fromARGB(200, 255, 255, 255), ), base: Container( width: 160, height: 32, decoration: BoxDecoration( color: Color.fromARGB(60, 255, 255, 255), borderRadius: const BorderRadius.all( Radius.circular(20) ), border: BoxBorder.all( color: Theme.of(context).colorScheme.outline, width: 1 ) ) ), listener: (details) { setState(() { rx = details.x * 127 + 127.5; }); }, ), ), Positioned( bottom: 30, right: 256, child: Joystick( period: Duration(milliseconds: callBackPeriod), mode: JoystickMode.vertical, stick: CircleAvatar( radius: 20, backgroundColor: Color.fromARGB(200, 255, 255, 255), ), base: Container( width: 32, height: 120, decoration: BoxDecoration( color: Color.fromARGB(60, 255, 255, 255), borderRadius: const BorderRadius.all( Radius.circular(20) ), border: BoxBorder.all( color: Theme.of(context).colorScheme.outline, width: 1 ) ) ), listener: (details) { setState(() { paw = details.y / 2 + 0.5; }); }, ), ), if(finishConnection) Positioned( top: 8, left: 140, child: IconButton.filled( iconSize: 24.0, onPressed: () { setState(() { reset = 1; }); }, icon: Icon(Icons.refresh), style: ButtonStyle( backgroundColor: WidgetStateProperty.all(Color.fromARGB(60, 255, 255, 255)), iconColor: WidgetStateProperty.all(Color.fromARGB(200, 255, 255, 255)) ), ) ), ], ), ), ); } }