You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 17:45:29