Flutter+WebRTC+gRPC开发点对点加密即时通讯APP--WebRTC视频通话模拟器实战

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环境下,目前勉强能用了,但是对技术的细节还是不太了解,后续会继续进行学习的!

相关推荐
风华圆舞5 小时前
在 Flutter 鸿蒙项目里接入语音识别的完整思路
flutter·语音识别·harmonyos
风华圆舞7 小时前
鸿蒙 + Flutter 下如何让 HarmonyOS 能力真正服务于 AI 体验
人工智能·flutter·harmonyos
换个昵称都难8 小时前
webrtc源码解析概要介绍
webrtc
BreezeDove8 小时前
【Android】Flutter3.35项目启动超时问题
android·flutter
换个昵称都难8 小时前
WebRTC 完整调用流程(前端纯 JS 实现,最简可运行)
webrtc
风华圆舞8 小时前
鸿蒙 MICROPHONE 权限在 Flutter 项目里怎么处理
flutter·华为·harmonyos
愚者Pro1 天前
切换本地 Flutter SDK 版本
flutter
TT_Close1 天前
别再复制旧 Flutter 工程了,真正拖慢你的不是业务代码
flutter·npm·visual studio code
风华圆舞1 天前
鸿蒙 + Flutter 下 AI 助手为什么要支持流式输出
人工智能·flutter·harmonyos
换个昵称都难1 天前
webrtc 拥塞控制GCC 和PCC
webrtc