Flutter+WebRTC+gRPC开发点对点加密即时通讯APP--WebRTC视频通话模拟器实战
开篇
基于Flutter+WebRTC+gRPC,开发一款点对点加密、跨端、即时通讯APP,实现文字、音视频通话聊天,同时支持图片、短视频等文件传输功能,计划支持Windows、Android平台。我准备将自己的学习和实践过程记录下来,同时分享给大家,欢迎大家一起研讨交流。这个工程是利用自己的业余时间来实现的,不定时更新。本篇文章是基于WebRTC协议的视频通话APP开发实战,是在Android模拟器环境中来实现的。
WebRTC(Web Real-Time Communications)是一项实时通讯技术,它允许网络应用或者站点,在不借助中间媒介的情况下,建立浏览器之间点对点(Peer-to-Peer)的连接,实现视频流和(或)音频流或者其他任意数据的传输。WebRTC 包含的这些标准使用户在无需安装任何插件或者第三方的软件的情况下,创建点对点(Peer-to-Peer)的数据分享和电话会议成为可能。
创建空白Flutter项目
我们输入flutter create .
在当前目录创建新的Flutter项目。在Vscode下打开该项目目录,同时我们按下ctrl+shift+p
运行Androi模拟器,输入flutter run
并选择我们的Android模拟器,将项目的空白模版程序运行起来。
Android权限申请
前边的文章已经介绍过Android下的相关权限的申请,这里直接进行设置。在flutter项目中找到android\app\src\main\AndroidManifest.xml
文件,添加下边代码即可完成摄像头、麦克风、蓝牙权限申请。
xml
<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" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
minSdkVersion
在进行开发之前还需要对minSdkVersion进行设置,找到android\app\build.gradle
文件,将flutter.minSdkVersion替换成23即可。如果不替换的话,可能会遇到程序无法运行的问题。
xml
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.WebRTCpracticesingle"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion flutter.minSdkVersion
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
WebRTC连接建立过程
下边介绍了WebRTC的连接建立过程以及使用到的API函数:
1、呼叫者通过getUserMedia()捕捉本地媒体。 呼叫者创建一个RTCPeerConnection 并调用 RTCPeerConnection.addTrack() (注:addStream 已经过时)。 呼叫者调用 RTCPeerConnection.createOffer() 来创建一个提议 (offer)。 呼叫者调用 RTCPeerConnection.setLocalDescription() (en-US) 将提议 (Offer)设置为本地描述 (即连接的本地描述)。 setLocalDescription() 之后,呼叫者请求 STUN 服务创建 ice 候选 (ice candidates)。 呼叫者通过信令服务器将提议 (offer) 传递至本次呼叫的预期的接受者。 接受者收到了提议 (offer) 并调用 RTCPeerConnection.setRemoteDescription() 将其记录为远程描述 (也就是连接的另一端的描述)。 接受者做一些可能需要的步骤结束本次呼叫:捕获本地媒体,然后通过RTCPeerConnection.addTrack()添加到连接中。 接受者通过 RTCPeerConnection.createAnswer() 创建一个应答。 接受者调用 RTCPeerConnection.setLocalDescription()将应答 (answer) 设置为本地描述。此时,接受者已经获知连接双方的配置了。 接受者通过信令服务器将应答传递到呼叫者。 呼叫者接受到应答。 呼叫者调用RTCPeerConnection.setRemoteDescription() 将应答设定为远程描述。如此,呼叫者已经获知连接双方的配置了。
注意!上述API函数并不是Flutter WebRTC插件下的接口函数,在Flutter下对应的函数的名称可能有差异。
参数定义与初始化
首先要引入flutter_WebRTC库,这是适用于Flutter Mobile/Desktop/Web的 WebRTC插件,直接import即可。接着对RTCVideoRenderer、RTCPeerConnection、MediaStream三个参数进行初始化,因为需要一对一进行连接,所以参数也需要成对出现,我们同local和remote作为参数名前缀来进行区分。定义完成后进行本地和远端的RTCVideoRenderer初始化,调用initialize()函数使之生效。
MediaStream:通过MediaStream的API能够通过设备的摄像头及话筒获得视频、音频的同步流。
RTCPeerConnection:RTCPeerConnection是WebRTC用于构建点对点之间稳定、高效的流传输的组件。
dart
import 'package:flutter_webrtc/flutter_webrtc.dart';
final _localVideoRenderer = RTCVideoRenderer();
late RTCPeerConnection localConnection;
late MediaStream localstream;
final _remoteVideoRenderer = RTCVideoRenderer();
late RTCPeerConnection remoteConnection;
late MediaStream remotestream;
void initRenderers() async {
await _localVideoRenderer.initialize();
await _remoteVideoRenderer.initialize();
}
mediaConstraints音视频参数设置
我们定义localmediaConstraints来进行音视频参数设置,我们可以指定视频的分辨率,指定使用前置还是后置摄像头,设置视频帧率,需要的的话可以通过此参数设置来达到美颜效果。
dart
final Map<String, dynamic> localmediaConstraints = {
'audio': true,
'video': {
"width": {"ideal": 8000},
"heigth": {"ideal": 6000},
'facingMode':
'environment', //'facingMode': 'user','facingMode': 'environment',
"frameRate": {
"ideal": 60,
},
}
};
final Map<String, dynamic> remotemediaConstraints = {
'audio': true,
'video': {
"width": {"ideal": 8000},
"heigth": {"ideal": 6000},
'facingMode': 'environment', //'facingMode': 'user',
"frameRate": {
"ideal": 60,
},
}
};
上述代码的意思是开启音频轨道,视频理想分辨率为8000*6000,视频帧率理想值为60帧,user代表前置摄像头,environment代表后置摄像头。
SDP(Session Description Protocol)会话描述
SDP(Session Description Protocol)会话描述,WebRTC 连接上的端点配置称为会话描述。该描述包括关于要发送的媒体类型,正在使用的传输协议,端点的 IP 地址和端口以及描述媒体传输端点所需的其他信息的信息。使用会话描述协议(SDP) 来交换和存储该信息。当用户对另一个用户启动 WebRTC 调用时,将创建一个称为提议(offer) 的特定描述。该描述包括有关呼叫者建议的呼叫配置的所有信息。接收者然后用应答(answer) 进行响应,这是他们对呼叫结束的描述。以这种方式,两个设备彼此共享以便交换媒体数据所需的信息。每个对等端保持两个描述:描述本身的本地描述和描述呼叫的远端的远程描述。SDP 是通过webrtc框架里面的PeerConnection所创建。
dart
final Map<String, dynamic> sdpConstraints = {
"mandatory": {
"OfferToReceiveAudio": true,
"OfferToReceiveVideo": true,
},
"optional": [],
};
final Map<String, dynamic> pcConstraints = {
"mandatory": {},
"optional": [],
};
ICE Candidate
Candidate是ICE中用来描述可以用来和本地通信的地址相关的信息,主要包含了相关方的IP信息,包括自身局域网的ip、公网ip、turn服务器ip、stun服务器ip等,Candidate是通过webrtc框架里面的PeerConnection所创建的。
在启动WebRTC对等连接时,通常连接的每一端都会提出一些候选者,直到他们共同同意一个描述他们认为最好的连接。然后,WebRTC 使用该候选者的详细信息来启动连接。这里我们指定stun服务器为:stun.l.google.com
。
dart
Map<String, dynamic> configuration = {
"iceServers": [
{"url": "stun:stun.l.google.com:19302"},
]
};
本地媒体获取
使用getUserMedia函数获得本地媒体流localstream,使用createPeerConnection函数创建本地连接,在本地连接中添加轨道addTrack。
dart
_getlocalUserMedia() async {
localstream =
await navigator.mediaDevices.getUserMedia(localmediaConstraints);
_localVideoRenderer.srcObject = localstream;
localConnection = await createPeerConnection(configuration, pcConstraints);
localConnection.onIceCandidate = (candidate) {
remoteConnection.addCandidate(candidate);
};
localConnection.onConnectionState = (state) {
print(state);
};
for (MediaStreamTrack track in localstream.getTracks()) {
localConnection.addTrack(track, localstream);
}
localstream.getAudioTracks()[0].setTorch(true);
}
同理,远端也是同样的操作和配置,因为我们在一个项目中完成两端的连接建立,所以我们同时实现远端和本地的配置。
dart
remoteConnection = await createPeerConnection(configuration, pcConstraints);
remoteConnection.onIceCandidate = (candidate) {
localConnection.addCandidate(candidate);
print(candidate.candidate);
};
remoteConnection.onConnectionState = (state) {
print(state);
};
remoteConnection.onAddTrack = (stream, track) {
remotestream = stream;
_remoteVideoRenderer.srcObject = stream;
};
offer answer SDP生成
SDP前边以及介绍过,接下来是生成双方SDP的操作,SDP生成完毕将进行交换。连接发起方的SDP为offer,连接接收方的SDP为answer。
dart
///////////offer;
RTCSessionDescription offer =
await localConnection.createOffer(sdpConstraints);
localConnection.setLocalDescription(offer);
remoteConnection.setRemoteDescription(offer);
print(offer.sdp);
///////////answer;
RTCSessionDescription answer =
await remoteConnection.createAnswer(sdpConstraints);
remoteConnection.setLocalDescription(answer);
localConnection.setRemoteDescription(answer);
print(answer.sdp);
视频展示UI
最后,我们需要绘制视频展示UI,进行视频画面的展示。我们使用StatefulWidget有状态组件,因为我们需要进行视频采集,需要在组件初始化时进行,所以需要用有状态组件。在initState() 函数中调用我们上边定义的函数_getlocalUserMedia()。
展示视频只需要用插件给定好的的RTCVideoView组件就可以了,因为是双向视频对话聊天,所以我们使用 Row组件包裹两个RTCVideoView组件进行视频横向展示,在Row组件中使用Flexible组件使视频框能自适应大小进行展示。
dart
class GetMedia extends StatefulWidget {
const GetMedia({super.key});
@override
State<GetMedia> createState() => _GetMediaState();
}
class _GetMediaState extends State<GetMedia> {
@override
void initState() {
initRenderers();
_getlocalUserMedia();
super.initState();
}
SizedBox videoRenderers() => SizedBox(
height: 110,
child: Row(children: [
Flexible(
child: Container(
key: const Key('local'),
margin: const EdgeInsets.fromLTRB(5.0, 5.0, 5.0, 5.0),
decoration: const BoxDecoration(color: Colors.black),
child: RTCVideoView(_localVideoRenderer),
),
),
Flexible(
child: Container(
key: const Key('remote'),
margin: const EdgeInsets.fromLTRB(5.0, 5.0, 5.0, 5.0),
decoration: const BoxDecoration(color: Colors.black),
child: RTCVideoView(_remoteVideoRenderer),
),
),
]),
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Video"),
),
body: videoRenderers(),
);
}
}
总结
我们这里只是简单尝试一下Flutter下的WebRTC实践,在虚拟Android环境下,目前勉强能用了,但是对技术的细节还是不太了解,后续会继续进行学习的!