diff --git a/webrtc_controller_flutter/.metadata b/webrtc_controller_flutter/.metadata index 2459a95..596cd58 100644 --- a/webrtc_controller_flutter/.metadata +++ b/webrtc_controller_flutter/.metadata @@ -15,24 +15,9 @@ migration: - platform: root create_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06 base_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06 - - platform: android - create_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06 - base_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06 - platform: ios create_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06 base_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06 - - platform: linux - create_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06 - base_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06 - - platform: macos - create_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06 - base_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06 - - platform: web - create_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06 - base_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06 - - platform: windows - create_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06 - base_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06 # User provided section diff --git a/webrtc_controller_flutter/README.md b/webrtc_controller_flutter/README.md index 9e38daf..e41bbe2 100644 --- a/webrtc_controller_flutter/README.md +++ b/webrtc_controller_flutter/README.md @@ -1,17 +1,66 @@ -# webrtc_controller_flutter +# WebRTC 控制端 (webrtc_controller_flutter) -A new Flutter project. +将原生 Android 项目 **WebRTCController** 的功能移植到 Flutter,使用**跨平台**依赖,可同时运行于 **Android** 与 **iOS**。 -## Getting Started +## 功能 -This project is a starting point for a Flutter application. +对应原 Android 控制端(WebRTCController)的能力: -A few resources to get you started if this is your first Flutter project: +- 通过 WebSocket 连接信令服务器并注册为 `CONTROLLER` 设备 +- 创建 WebRTC 连接(仅接收远端视频 `recvonly` + 一条控制用 DataChannel) +- 显示被控端画面(远端视频流) +- 通过触摸 / 滑动 / 物理按键采集输入,转换为相对坐标(0.0~1.0)经 DataChannel 发送控制指令(TOUCH / SWIPE / KEY) +- 实时显示连接统计(分辨率、帧率、延迟、解码格式) -- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter) -- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources) +## 运行依赖(跨平台) -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +| 包 | 用途 | +| --- | --- | +| `flutter_webrtc` | WebRTC(Android + iOS 统一封装) | +| `web_socket_channel` | 信令 WebSocket | +| `uuid` | 生成本机设备 ID | + +> 以上包均支持 Android 与 iOS,无需编写任何平台原生代码。 + +## 目录结构 + +``` +lib/ +├── config/ice_servers.dart # ICE/TURN/STUN 配置与默认信令地址 +├── models/signal_message.dart # 信令消息模型 +├── signaling/signaling_client.dart# WebSocket 信令客户端 +├── webrtc/webrtc_controller.dart # WebRTC 连接 / DataChannel / 视频渲染 / 统计 +├── controller/remote_controller.dart # 编排:信令 + WebRTC 流程 +├── utils/control_commands.dart # 控制指令 JSON 构造 +├── widgets/remote_touch_view.dart # 触摸/按键采集(纯 Flutter,跨平台) +└── main.dart # UI(设置面板 + 控制面板) +``` + +## 运行 + +```bash +flutter pub get +flutter run # 默认运行到已连接设备 +flutter run -d android +flutter run -d ios # 需 macOS + Xcode +``` + +### iOS 注意事项 + +- 本项目已补齐 `ios/Podfile`(基于当前 Flutter 版本官方模板)。 + 在 macOS 上首次构建会自动执行 `pod install` 拉取 `flutter_webrtc`。 +- 为支持开发环境的 `ws://` 明文信令,`ios/Runner/Info.plist` 已添加 + `NSAppTransportSecurity -> NSAllowsArbitraryLoads = true`(生产环境建议改用 `wss://`)。 + +### Android 注意事项 + +- `android/app/src/main/AndroidManifest.xml` 已添加 `INTERNET` 权限与 + `android:usesCleartextTraffic="true"`(支持 `ws://`)。 + +## 对接说明 + +- **信令服务器**:需与 WebRTCControlled 项目共用同一套信令协议 + (REGISTER / OFFER / ANSWER / ICE_CANDIDATE,payload 为 JSON 字符串)。 +- **被控端**:使用 WebRTCControlled(Android)接收 OFFER 并回传 ANSWER, + 其 `InputCommandHandler` 解析 TOUCH / SWIPE / KEY 指令。 +- **ICE 服务器**:见 `lib/config/ice_servers.dart`,请按需替换为自己的 TURN 凭据。 diff --git a/webrtc_controller_flutter/android/app/build.gradle.kts b/webrtc_controller_flutter/android/app/build.gradle.kts index b9034fd..51dd372 100644 --- a/webrtc_controller_flutter/android/app/build.gradle.kts +++ b/webrtc_controller_flutter/android/app/build.gradle.kts @@ -1,5 +1,6 @@ plugins { id("com.android.application") + id("kotlin-android") // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id("dev.flutter.flutter-gradle-plugin") } diff --git a/webrtc_controller_flutter/android/app/src/main/AndroidManifest.xml b/webrtc_controller_flutter/android/app/src/main/AndroidManifest.xml index 10ed635..26ccd8e 100644 --- a/webrtc_controller_flutter/android/app/src/main/AndroidManifest.xml +++ b/webrtc_controller_flutter/android/app/src/main/AndroidManifest.xml @@ -1,8 +1,13 @@ + + + + + android:icon="@mipmap/ic_launcher" + android:usesCleartextTraffic="true"> :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/webrtc_controller_flutter/ios/Runner/Info.plist b/webrtc_controller_flutter/ios/Runner/Info.plist index cd6d54a..7cf1f2c 100644 --- a/webrtc_controller_flutter/ios/Runner/Info.plist +++ b/webrtc_controller_flutter/ios/Runner/Info.plist @@ -26,6 +26,12 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + UIApplicationSceneManifest UIApplicationSupportsMultipleScenes diff --git a/webrtc_controller_flutter/lib/config/ice_servers.dart b/webrtc_controller_flutter/lib/config/ice_servers.dart new file mode 100644 index 0000000..c6ed220 --- /dev/null +++ b/webrtc_controller_flutter/lib/config/ice_servers.dart @@ -0,0 +1,26 @@ +/// ICE 服务器配置(与 WebRTCController Android 端保持一致)。 +/// +/// 注意:TURN 用户名/密码为示例凭据,请按需替换为自己的服务器配置。 +const List> kIceServers = [ + { + 'urls': 'turn:175.178.213.60:3478', + 'username': 'fanhuitong', + 'credential': 'Fan19961207..', + }, + { + 'urls': 'turn:192.168.5.224:3478', + 'username': 'tt', + 'credential': 'fht', + }, + {'urls': 'stun:175.178.213.60:3478'}, + {'urls': 'stun:192.168.5.224:3478'}, + {'urls': 'stun:stun.l.google.com:19302'}, + {'urls': 'stun:stun1.l.google.com:19302'}, + {'urls': 'stun:stun2.l.google.com:19302'}, +]; + +/// 信令 WebSocket 默认地址。 +const String kDefaultSignalServer = 'ws://192.168.100.222:8080/ws/signal'; + +/// DataChannel 标签,控制端与被控端需一致。 +const String kDataChannelLabel = 'control_channel'; diff --git a/webrtc_controller_flutter/lib/controller/remote_controller.dart b/webrtc_controller_flutter/lib/controller/remote_controller.dart new file mode 100644 index 0000000..bd515a8 --- /dev/null +++ b/webrtc_controller_flutter/lib/controller/remote_controller.dart @@ -0,0 +1,117 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter_webrtc/flutter_webrtc.dart'; + +import '../models/signal_message.dart'; +import '../signaling/signaling_client.dart'; +import '../webrtc/webrtc_controller.dart'; + +/// 控制端编排器:组合信令客户端与 WebRTC 控制器, +/// 对外暴露连接/断开/发送指令等高层接口(对应 Android 端 MainActivity 的流程)。 +class RemoteController { + final String serverUrl; + final String deviceId; + final String targetDeviceId; + + late final SignalingClient _signaling; + WebRtcController? _webRtc; + Timer? _statsTimer; + + /// 信令状态变化(如“正在连接…”、“已连接…”)。 + void Function(String status)? onStatusChanged; + + /// WebRTC 连接建立(可开始远程控制)。 + void Function()? onConnectionEstablished; + + /// 连接断开。 + void Function()? onDisconnected; + + /// 远端视频渲染器就绪。 + void Function(RTCVideoRenderer renderer)? onRemoteStream; + + /// 统计信息刷新(每秒一次)。 + void Function(String stats)? onStats; + + RemoteController({ + required this.serverUrl, + required this.deviceId, + required this.targetDeviceId, + }); + + /// 发起连接:先连接信令服务器,成功后建立 WebRTC 并创建 Offer。 + void connect() { + onStatusChanged?.call('状态: 正在连接信令服务器...'); + + _signaling = SignalingClient(serverUrl: serverUrl, deviceId: deviceId); + _signaling.onConnected = () { + onStatusChanged?.call('状态: 已连接信令服务器,正在发起连接...'); + _initWebRtc(); + }; + _signaling.onMessage = _handleSignalMessage; + _signaling.onDisconnected = () { + onStatusChanged?.call('状态: 已断开连接'); + onDisconnected?.call(); + }; + _signaling.onError = (error) { + onStatusChanged?.call('状态: 连接错误 - $error'); + }; + _signaling.connect(); + } + + void _initWebRtc() { + _webRtc = WebRtcController( + signaling: _signaling, + deviceId: deviceId, + targetDeviceId: targetDeviceId, + ); + _webRtc!.onConnectionEstablished = () { + onConnectionEstablished?.call(); + _startStats(); + }; + _webRtc!.onDisconnected = () { + onDisconnected?.call(); + }; + _webRtc!.onRemoteStream = (renderer) => onRemoteStream?.call(renderer); + _webRtc!.initialize().catchError((e) { + onStatusChanged?.call('状态: 连接失败 - $e'); + }); + } + + void _handleSignalMessage(SignalMessage message) { + switch (message.type?.toUpperCase()) { + case 'ANSWER': + final payload = jsonDecode(message.payload!) as Map; + _webRtc?.handleAnswer(payload['sdp'] as String); + break; + case 'ICE_CANDIDATE': + final payload = jsonDecode(message.payload!) as Map; + _webRtc?.handleIceCandidate(payload); + break; + } + } + + void _startStats() { + _statsTimer?.cancel(); + _statsTimer = Timer.periodic(const Duration(seconds: 1), (_) async { + final text = await _webRtc?.getStatsText(); + if (text != null && text.isNotEmpty) { + onStats?.call(text); + } + }); + } + + /// 发送控制指令(JSON 字符串)。 + void sendControlCommand(String commandJson) { + _webRtc?.sendControlCommand(commandJson); + } + + /// 断开连接并释放资源。 + Future disconnect() async { + _statsTimer?.cancel(); + _statsTimer = null; + await _webRtc?.close(); + _webRtc = null; + _signaling.disconnect(); + } +} diff --git a/webrtc_controller_flutter/lib/main.dart b/webrtc_controller_flutter/lib/main.dart index 244a702..538bd28 100644 --- a/webrtc_controller_flutter/lib/main.dart +++ b/webrtc_controller_flutter/lib/main.dart @@ -1,4 +1,11 @@ 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()); @@ -7,116 +14,231 @@ void main() { class MyApp extends StatelessWidget { const MyApp({super.key}); - // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( - title: 'Flutter Demo', + title: 'WebRTC 控制端', theme: ThemeData( - // This is the theme of your application. - // - // TRY THIS: Try running your application with "flutter run". You'll see - // the application has a purple toolbar. Then, without quitting the app, - // try changing the seedColor in the colorScheme below to Colors.green - // and then invoke "hot reload" (save your changes or press the "hot - // reload" button in a Flutter-supported IDE, or press "r" if you used - // the command line to start the app). - // - // Notice that the counter didn't reset back to zero; the application - // state is not lost during the reload. To reset the state, use hot - // restart instead. - // - // This works for code too, not just values: Most code changes can be - // tested with just a hot reload. - colorScheme: .fromSeed(seedColor: Colors.deepPurple), + colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), + useMaterial3: true, ), - home: const MyHomePage(title: 'Flutter Demo Home Page'), + home: const ControllerHome(), ); } } -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); - - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". - - final String title; +class ControllerHome extends StatefulWidget { + const ControllerHome({super.key}); @override - State createState() => _MyHomePageState(); + State createState() => _ControllerHomeState(); } -class _MyHomePageState extends State { - int _counter = 0; +class _ControllerHomeState extends State { + final _serverUrlController = + TextEditingController(text: kDefaultSignalServer); + final _deviceIdController = TextEditingController(); + final _targetController = TextEditingController(text: '981964879'); - void _incrementCounter() { + RemoteController? _controller; + RTCVideoRenderer? _renderer; + + 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!.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); + } + } + + Future _disconnect() async { + await _controller?.disconnect(); + _controller = null; + _renderer?.removeListener(_onRendererUpdate); setState(() { - // This call to setState tells the Flutter framework that something has - // changed in this State, which causes it to rerun the build method below - // so that the display can reflect the updated values. If we changed - // _counter without calling setState(), then the build method would not be - // called again, and so nothing would appear to happen. - _counter++; + _connected = false; + _renderer = null; + _status = '状态: 已停止'; + _stats = ''; }); } @override Widget build(BuildContext context) { - // This method is rerun every time setState is called, for instance as done - // by the _incrementCounter method above. - // - // The Flutter framework has been optimized to make rerunning build methods - // fast, so that you can just rebuild anything that needs updating rather - // than having to individually change instances of widgets. return Scaffold( - appBar: AppBar( - // TRY THIS: Try changing the color here to a specific color (to - // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar - // change color while the other colors stay the same. - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. - title: Text(widget.title), - ), - body: Center( - // Center is a layout widget. It takes a single child and positions it - // in the middle of the parent. - child: Column( - // Column is also a layout widget. It takes a list of children and - // arranges them vertically. By default, it sizes itself to fit its - // children horizontally, and tries to be as tall as its parent. - // - // Column has various properties to control how it sizes itself and - // how it positions its children. Here we use mainAxisAlignment to - // center the children vertically; the main axis here is the vertical - // axis because Columns are vertical (the cross axis would be - // horizontal). - // - // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint" - // action in the IDE, or press "p" in the console), to see the - // wireframe for each widget. - mainAxisAlignment: .center, - children: [ - const Text('You have pushed the button this many times:'), - Text( - '$_counter', - style: Theme.of(context).textTheme.headlineMedium, + 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://192.168.100.222:8080/ws/signal', ), - ], - ), - ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), + ), + 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)), + ), + ], + ), + ), + ), + 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), + ), + ), + ], + ); + } } diff --git a/webrtc_controller_flutter/lib/models/signal_message.dart b/webrtc_controller_flutter/lib/models/signal_message.dart new file mode 100644 index 0000000..c17f4df --- /dev/null +++ b/webrtc_controller_flutter/lib/models/signal_message.dart @@ -0,0 +1,61 @@ +import 'dart:convert'; + +/// 信令消息模型,对应 Android 端的 SignalMessage。 +/// +/// 字段含义: +/// - [type] 消息类型:REGISTER / OFFER / ANSWER / ICE_CANDIDATE +/// - [fromDeviceId] 发送方设备 ID +/// - [toDeviceId] 接收方设备 ID +/// - [deviceType] 设备类型:CONTROLLER / CONTROLLED +/// - [payload] JSON 字符串形式的负载(SDP / ICE 候选等) +class SignalMessage { + final String? type; + final String? fromDeviceId; + final String? toDeviceId; + final String? deviceType; + final String? payload; + + const SignalMessage({ + this.type, + this.fromDeviceId, + this.toDeviceId, + this.deviceType, + this.payload, + }); + + factory SignalMessage.fromJson(Map json) => SignalMessage( + type: json['type'] as String?, + fromDeviceId: json['fromDeviceId'] as String?, + toDeviceId: json['toDeviceId'] as String?, + deviceType: json['deviceType'] as String?, + payload: json['payload'] as String?, + ); + + Map toJson() => { + if (type != null) 'type': type, + if (fromDeviceId != null) 'fromDeviceId': fromDeviceId, + if (toDeviceId != null) 'toDeviceId': toDeviceId, + if (deviceType != null) 'deviceType': deviceType, + if (payload != null) 'payload': payload, + }; + + /// 便捷构造方法:payload 为任意 Map,会自动序列化为 JSON 字符串。 + factory SignalMessage.withPayload({ + required String type, + required String fromDeviceId, + required String toDeviceId, + required String deviceType, + required Map payload, + }) { + return SignalMessage( + type: type, + fromDeviceId: fromDeviceId, + toDeviceId: toDeviceId, + deviceType: deviceType, + payload: jsonEncode(payload), + ); + } + + @override + String toString() => jsonEncode(toJson()); +} diff --git a/webrtc_controller_flutter/lib/signaling/signaling_client.dart b/webrtc_controller_flutter/lib/signaling/signaling_client.dart new file mode 100644 index 0000000..1209fa7 --- /dev/null +++ b/webrtc_controller_flutter/lib/signaling/signaling_client.dart @@ -0,0 +1,88 @@ +import 'dart:convert'; + +import 'package:web_socket_channel/web_socket_channel.dart'; + +import '../models/signal_message.dart'; + +/// 信令客户端,封装 WebSocket 连接与消息收发。 +/// +/// 连接成功后自动向服务器发送 REGISTER 注册为 CONTROLLER 设备, +/// 与 Android 端 WebSocketClient 行为一致。 +class SignalingClient { + final String serverUrl; + final String deviceId; + + WebSocketChannel? _channel; + + /// 收到信令消息回调(已解析为 SignalMessage)。 + void Function(SignalMessage message)? onMessage; + + /// 连接成功回调。 + void Function()? onConnected; + + /// 连接断开回调。 + void Function()? onDisconnected; + + /// 连接错误回调。 + void Function(String error)? onError; + + SignalingClient({required this.serverUrl, required this.deviceId}); + + /// 建立 WebSocket 连接并注册设备。 + void connect() { + try { + final uri = Uri.parse(serverUrl); + _channel = WebSocketChannel.connect(uri); + + _channel!.stream.listen( + _onData, + onDone: () => onDisconnected?.call(), + onError: (Object e) => onError?.call(e.toString()), + cancelOnError: false, + ); + + // 连接建立后注册设备(与 Android 端 onOpen -> registerDevice 对应)。 + _register(); + } catch (e) { + onError?.call(e.toString()); + } + } + + void _onData(dynamic data) { + if (data is! String) return; + try { + final map = jsonDecode(data) as Map; + final message = SignalMessage.fromJson(map); + onMessage?.call(message); + } catch (_) { + // 忽略无法解析的消息。 + } + } + + void _register() { + final msg = SignalMessage( + type: 'REGISTER', + fromDeviceId: deviceId, + deviceType: 'CONTROLLER', + ); + _send(msg); + // 通知上层已“连接”(WebSocketChannel 会缓冲发送,直到底层连接就绪); + // 后续 OFFER 等消息会排在 REGISTER 之后发送,保证服务器先完成注册。 + onConnected?.call(); + } + + /// 发送信令消息。 + void send(SignalMessage message) => _send(message); + + void _send(SignalMessage message) { + _channel?.sink.add(jsonEncode(message.toJson())); + } + + /// 关闭连接。 + void disconnect() { + _channel?.sink.close(); + _channel = null; + } + + bool get isConnected => _channel != null; +} diff --git a/webrtc_controller_flutter/lib/utils/control_commands.dart b/webrtc_controller_flutter/lib/utils/control_commands.dart new file mode 100644 index 0000000..c133cdd --- /dev/null +++ b/webrtc_controller_flutter/lib/utils/control_commands.dart @@ -0,0 +1,40 @@ +import 'dart:convert'; + +/// 控制指令构造工具,对应 Android 端 RemoteTouchView 的指令格式。 +/// +/// 指令通过 DataChannel 以 JSON 字符串发送,被控端(WebRTCControlled) +/// 的 InputCommandHandler 负责解析执行。 +class ControlCommands { + ControlCommands._(); + + /// 单击指令。坐标 x/y 为相对于屏幕的百分比(0.0 ~ 1.0)。 + static String touch(double x, double y) => jsonEncode({ + 'action': 'TOUCH', + 'x': x, + 'y': y, + }); + + /// 滑动指令。坐标为相对百分比,duration 为毫秒。 + static String swipe( + double x1, + double y1, + double x2, + double y2, + int durationMs, + ) => + jsonEncode({ + 'action': 'SWIPE', + 'x1': x1, + 'y1': y1, + 'x2': x2, + 'y2': y2, + 'duration': durationMs, + }); + + /// 按键指令。action: 0=按下, 1=抬起(对应 Android KeyEvent.ACTION_DOWN/UP)。 + static String key(int keyCode, int action) => jsonEncode({ + 'action': 'KEY', + 'keyCode': keyCode, + 'keyAction': action, + }); +} diff --git a/webrtc_controller_flutter/lib/webrtc/webrtc_controller.dart b/webrtc_controller_flutter/lib/webrtc/webrtc_controller.dart new file mode 100644 index 0000000..a61de67 --- /dev/null +++ b/webrtc_controller_flutter/lib/webrtc/webrtc_controller.dart @@ -0,0 +1,221 @@ +import 'package:flutter_webrtc/flutter_webrtc.dart'; + +import '../config/ice_servers.dart'; +import '../models/signal_message.dart'; +import '../signaling/signaling_client.dart'; + +/// 封装 WebRTC 连接逻辑(对应 Android 端 WebRtcClient)。 +/// +/// 职责: +/// - 创建 PeerConnection(recvonly 视频 + 控制用 DataChannel) +/// - 创建 Offer 并通过信令发送 +/// - 处理 Answer / ICE 候选 +/// - 通过 DataChannel 发送控制指令(触摸/滑动/按键) +/// - 采集并解析连接统计信息 +/// +/// 本类同时兼容 Android 与 iOS(基于 flutter_webrtc)。 +class WebRtcController { + final SignalingClient signaling; + final String deviceId; + final String targetDeviceId; + + RTCPeerConnection? _pc; + RTCDataChannel? _dataChannel; + + /// 远端视频渲染器,由调用方持有并显示。 + final RTCVideoRenderer renderer = RTCVideoRenderer(); + + void Function()? onConnectionEstablished; + void Function(String error)? onConnectionFailed; + void Function()? onDisconnected; + + /// 远端视频流就绪(renderer 已绑定视频轨道)。 + void Function(RTCVideoRenderer renderer)? onRemoteStream; + + WebRtcController({ + required this.signaling, + required this.deviceId, + required this.targetDeviceId, + }); + + /// 初始化渲染器、PeerConnection,并创建 Offer。 + Future initialize() async { + await renderer.initialize(); + + final configuration = { + 'iceServers': kIceServers, + 'sdpSemantics': 'unified-plan', + 'iceCandidatePoolSize': 10, + }; + + _pc = await createPeerConnection(configuration); + + _pc!.onIceCandidate = _onIceCandidate; + _pc!.onIceConnectionState = _onIceConnectionState; + _pc!.onTrack = _onTrack; + _pc!.onDataChannel = _onDataChannel; + + // 仅接收远端视频(recvonly)。 + await _pc!.addTransceiver( + kind: RTCRtpMediaType.RTCRtpMediaTypeVideo, + init: RTCRtpTransceiverInit(direction: TransceiverDirection.RecvOnly), + ); + + // 创建控制用 DataChannel(有序)。 + final dcInit = RTCDataChannelInit()..ordered = true; + _dataChannel = await _pc!.createDataChannel(kDataChannelLabel, dcInit); + _setupDataChannel(_dataChannel!); + + // 创建并发送 Offer。 + final constraints = { + 'mandatory': { + 'OfferToReceiveVideo': true, + 'OfferToReceiveAudio': false, + }, + 'optional': [], + }; + final offer = await _pc!.createOffer(constraints); + await _pc!.setLocalDescription(offer); + _sendOffer(offer.sdp!); + } + + void _onTrack(RTCTrackEvent event) { + if (event.track.kind == 'video' && event.streams.isNotEmpty) { + renderer.srcObject = event.streams[0]; + onRemoteStream?.call(renderer); + } + } + + void _onDataChannel(RTCDataChannel channel) { + // 被控端也可能主动创建 DataChannel,统一处理。 + _setupDataChannel(channel); + } + + void _setupDataChannel(RTCDataChannel channel) { + channel.onMessage = (RTCDataChannelMessage message) { + // 处理来自被控端的消息(本控制端主要发送,此处仅做日志记录)。 + // ignore: avoid_print + print('DataChannel message: ${message.text}'); + }; + channel.onDataChannelState = (RTCDataChannelState state) { + // ignore: avoid_print + print('DataChannel state: $state'); + }; + } + + void _onIceCandidate(RTCIceCandidate candidate) { + final payload = { + 'sdpMid': candidate.sdpMid, + 'sdpMLineIndex': candidate.sdpMLineIndex, + 'candidate': candidate.candidate, + }; + final msg = SignalMessage.withPayload( + type: 'ICE_CANDIDATE', + fromDeviceId: deviceId, + toDeviceId: targetDeviceId, + deviceType: 'CONTROLLER', + payload: payload, + ); + signaling.send(msg); + } + + void _onIceConnectionState(RTCIceConnectionState state) { + // ignore: avoid_print + print('ICE connection state: $state'); + if (state == RTCIceConnectionState.RTCIceConnectionStateConnected) { + onConnectionEstablished?.call(); + } else if (state == RTCIceConnectionState.RTCIceConnectionStateFailed) { + onDisconnected?.call(); + } + } + + void _sendOffer(String sdp) { + final payload = {'sdp': sdp}; + final msg = SignalMessage.withPayload( + type: 'OFFER', + fromDeviceId: deviceId, + toDeviceId: targetDeviceId, + deviceType: 'CONTROLLER', + payload: payload, + ); + signaling.send(msg); + } + + /// 处理来自信令服务器的 Answer。 + Future handleAnswer(String sdp) async { + final answer = RTCSessionDescription(sdp, 'answer'); + await _pc?.setRemoteDescription(answer); + } + + /// 处理来自信令服务器的 ICE 候选。 + Future handleIceCandidate(Map payload) async { + final candidate = RTCIceCandidate( + payload['candidate'] as String, + payload['sdpMid'] as String?, + payload['sdpMLineIndex'] as int?, + ); + await _pc?.addCandidate(candidate); + } + + /// 通过 DataChannel 发送控制指令(JSON 字符串)。 + void sendControlCommand(String commandJson) { + if (_dataChannel?.state == RTCDataChannelState.RTCDataChannelOpen) { + _dataChannel!.send(RTCDataChannelMessage(commandJson)); + } + } + + /// 采集连接统计信息,回调格式化后的文本(对应 Android 端 updateStats)。 + Future getStatsText() async { + if (_pc == null) return ''; + final reports = await _pc!.getStats(); + + dynamic width = '-'; + dynamic height = '-'; + dynamic fps = '-'; + dynamic delay = '-'; + dynamic codec = '-'; + + for (final report in reports) { + final values = report.values; + if (report.type == 'inbound-rtp' && values['kind'] == 'video') { + width = values['frameWidth'] ?? '-'; + height = values['frameHeight'] ?? '-'; + fps = values['framesPerSecond'] ?? '-'; + + final codecId = values['codecId']; + if (codecId != null) { + final codecReport = reports.where((r) => r.id == codecId).firstOrNull; + final mime = codecReport?.values['mimeType']; + if (mime is String && mime.startsWith('video/')) { + codec = mime.substring(6); + } + } + } else if (report.type == 'candidate-pair') { + if (values['nominated'] == true) { + final rtt = values['currentRoundTripTime']; + if (rtt is num) { + delay = (rtt * 1000).toStringAsFixed(0); + } + } + } + } + + return '分辨率: ${width}x$height 帧率: $fps 延迟: $delay ms\n' + '解码格式: $codec'; + } + + /// 释放所有资源。 + Future close() async { + try { + await _dataChannel?.close(); + } catch (_) {} + try { + await _pc?.close(); + } catch (_) {} + try { + await renderer.dispose(); + } catch (_) {} + _dataChannel = null; + _pc = null; + } +} diff --git a/webrtc_controller_flutter/lib/widgets/remote_touch_view.dart b/webrtc_controller_flutter/lib/widgets/remote_touch_view.dart new file mode 100644 index 0000000..7a15cc4 --- /dev/null +++ b/webrtc_controller_flutter/lib/widgets/remote_touch_view.dart @@ -0,0 +1,99 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// 远端触摸控制层(对应 Android 端 RemoteTouchView)。 +/// +/// 捕获指针(触摸/鼠标)事件,将其转换为相对于控件区域的百分比坐标 +/// (0.0 ~ 1.0),以便在不同分辨率的设备间换算。 +/// +/// - 短时间内位移很小的抬起 -> 单击(TOUCH) +/// - 否则 -> 滑动(SWIPE) +/// 同时捕获物理按键事件(KEY),用于外接键盘 / 遥控器场景。 +/// +/// 该控件完全基于 Flutter 框架实现,无需任何原生代码,可运行于 +/// Android、iOS 等平台。 +class RemoteTouchView extends StatefulWidget { + final void Function(double x, double y) onTouch; + final void Function( + double x1, + double y1, + double x2, + double y2, + int durationMs, + ) onSwipe; + final void Function(int keyCode, int action) onKey; + + const RemoteTouchView({ + super.key, + required this.onTouch, + required this.onSwipe, + required this.onKey, + }); + + @override + State createState() => _RemoteTouchViewState(); +} + +class _RemoteTouchViewState extends State { + Offset? _startPosition; + DateTime? _startTime; + + double _toRelativeX(Offset local, Size size) => + (local.dx / size.width).clamp(0.0, 1.0); + + double _toRelativeY(Offset local, Size size) => + (local.dy / size.height).clamp(0.0, 1.0); + + @override + Widget build(BuildContext context) { + return Focus( + autofocus: true, + // 捕获物理按键(外接键盘 / 遥控器等)。 + onKeyEvent: (node, event) { + if (event is KeyDownEvent) { + widget.onKey(event.logicalKey.keyId, 0); + return KeyEventResult.handled; + } else if (event is KeyUpEvent) { + widget.onKey(event.logicalKey.keyId, 1); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + child: Listener( + behavior: HitTestBehavior.opaque, + onPointerDown: (event) { + _startPosition = event.localPosition; + _startTime = DateTime.now(); + }, + onPointerUp: (event) { + final size = context.size; + if (_startPosition == null || size == null || _startTime == null) { + return; + } + final startX = _toRelativeX(_startPosition!, size); + final startY = _toRelativeY(_startPosition!, size); + final endX = _toRelativeX(event.localPosition, size); + final endY = _toRelativeY(event.localPosition, size); + final duration = DateTime.now().difference(_startTime!).inMilliseconds; + + final dx = (endX - startX).abs(); + final dy = (endY - startY).abs(); + + if (duration < 200 && dx < 0.02 && dy < 0.02) { + widget.onTouch(startX, startY); + } else { + widget.onSwipe(startX, startY, endX, endY, max(duration, 1)); + } + _startPosition = null; + _startTime = null; + }, + child: const ColoredBox( + color: Colors.transparent, + child: SizedBox.expand(), + ), + ), + ); + } +} diff --git a/webrtc_controller_flutter/linux/flutter/generated_plugin_registrant.cc b/webrtc_controller_flutter/linux/flutter/generated_plugin_registrant.cc index e71a16d..3f48831 100644 --- a/webrtc_controller_flutter/linux/flutter/generated_plugin_registrant.cc +++ b/webrtc_controller_flutter/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin"); + flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar); } diff --git a/webrtc_controller_flutter/linux/flutter/generated_plugins.cmake b/webrtc_controller_flutter/linux/flutter/generated_plugins.cmake index 2e1de87..53574ba 100644 --- a/webrtc_controller_flutter/linux/flutter/generated_plugins.cmake +++ b/webrtc_controller_flutter/linux/flutter/generated_plugins.cmake @@ -3,9 +3,11 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_webrtc ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/webrtc_controller_flutter/macos/Flutter/GeneratedPluginRegistrant.swift b/webrtc_controller_flutter/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..4f68d10 100644 --- a/webrtc_controller_flutter/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/webrtc_controller_flutter/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,8 @@ import FlutterMacOS import Foundation +import flutter_webrtc func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) } diff --git a/webrtc_controller_flutter/pubspec.lock b/webrtc_controller_flutter/pubspec.lock index cd0fdcd..c11932e 100644 --- a/webrtc_controller_flutter/pubspec.lock +++ b/webrtc_controller_flutter/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.7.0" async: dependency: transitive description: @@ -33,6 +41,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" collection: dependency: transitive description: @@ -41,6 +57,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.19.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.7" cupertino_icons: dependency: "direct main" description: @@ -49,6 +73,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.0.9" + dart_webrtc: + dependency: transitive + description: + name: dart_webrtc + sha256: f6d615bddea5e458ce180a914f3055c234ffb52fb7397a51b3491e76d6d7edb2 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.1" fake_async: dependency: transitive description: @@ -57,6 +89,22 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -75,6 +123,46 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_webrtc: + dependency: "direct main" + description: + name: flutter_webrtc + sha256: "0f89dee7f4c35dab5611f351c3897a3bfe9f7057720cc7da209b52455af4316e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.5.2" + hooks: + dependency: transitive + description: + name: hooks + sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.2" + jni: + dependency: transitive + description: + name: jni + sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.0" + jni_flutter: + dependency: transitive + description: + name: jni_flutter + sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" + js: + dependency: transitive + description: + name: js + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.2" leak_tracker: dependency: transitive description: @@ -107,6 +195,22 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "6.1.0" + logger: + dependency: transitive + description: + name: logger + sha256: "25aee487596a6257655a1e091ec2ae66bc30e7af663592cc3a27e6591e05035c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.7.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.0" matcher: dependency: transitive description: @@ -131,6 +235,22 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.18.0" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed" + url: "https://pub.flutter-io.cn" + source: hosted + version: "9.4.1" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" path: dependency: transitive description: @@ -139,6 +259,86 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.6" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.1" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.6.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.2" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.3" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.8" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.6.0" sky_engine: dependency: transitive description: flutter @@ -176,6 +376,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "93b153dcb6a26dcddee6ca087dd634b53e38c10b5aa163e8e49501a776456153" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.4.1" term_glyph: dependency: transitive description: @@ -192,6 +400,22 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.7.11" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.0" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.5.3" vector_math: dependency: transitive description: @@ -208,6 +432,54 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "15.2.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: "direct main" + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.3" + webrtc_interface: + dependency: transitive + description: + name: webrtc_interface + sha256: c6f100eac5057d9a817a60473126f9828c796d42884d498af4f339c97b21014f + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.5.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.3" sdks: dart: ">=3.12.2 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.38.4" diff --git a/webrtc_controller_flutter/pubspec.yaml b/webrtc_controller_flutter/pubspec.yaml index d54dff1..2982b85 100644 --- a/webrtc_controller_flutter/pubspec.yaml +++ b/webrtc_controller_flutter/pubspec.yaml @@ -35,6 +35,15 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 + # 跨平台 WebRTC(同时支持 Android 与 iOS) + flutter_webrtc: ^1.5.2 + + # WebSocket 信令通信 + web_socket_channel: ^3.0.3 + + # 设备 ID 生成 + uuid: ^4.4.0 + dev_dependencies: flutter_test: sdk: flutter diff --git a/webrtc_controller_flutter/windows/flutter/generated_plugin_registrant.cc b/webrtc_controller_flutter/windows/flutter/generated_plugin_registrant.cc index 8b6d468..e8559e4 100644 --- a/webrtc_controller_flutter/windows/flutter/generated_plugin_registrant.cc +++ b/webrtc_controller_flutter/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,9 @@ #include "generated_plugin_registrant.h" +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + FlutterWebRTCPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterWebRTCPlugin")); } diff --git a/webrtc_controller_flutter/windows/flutter/generated_plugins.cmake b/webrtc_controller_flutter/windows/flutter/generated_plugins.cmake index b93c4c3..f3cdbfe 100644 --- a/webrtc_controller_flutter/windows/flutter/generated_plugins.cmake +++ b/webrtc_controller_flutter/windows/flutter/generated_plugins.cmake @@ -3,9 +3,11 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_webrtc ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni ) set(PLUGIN_BUNDLED_LIBRARIES)