如何在 Flutter 中使用 WebRTC _flutter webrtc
一 .如何在 Flutter 中使用 WebRTC 实现实时音视频通信
📱 Flutter 中使用 WebRTC 实现实时音视频通话
随着实时通信技术的快速发展,WebRTC 已逐渐成为实现视频通话和直播的一种主流技术。在 Flutter 中,你同样可以轻松调用 WebRTC 来实现跨平台的音视频实时通信。
### 📥 一、添加依赖
首先打开 `pubspec.yaml` 文件,添加 flutter_webrtc 插件:
```yaml
dependencies:
flutter_webrtc: ^0.9.48
```
执行以下命令安装:
```shell
flutter pub get
```
⚙️ 二、配置平台权限
- **Android** (`android/app/src/main/AndroidManifest.xml`):
```xml
```
- **iOS** (`ios/Runner/Info.plist`):
```xml
NSMicrophoneUsageDescription
允许此应用访问麦克风以进行音视频通话
NSCameraUsageDescription
允许此应用访问摄像头以进行音视频通话
```
🚀 三、WebRTC 使用示例
下面以点对点视频通话为例,展示 Flutter 如何调用 WebRTC:
#### 1. 引入库文件
```dart
import \'package:flutter_webrtc/flutter_webrtc.dart\';
```
#### 2. 创建媒体流(MediaStream)
```dart
MediaStream? _localStream;
Future getUserMedia() async {
final Map mediaConstraints = {
\'audio\': true,
\'video\': {
\'facingMode\': \'user\', // 前置摄像头
}
};
_localStream = await navigator.mediaDevices.getUserMedia(mediaConstraints);
return _localStream!;
}
```
#### 3. 初始化 PeerConnection 并进行 SDP 交换(简化版)
建立点对点连接需创建两个终端的 PeerConnection 并交换SDP(Session Description Protocol)信息。实际情况可能需要服务器辅助,简化版示例如下:
```dart
RTCPeerConnection? _peerConnection;
createPeerConnection() async {
final Map config = {
\"iceServers\": [
{\"urls\": \"stun:stun.l.google.com:19302\"},
]
};
_peerConnection = await createPeerConnection(config);
_localStream!.getTracks().forEach((track) {
_peerConnection!.addTrack(track, _localStream!);
});
_peerConnection!.onIceCandidate = (candidate) {
// 将 candidate 发送给远程客户端
};
_peerConnection!.onTrack = (RTCTrackEvent event) {
// 远程流可以从这里获取
MediaStream remoteStream = event.streams[0];
};
RTCSessionDescription offer = await _peerConnection!.createOffer();
await _peerConnection!.setLocalDescription(offer);
// 将 offer.sdp 和 offer.type 发送给对方
}
```
接收方收到 offer 后,进行回应:
```dart
Future answerOffer(String remoteSdp) async {
await _peerConnection!.setRemoteDescription(
RTCSessionDescription(remoteSdp, \'offer\'));
RTCSessionDescription answer = await _peerConnection!.createAnswer();
await _peerConnection!.setLocalDescription(answer);
// 将 answer.sdp 和 answer.type 发送回对方
}
```
4. 使用 RTCVideoView 显示视频流
在你的 Flutter Widget 中放置两个视频容器:
```dart
RTCVideoView(
RTCVideoRenderer()..initialize()..srcObject = _localStream,
mirror: true,
);
// 远端视频流类似处理,srcObject设置为远程流即可
```
✅ 四、注意事项
- 实际生产环境需搭配 Signal Server(如 WebSocket 或 Firebase)进行信令交换。
- ICE servers 中 STUN 服务确保 NAT 穿透能力,如需完全穿透防火墙,可能需要 TURN 服务(如 coturn)。
- 要实现多人视频通话,建议采用 SFU 或 MCU 服务器架构。
📖 总结
通过使用 flutter_webrtc 包,我们可以快速构建实时音视频通信应用。本文简要讲解了在 Flutter 中调用 WebRTC 所需的基本概念及关键代码,使你可以轻松上手实现一个具有音视频功能的 App!
二 .Flutter WebRTC 与浏览器 WebRTC Demo 的互通指南
要让 Flutter 应用与浏览器中的 WebRTC Demo 进行通信,关键在于正确处理信令交换和确保跨平台兼容性。下面我将详细介绍实现流程:
一、信令服务器的搭建
两个平台要互相通信,首先需要一个共同的信令服务器。以下是基于 Node.js 和 Socket.IO 的简易信令服务器:
```javascript
const express = require(\'express\');
const http = require(\'http\');
const socketIo = require(\'socket.io\');
const app = express();
const server = http.createServer(app);
const io = socketIo(server);
// 存储连接的用户
let users = {};
io.on(\'connection\', (socket) => {
console.log(\'用户已连接:\', socket.id);
// 用户加入房间
socket.on(\'join\', (roomId) => {
socket.join(roomId);
users[socket.id] = roomId;
// 通知房间内其他用户
socket.to(roomId).emit(\'user-joined\', socket.id);
});
// 转发 offer
socket.on(\'offer\', (data) => {
socket.to(data.target).emit(\'offer\', {
sdp: data.sdp,
type: data.type,
from: socket.id
});
});
// 转发 answer
socket.on(\'answer\', (data) => {
socket.to(data.target).emit(\'answer\', {
sdp: data.sdp,
type: data.type,
from: socket.id
});
});
// 转发 ICE candidate
socket.on(\'ice-candidate\', (data) => {
socket.to(data.target).emit(\'ice-candidate\', {
candidate: data.candidate,
from: socket.id
});
});
// 断开连接
socket.on(\'disconnect\', () => {
const roomId = users[socket.id];
if (roomId) {
socket.to(roomId).emit(\'user-left\', socket.id);
delete users[socket.id];
}
});
});
server.listen(3000, () => {
console.log(\'信令服务器运行在 http://localhost:3000\');
});
```
二、Flutter 端实现
在前文的基础上,调整 Flutter WebRTC 代码以支持与浏览器通信:
```dart
import \'package:flutter/material.dart\';
import \'package:flutter_webrtc/flutter_webrtc.dart\';
import \'package:socket_io_client/socket_io_client.dart\' as IO;
class WebRTCPage extends StatefulWidget {
@override
_WebRTCPageState createState() => _WebRTCPageState();
}
class _WebRTCPageState extends State {
final RTCVideoRenderer _localRenderer = RTCVideoRenderer();
final RTCVideoRenderer _remoteRenderer = RTCVideoRenderer();
MediaStream? _localStream;
RTCPeerConnection? _peerConnection;
IO.Socket? _socket;
String roomId = \"test_room\";
@override
void initState() {
super.initState();
initRenderers();
_connectSocket();
}
// 初始化视频渲染器
Future initRenderers() async {
await _localRenderer.initialize();
await _remoteRenderer.initialize();
}
// 连接到信令服务器
void _connectSocket() {
_socket = IO.io(\'http://your-signaling-server:3000\', {
\'transports\': [\'websocket\'],
\'autoConnect\': true,
});
_socket!.on(\'connect\', (_) {
print(\'已连接到信令服务器\');
_socket!.emit(\'join\', roomId);
_initWebRTC();
});
_socket!.on(\'user-joined\', (id) {
print(\'新用户加入: $id\');
_createOffer(id);
});
_socket!.on(\'offer\', (data) async {
print(\'收到 offer\');
await _handleOffer(data);
});
_socket!.on(\'answer\', (data) async {
print(\'收到 answer\');
await _handleAnswer(data);
});
_socket!.on(\'ice-candidate\', (data) async {
print(\'收到 ICE candidate\');
await _addIceCandidate(data);
});
}
// 初始化WebRTC
Future _initWebRTC() async {
// 获取本地媒体流
final Map mediaConstraints = {
\'audio\': true,
\'video\': {
\'facingMode\': \'user\',
}
};
_localStream = await navigator.mediaDevices.getUserMedia(mediaConstraints);
setState(() {
_localRenderer.srcObject = _localStream;
});
// 创建PeerConnection配置
final Map config = {
\"iceServers\": [
{\"urls\": \"stun:stun.l.google.com:19302\"},
// 添加TURN服务器以提高连接成功率
]
};
// 创建RTC约束条件,明确使用统一计划
final Map offerSdpConstraints = {
\"mandatory\": {
\"OfferToReceiveAudio\": true,
\"OfferToReceiveVideo\": true,
},
\"optional\": [],
};
// 创建PeerConnection
_peerConnection = await createPeerConnection(config, offerSdpConstraints);
// 添加本地媒体流
_localStream!.getTracks().forEach((track) {
_peerConnection!.addTrack(track, _localStream!);
});
// 监听远程流
_peerConnection!.onTrack = (RTCTrackEvent event) {
print(\"收到远程媒体流\");
if (event.streams.isNotEmpty) {
setState(() {
_remoteRenderer.srcObject = event.streams[0];
});
}
};
// 监听ICE候选项
_peerConnection!.onIceCandidate = (RTCIceCandidate candidate) {
if(_socket != null && candidate.candidate != null) {
_socket!.emit(\'ice-candidate\', {
\'target\': \'browser\', // 发送给浏览器端
\'candidate\': candidate.toMap(),
});
}
};
}
// 创建Offer
Future _createOffer(String targetId) async {
RTCSessionDescription description = await _peerConnection!.createOffer();
await _peerConnection!.setLocalDescription(description);
// 发送Offer给对方
_socket!.emit(\'offer\', {
\'target\': targetId,
\'type\': description.type,
\'sdp\': description.sdp,
});
}
// 处理收到的Offer
Future _handleOffer(dynamic data) async {
// 确保已创建PeerConnection
if (_peerConnection == null) {
await _initWebRTC();
}
// 设置远程描述
await _peerConnection!.setRemoteDescription(
RTCSessionDescription(data[\'sdp\'], data[\'type\']),
);
// 创建并发送Answer
RTCSessionDescription answer = await _peerConnection!.createAnswer();
await _peerConnection!.setLocalDescription(answer);
_socket!.emit(\'answer\', {
\'target\': data[\'from\'],
\'type\': answer.type,
\'sdp\': answer.sdp,
});
}
// 处理收到的Answer
Future _handleAnswer(dynamic data) async {
await _peerConnection!.setRemoteDescription(
RTCSessionDescription(data[\'sdp\'], data[\'type\']),
);
}
// 添加ICE候选项
Future _addIceCandidate(dynamic data) async {
if (_peerConnection != null) {
await _peerConnection!.addCandidate(
RTCIceCandidate(
data[\'candidate\'][\'candidate\'],
data[\'candidate\'][\'sdpMid\'],
data[\'candidate\'][\'sdpMLineIndex\'],
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(\'Flutter WebRTC\')),
body: Column(
children: [
Expanded(
child: Container(
margin: EdgeInsets.all(8.0),
decoration: BoxDecoration(color: Colors.black),
child: RTCVideoView(_localRenderer, mirror: true),
),
),
Expanded(
child: Container(
margin: EdgeInsets.all(8.0),
decoration: BoxDecoration(color: Colors.black),
child: RTCVideoView(_remoteRenderer),
),
),
],
),
);
}
@override
void dispose() {
_localRenderer.dispose();
_remoteRenderer.dispose();
_localStream?.getTracks().forEach((track) => track.stop());
_peerConnection?.close();
_socket?.disconnect();
super.dispose();
}
}
```
三、浏览器端 WebRTC Demo 实现
浏览器端可以通过以下 JavaScript 代码实现与 Flutter 端的通信:
```html
.videos {
display: flex;
flex-wrap: wrap;
}
video {
width: 45%;
margin: 10px;
background: #000;
}
WebRTC 浏览器演示
const socket = io(\'http://your-signaling-server:3000\');
const roomId = \'test_room\';
let localStream;
let peerConnection;
// 配置ICE服务器
const configuration = {
iceServers: [
{ urls: \'stun:stun.l.google.com:19302\' },
// 添加TURN服务器以提高连接成功率
]
};
// 连接信令服务器
socket.on(\'connect\', async () => {
console.log(\'已连接到信令服务器\');
try {
// 获取本地媒体流
localStream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: true
});
document.getElementById(\'localVideo\').srcObject = localStream;
// 加入房间
socket.emit(\'join\', roomId);
} catch (err) {
console.error(\'获取媒体流失败:\', err);
}
});
// 处理新用户加入
socket.on(\'user-joined\', (id) => {
console.log(\'新用户加入:\', id);
startCall(id);
});
// 处理收到的offer
socket.on(\'offer\', async (data) => {
console.log(\'收到offer\');
await handleOffer(data);
});
// 处理收到的answer
socket.on(\'answer\', async (data) => {
console.log(\'收到answer\');
await handleAnswer(data);
});
// 处理收到的ICE候选项
socket.on(\'ice-candidate\', async (data) => {
console.log(\'收到ICE候选项\');
await addIceCandidate(data);
});
async function createPeerConnection() {
try {
peerConnection = new RTCPeerConnection(configuration);
// 添加本地媒体流
localStream.getTracks().forEach(track => {
peerConnection.addTrack(track, localStream);
});
// 接收远程流
peerConnection.ontrack = (event) => {
console.log(\'收到远程轨道\');
if (event.streams && event.streams[0]) {
document.getElementById(\'remoteVideo\').srcObject = event.streams[0];
}
};
// 处理ICE候选项
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
socket.emit(\'ice-candidate\', {
target: \'flutter\', // 发送给Flutter端
candidate: event.candidate
});
}
};
return peerConnection;
} catch (err) {
console.error(\'创建PeerConnection失败:\', err);
}
}
async function startCall(targetId) {
if (!peerConnection) {
await createPeerConnection();
}
try {
// 创建并发送offer
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
socket.emit(\'offer\', {
target: targetId,
type: offer.type,
sdp: offer.sdp
});
} catch (err) {
console.error(\'创建offer失败:\', err);
}
}
async function handleOffer(data) {
if (!peerConnection) {
await createPeerConnection();
}
try {
// 设置远程描述
await peerConnection.setRemoteDescription(
new RTCSessionDescription({ type: data.type, sdp: data.sdp })
);
// 创建并发送answer
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
socket.emit(\'answer\', {
target: data.from,
type: answer.type,
sdp: answer.sdp
});
} catch (err) {
console.error(\'处理offer失败:\', err);
}
}
async function handleAnswer(data) {
try {
await peerConnection.setRemoteDescription(
new RTCSessionDescription({ type: data.type, sdp: data.sdp })
);
} catch (err) {
console.error(\'处理answer失败:\', err);
}
}
async function addIceCandidate(data) {
try {
if (peerConnection) {
await peerConnection.addIceCandidate(new RTCIceCandidate(data.candidate));
}
} catch (err) {
console.error(\'添加ICE候选项失败:\', err);
}
}