将 WebRTC 集成到 Flutter 中可以带来多种优势,并实现对现代实时通信应用至关重要的各种功能。
在 Flutter 应用程序中需要 WebRTC 的一些原因
实时通信
WebRTC 可直接在对等体(如聊天或视频通话中的用户)之间进行实时音频和视频通信,而无需中间服务器进行媒体处理。这一功能对于构建视频会议、实时流媒体或语音呼叫等应用至关重要。
跨平台兼容性
Flutter 允许您为多个平台(iOS、Android、Web、桌面)构建应用程序。WebRTC 可跨平台集成到 Flutter 应用程序中,无论设备或操作系统如何,都能提供一致的实时通信功能。
点对点连接
WebRTC 支持点对点通信,这意味着数据(音频、视频或其他自定义数据)可在客户端(如移动设备或浏览器)之间直接交换。这种分散式方法可减少延迟并提高整体性能。
交互式和吸引人的应用
通过利用 Flutter 中的 WebRTC,您可以创建支持用户之间实时互动的交互式和吸引人的应用程序。这可能包括视频会议应用程序、远程协作工具、带有语音聊天功能的多人游戏或直播应用程序。
高效媒体流
WebRTC 通过动态适应网络条件(如带宽和延迟)来优化媒体流。这确保了即使在不同的网络环境下也能获得流畅的用户体验,使其适用于需要可靠音频和视频传输的应用。
安全通信
WebRTC 包括内置的安全功能,如媒体流加密(SRTP – 安全实时传输协议)和信令加密(使用 HTTPS 或 WebSocket)。这有助于确保对等方之间的通信安全,并防止未经授权的访问。
如何集成 Flutter WebRTC?
添加包:
flutter_webrtc: ^0.9.27
// 用于 Firebase 连接
firebase_core: ^2.11.0
cloud_firestore: ^4.6.0
cloud_firestore_web: ^3.4.3
在Menifest中添加权限:
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
为 WebRTC 创建信令类:
import 'dart:convert';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
typedef void StreamStateCallback(MediaStream stream);
class Signaling {
Map<String, dynamic> configuration = {
'iceServers': [
{
'urls': [
'stun:stun1.l.google.com:19302',
'stun:stun2.l.google.com:19302'
]
}
]
};
RTCPeerConnection? peerConnection;
MediaStream? localStream;
MediaStream? remoteStream;
String? roomId;
String? currentRoomText;
StreamStateCallback? onAddRemoteStream;
Future<String> createRoom(RTCVideoRenderer remoteRenderer) async {
FirebaseFirestore db = FirebaseFirestore.instance;
DocumentReference roomRef = db.collection('rooms').doc();
print('Create PeerConnection with configuration: $configuration');
peerConnection = await createPeerConnection(configuration);
registerPeerConnectionListeners();
localStream?.getTracks().forEach((track) {
peerConnection?.addTrack(track, localStream!);
});
// Code for collecting ICE candidates below
var callerCandidatesCollection = roomRef.collection('callerCandidates');
peerConnection?.onIceCandidate = (RTCIceCandidate candidate) {
print('Got candidate: ${candidate.toMap()}');
callerCandidatesCollection.add(candidate.toMap());
};
// Finish Code for collecting ICE candidate
// Add code for creating a room
RTCSessionDescription offer = await peerConnection!.createOffer();
await peerConnection!.setLocalDescription(offer);
print('Created offer: $offer');
Map<String, dynamic> roomWithOffer = {'offer': offer.toMap()};
await roomRef.set(roomWithOffer);
var roomId = roomRef.id;
print('New room created with SDK offer. Room ID: $roomId');
currentRoomText = 'Current room is $roomId - You are the caller!';
// Created a Room
peerConnection?.onTrack = (RTCTrackEvent event) {
print('Got remote track: ${event.streams[0]}');
event.streams[0].getTracks().forEach((track) {
print('Add a track to the remoteStream $track');
remoteStream?.addTrack(track);
});
};
// Listening for remote session description below
roomRef.snapshots().listen((snapshot) async {
print('Got updated room: ${snapshot.data()}');
Map<String, dynamic> data = snapshot.data() as Map<String, dynamic>;
if (peerConnection?.getRemoteDescription() != null &&
data['answer'] != null) {
var answer = RTCSessionDescription(
data['answer']['sdp'],
data['answer']['type'],
);
print("Someone tried to connect");
await peerConnection?.setRemoteDescription(answer);
}
});
// Listening for remote session description above
// Listen for remote Ice candidates below
roomRef.collection('calleeCandidates').snapshots().listen((snapshot) {
snapshot.docChanges.forEach((change) {
if (change.type == DocumentChangeType.added) {
Map<String, dynamic> data = change.doc.data() as Map<String, dynamic>;
print('Got new remote ICE candidate: ${jsonEncode(data)}');
peerConnection!.addCandidate(
RTCIceCandidate(
data['candidate'],
data['sdpMid'],
data['sdpMLineIndex'],
),
);
}
});
});
// Listen for remote ICE candidates above
return roomId;
}
Future<void> joinRoom(String roomId, RTCVideoRenderer remoteVideo) async {
FirebaseFirestore db = FirebaseFirestore.instance;
print(roomId);
DocumentReference roomRef = db.collection('rooms').doc('$roomId');
var roomSnapshot = await roomRef.get();
print('Got room ${roomSnapshot.exists}');
if (roomSnapshot.exists) {
print('Create PeerConnection with configuration: $configuration');
peerConnection = await createPeerConnection(configuration);
registerPeerConnectionListeners();
localStream?.getTracks().forEach((track) {
peerConnection?.addTrack(track, localStream!);
});
// Code for collecting ICE candidates below
var calleeCandidatesCollection = roomRef.collection('calleeCandidates');
peerConnection!.onIceCandidate = (RTCIceCandidate? candidate) {
if (candidate == null) {
print('onIceCandidate: complete!');
return;
}
print('onIceCandidate: ${candidate.toMap()}');
calleeCandidatesCollection.add(candidate.toMap());
};
// Code for collecting ICE candidate above
peerConnection?.onTrack = (RTCTrackEvent event) {
print('Got remote track: ${event.streams[0]}');
event.streams[0].getTracks().forEach((track) {
print('Add a track to the remoteStream: $track');
remoteStream?.addTrack(track);
});
};
// Code for creating SDP answer below
var data = roomSnapshot.data() as Map<String, dynamic>;
print('Got offer $data');
var offer = data['offer'];
await peerConnection?.setRemoteDescription(
RTCSessionDescription(offer['sdp'], offer['type']),
);
var answer = await peerConnection!.createAnswer();
print('Created Answer $answer');
await peerConnection!.setLocalDescription(answer);
Map<String, dynamic> roomWithAnswer = {
'answer': {'type': answer.type, 'sdp': answer.sdp}
};
await roomRef.update(roomWithAnswer);
// Finished creating SDP answer
// Listening for remote ICE candidates below
roomRef.collection('callerCandidates').snapshots().listen((snapshot) {
snapshot.docChanges.forEach((document) {
var data = document.doc.data() as Map<String, dynamic>;
print(data);
print('Got new remote ICE candidate: $data');
peerConnection!.addCandidate(
RTCIceCandidate(
data['candidate'],
data['sdpMid'],
data['sdpMLineIndex'],
),
);
});
});
}
}
Future<void> openUserMedia(
RTCVideoRenderer localVideo,
RTCVideoRenderer remoteVideo,
) async {
var stream = await navigator.mediaDevices
.getUserMedia(
{
'video': true,
'audio': true
});
localVideo.srcObject = stream;
localStream = stream;
remoteVideo.srcObject = await createLocalMediaStream('key');
}
Future<void> hangUp(RTCVideoRenderer localVideo) async {
List<MediaStreamTrack> tracks = localVideo.srcObject!.getTracks();
tracks.forEach((track) {
track.stop();
});
if (remoteStream != null) {
remoteStream!.getTracks().forEach((track) => track.stop());
}
if (peerConnection != null) peerConnection!.close();
if (roomId != null) {
var db = FirebaseFirestore.instance;
var roomRef = db.collection('rooms').doc(roomId);
var calleeCandidates = await roomRef.collection('calleeCandidates').get();
calleeCandidates.docs.forEach((document) => document.reference.delete());
var callerCandidates = await roomRef.collection('callerCandidates').get();
callerCandidates.docs.forEach((document) => document.reference.delete());
await roomRef.delete();
}
localStream!.dispose();
remoteStream?.dispose();
}
void registerPeerConnectionListeners() {
peerConnection?.onIceGatheringState = (RTCIceGatheringState state) {
print('ICE gathering state changed: $state');
};
peerConnection?.onConnectionState = (RTCPeerConnectionState state) {
print('Connection state change: $state');
};
peerConnection?.onSignalingState = (RTCSignalingState state) {
print('Signaling state change: $state');
};
peerConnection?.onIceGatheringState = (RTCIceGatheringState state) {
print('ICE connection state change: $state');
};
peerConnection?.onAddStream = (MediaStream stream) {
print("Add remote stream");
onAddRemoteStream?.call(stream);
remoteStream = stream;
};
}
}
完整代码:
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:webrtc_tutorial/signaling.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key}) : super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
Signaling signaling = Signaling();
RTCVideoRenderer _localRenderer = RTCVideoRenderer();
RTCVideoRenderer _remoteRenderer = RTCVideoRenderer();
String? roomId;
TextEditingController textEditingController = TextEditingController(text: '');
@override
void initState() {
_localRenderer.initialize();
_remoteRenderer.initialize();
signaling.onAddRemoteStream = ((stream) {
_remoteRenderer.srcObject = stream;
setState(() {});
});
super.initState();
}
@override
void dispose() {
_localRenderer.dispose();
_remoteRenderer.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Welcome to Flutter Explained - WebRTC"),
),
body: Column(
children: [
SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
signaling.openUserMedia(_localRenderer, _remoteRenderer);
},
child: Text("Open camera & microphone"),
),
SizedBox(
width: 8,
),
ElevatedButton(
onPressed: () async {
roomId = await signaling.createRoom(_remoteRenderer);
textEditingController.text = roomId!;
setState(() {});
},
child: Text("Create room"),
),
SizedBox(
width: 8,
),
ElevatedButton(
onPressed: () {
// Add roomId
signaling.joinRoom(
textEditingController.text.trim(),
_remoteRenderer,
);
},
child: Text("Join room"),
),
SizedBox(
width: 8,
),
ElevatedButton(
onPressed: () {
signaling.hangUp(_localRenderer);
},
child: Text("Hangup"),
)
],
),
SizedBox(height: 8),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(child: RTCVideoView(_localRenderer, mirror: true)),
Expanded(child: RTCVideoView(_remoteRenderer)),
],
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("Join the following Room: "),
Flexible(
child: TextFormField(
controller: textEditingController,
),
)
],
),
),
SizedBox(height: 8)
],
),
);
}
}
要实现视频通话,只需添加 google-services.json 配置文件,将 Firebase 集成到项目中。配置完成后,运行项目,就可以进行无缝视频通信了。
总之,将 WebRTC 集成到 Flutter 应用程序中,开发人员就能在不同平台上构建功能丰富的实时通信体验,实现具有高质量音频和视频功能的无缝点对点交互。无论您是要构建视频会议应用程序、实时流媒体平台还是协作工具,Flutter 中的 WebRTC 都为创建引人入胜的交互式应用程序提供了可能性。
本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/webrtc/47622.html