如何在Flutter WebRTC中通过Data Channel实现文件传输?
用Flutter WebRTC Data Channel实现文件传输的完整示例
Data Channel和媒体流的PeerConnection逻辑差异很大,不需要配置媒体轨道,核心是通过RTCDataChannel直接传输二进制数据。下面是完整的可运行示例,包含文件分片发送、接收拼接的核心逻辑:
核心依赖
确保pubspec.yaml中引入:
dependencies: flutter_webrtc: ^0.9.46 path_provider: ^2.1.2 # 用于保存接收的文件
完整代码示例
import 'dart:typed_data'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:path_provider/path_provider.dart'; class FileTransferPage extends StatefulWidget { @override _FileTransferPageState createState() => _FileTransferPageState(); } class _FileTransferPageState extends State<FileTransferPage> { late RTCPeerConnection _peerConnection; RTCDataChannel? _dataChannel; final List<Uint8List> _receivedDataChunks = []; String? _fileName; int _totalFileSize = 0; int _receivedSize = 0; // 初始化PeerConnection(信令逻辑需自行对接,这里省略) Future<void> _initPeerConnection() async { final configuration = RTCConfiguration({ 'iceServers': [ {'urls': 'stun:stun.l.google.com:19302'} ] }); _peerConnection = await createPeerConnection(configuration); // 监听远端Data Channel _peerConnection.onDataChannel = (channel) { _setupDataChannel(channel); }; // 信令相关的offer/answer/ice交换逻辑请自行实现 } // 创建发送端Data Channel Future<void> _createSendDataChannel() async { final dataChannelInit = RTCDataChannelInit(); dataChannelInit.ordered = true; // 保证数据顺序 dataChannelInit.maxRetransmits = 3; _dataChannel = await _peerConnection.createDataChannel('fileTransfer', dataChannelInit); _setupDataChannel(_dataChannel!); } // 配置Data Channel监听 void _setupDataChannel(RTCDataChannel channel) { channel.onOpen = () { print('Data Channel 已连接'); }; channel.onClose = () { print('Data Channel 已关闭'); }; channel.onMessage = (message) { if (message.isBinary) { _handleReceivedBinary(message.binary); } else { // 处理元数据(文件名、文件大小) final meta = message.text.split('|'); if (meta.length == 2) { _fileName = meta[0]; _totalFileSize = int.parse(meta[1]); _receivedSize = 0; _receivedDataChunks.clear(); print('开始接收文件: $_fileName,大小: $_totalFileSize bytes'); } } }; } // 选择并发送文件 Future<void> _selectAndSendFile() async { // 这里用模拟文件,实际项目中用文件选择器插件(如file_picker)获取文件 final file = File('/path/to/your/test/file'); // 替换为实际文件路径 if (!await file.exists()) return; final fileName = file.path.split('/').last; final fileSize = await file.length(); // 先发送元数据 await _dataChannel?.send(RTCDataChannelMessage('$fileName|$fileSize')); // 分片读取文件并发送(每次发送16KB,避免数据包过大) const chunkSize = 16 * 1024; final fileStream = file.openRead(); await for (final chunk in fileStream.transform(ChunkTransformer(chunkSize))) { await _dataChannel?.send(RTCDataChannelMessage.fromBinary(chunk)); print('已发送 ${chunk.length} bytes'); } print('文件发送完成'); } // 处理接收的二进制数据块 void _handleReceivedBinary(Uint8List chunk) { _receivedDataChunks.add(chunk); _receivedSize += chunk.length; // 进度提示 print('已接收 ${(_receivedSize / _totalFileSize * 100).toStringAsFixed(1)}%'); // 接收完成,拼接并保存文件 if (_receivedSize == _totalFileSize && _fileName != null) { _saveReceivedFile(); } } // 保存接收的文件 Future<void> _saveReceivedFile() async { final appDir = await getApplicationDocumentsDirectory(); final savePath = '${appDir.path}/$_fileName'; final file = File(savePath); // 拼接所有数据块 final totalData = Uint8List(_totalFileSize); int offset = 0; for (final chunk in _receivedDataChunks) { totalData.setRange(offset, offset + chunk.length, chunk); offset += chunk.length; } await file.writeAsBytes(totalData); print('文件已保存到: $savePath'); } @override void initState() { super.initState(); _initPeerConnection(); } @override void dispose() { _dataChannel?.close(); _peerConnection.close(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('文件传输')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( onPressed: _createSendDataChannel, child: Text('创建数据通道'), ), SizedBox(height: 20), ElevatedButton( onPressed: _selectAndSendFile, child: Text('选择并发送文件'), ), ], ), ), ); } } // 文件分片转换器 class ChunkTransformer extends StreamTransformerBase<List<int>, Uint8List> { final int chunkSize; ChunkTransformer(this.chunkSize); @override Stream<Uint8List> bind(Stream<List<int>> stream) { return Stream.multi((controller) { final buffer = BytesBuilder(); stream.listen( (data) { buffer.add(data); while (buffer.length >= chunkSize) { controller.add(Uint8List.fromList(buffer.take(chunkSize).toBytes())); } }, onDone: () { if (buffer.isNotEmpty) { controller.add(Uint8List.fromList(buffer.toBytes())); } controller.close(); }, onError: controller.addError, ); }); } }
关键注意事项
- 不要混用媒体流逻辑:Data Channel不需要
addTrack或createOffer时配置媒体约束,只需初始化PeerConnection后直接创建/监听Data Channel。 - 分片传输:WebRTC Data Channel单包大小有限制(通常建议不超过16KB),必须分片大文件,避免丢包或失败。
- 元数据先行:发送文件前先发送文件名和大小,方便接收端处理拼接和保存。
- 信令对接:示例中省略了信令交换(offer/answer/ICE候选),需自行对接你已实现的实时通信信令逻辑。
内容的提问来源于stack exchange,提问作者k.venkat nagsai
相关产品推荐
相关产品推荐

