前言
本章主要介绍如何 Xamarin.Android 项目中集成 WebRTC。Xamarin.Android 是 Microsoft 一个开源应用开发平台,可用于在 Visual Studio 中通过 C# 创建完全原生的 Android 应用。
WebRTC 是 Google 开源的一个实时音视频通信库,能在 Windows、Mac、Android、iOS 平台上都能实现音视频的互联互通,拥有优秀的网络拥塞算法以及回声消除算法,精准的带宽评估,即便在弱网的情况下依然能保证正常的视频通话。WebRTC 已经成为音视频通信技术的标准,现在各大音视频通信公司或多或少都参考或借鉴了 WebRTC,甚至有些公司都是在 WebRTC 的基础上来研发自己的产品。
下载依赖库
从 maven 中央仓库中搜索 google-webrtc 下载最新版本即可,以下提供两个可参考的maven 中央仓库地址:
Sonatype maven 中央仓库:https://central.sonatype.com
阿里云maven 中央仓库:https://developer.aliyun.com/mvn/search
下载源码自行编译
如果需要在 WebRTC 的基础上进行功能定制,这时候我们就需要下载 WebRTC 的源代码来进行修改了,可以从 WebRTC 官方网站上下载源码(https://webrtc.googlesource.com/src)官方拉取代码非常慢,编译的时候还会出现各种依赖问题。建议从从国内声网 Agora WebRTC 团队提供的 WebRTC 镜像源拉取(WebRTC 国内镜像 )。如何编译 WebRTC 这里就不阐述了,可以查看声网提供的文档。
新建 Android 绑定库
- 在 Visual Studio 中选中解决方案,右键新建项目,选择 “Android 绑定库(Xamarin)”
- 将 google-webrtc-1.0.39950.aar 库复制到 Android 绑定库项目的 Jars 目录下。
- 选中 google-webrtc-1.0.39950.aar 右键属性,将生成操作设置为 LibraryProjectZip。
引用 Android 绑定库到项目中
选中你的 Xamarin.Android 项目,右键添加引用,就可以从应用管理器中选择 “Android 绑定库” 了。
在 Xamarin.Android 中使用 WebRTC
在使用之前首先介绍一下 WebRTC 中的 PeerConnection,PeerConnection 对象是 WebRTC 的核心,它是 WebRTC 暴露给用户的统一接口,其内部有多个模块组成,有网络处理模块、服务质量模块、音视频引擎模块等等。
- 申请权限,使用 WebRTC 需要用三个系统权限:
CAMERA: 相机访问权限,用于采集视频数据。
RECORD_AUDIO: 使用麦克风录音权限,用于采集音频数据。
INTERNET: 网络权限,用于通过网卡传输媒体数据。
- 动态申请权限,从 Android 6.0起还需要动态申请权限。
ActivityCompat.RequestPermissions
(
this,
new string[] { Manifest.Permission.Camera, Manifest.Permission.RecordAudio },
RequestCode
);
- 创建 EglBase 对象,我们在初始化视频编码格式工厂(DefaultVideoDecoderFactory)需要用到 EglBaseContext 实例。如果直接使用 EglBase.Create() 静态方法来创建会出现一个错误:java.lang.NoSuchMethodError: No static method create()Lorg/webrtc/EglBase,原因是 WebRTC SDK 中使用了 Java 语法糖引起的(如果使用高版本 JDK 就不会出现此问题了),处理时会把原本在 EglBase 里的静态方法挪到 EglBase$-CC 这个 class 文件,导致在 EglBase 里找不到原本属于他的方法,所以这里我们需要通过反射来调用。
private IEglBase CreateIEglBase()
{
var @class = Java.Lang.Class.ForName("org.webrtc.EglBase$-CC");
var method = @class.GetDeclaredMethod("create");
var ieglbase = method.Invoke(null).JavaCast<IEglBase>();
return ieglbase;
}
- 创建一个 PeerConnection 对象 。
var eglBase = CreateIEglBase();
var eglBaseContext = _eglBase.EglBaseContext;
var initializeOptions = PeerConnectionFactory.InitializationOptions
.InvokeBuilder(_context)
.CreateInitializationOptions();
// 初始化 PeerConnectionFactory
PeerConnectionFactory.Initialize(initializeOptions);
var options = new PeerConnectionFactory.Options();
// 视频编码格式工厂。
var encoderFactory = new DefaultVideoEncoderFactory(eglBaseContext, true, true);
// 视频解码。
var decoderFactory = new DefaultVideoDecoderFactory(eglBaseContext);
// 构造音频设备模块。
var audioDeviceModule = JavaAudioDeviceModule.InvokeBuilder(context).CreateAudioDeviceModule();
// 构建一个 RTC 连接工厂。
var peerConnectionFactory = PeerConnectionFactory.InvokeBuilder()
.SetOptions(options)
.SetVideoEncoderFactory(encoderFactory)
.SetVideoDecoderFactory(decoderFactory)
.SetAudioDeviceModule(audioDeviceModule)
.CreatePeerConnectionFactory();
// 初始化一个 coturn 服务。
var iceServer = PeerConnection.IceServer.InvokeBuilder("turn:ip:port")
iceServer.SetUsername("coturn 服务账户");
iceServer.SetPassword("coturn 服务密码");
var rtcConfiguration = new PeerConnection.RTCConfiguration
(
new List<PeerConnection.IceServer>{ iceServer }
);
// 为通信双方提供了网络通道,所有媒体数据的传输都是由它来完成的。
var peerConnection = peerConnectionFactory.CreatePeerConnection
(
rtcConfiguration,
new Observer(_targetId, _rtcClient, _remoteSurfaceView)
);
- 创建本地音频轨(AudioTrack)、视频轨(VideoTrack)对象。
/// <summary>
/// 创建本地音频轨。
/// </summary>
/// <param name="peerConnectionFactory"> </param>
/// <returns> 音频轨。</returns>
private AudioTrack CreateAudioTrack(PeerConnectionFactory peerConnectionFactory)
{
var audioConstraints = new MediaConstraints();
// 回声消除。
audioConstraints.Mandatory.Add(new MediaConstraints.KeyValuePair("googEchoCancellation", "true"));
// 自动增益。
audioConstraints.Mandatory.Add(new MediaConstraints.KeyValuePair("googAutoGainControl", "true"));
// 噪音处理。
audioConstraints.Mandatory.Add(new MediaConstraints.KeyValuePair("googNoiseSuppression", "true"));
// 创建音频源。
AudioSource audioSource = peerConnectionFactory.CreateAudioSource(audioConstraints);
// 创建本地音频轨。
return peerConnectionFactory.CreateAudioTrack(AudioTrackId, audioSource);
}
/// <summary>
/// 创建本地视频源。
/// </summary>
/// <param name="peerConnectionFactory"> </param>
/// <returns> 视频源。</returns>
private VideoSource CreateVideoSource(PeerConnectionFactory peerConnectionFactory)
{
// isScreencast 为 true 表示数据来源是截屏的,适用于屏幕共享。
return peerConnectionFactory.CreateVideoSource(false);
}
/// <summary>
/// 创建本地视频轨。
/// </summary>
/// <param name="videoSource"> 视频源。</param>
/// <returns> 视频轨。</returns>
private VideoTrack CreateVideoTrack(PeerConnectionFactory peerConnectionFactory, VideoSource videoSource)
{
// 创建本地视频轨。
return peerConnectionFactory.CreateVideoTrack(VideoTrackId, videoSource);
}
- 将本地 AudioTrack、VideoTrack 添加到 PeerConnection 中
void Init(SurfaceViewRenderer localSurfaceView, SurfaceViewRenderer remoteSurfaceView)
{
// 创建音频轨道。
_localAudioTrack = CreateAudioTrack(_peerConnectionFactory);
_localAudioTrack.SetEnabled(true);
// 创建视频源。
var videoSource = CreateVideoSource(_peerConnectionFactory);
// 将视频源和视频轨关联在一起。
_localVideoTrack = CreateVideoTrack(_peerConnectionFactory, videoSource);
// 设置启用状态。
_localVideoTrack.SetEnabled(true);
// 创建视频采集。
_videoCapturer = CreateVideoCapturer();
// 创建 Surface 工具类。
var surfaceTextureHelper = SurfaceTextureHelper.Create("CaptureThread", _eglBaseContext);
// 将 VideoCapturer 与 VideoSource 关联到一起,VideoTrack 就可以从设备上获取到源源不断的音视频数据了。
_videoCapturer.Initialize(surfaceTextureHelper, _context.ApplicationContext, videoSource.CapturerObserver);
// 配置 SurfaceView。
ConfigureSurfaceView(localSurfaceView);
ConfigureSurfaceView(remoteSurfaceView);
// 视频与 View 绑定。
_localVideoTrack.AddSink(localSurfaceView);
// 把本地的音频轨、视频轨添加到连接对象中,用于将数据发送给远程端。
_peerConnection.AddTrack(_localAudioTrack);
_peerConnection.AddTrack(_localVideoTrack);
// 打开扬声器。
SetSpeakerphoneOn(true);
}
/// <summary>
/// 创建视频采集。
/// </summary>
/// <returns> 视频采集接口。</returns>
/// <exception cref="Exception"> 设备初始化异常。</exception>
private ICameraVideoCapturer CreateVideoCapturer()
{
// 检查是否支持 Camera2。
if (Camera2Enumerator.IsSupported(_context))
{
return CreateVideoCapturer(new Camera2Enumerator(_context));
}
return CreateVideoCapturer(new Camera1Enumerator(true));
}
private ICameraVideoCapturer CreateVideoCapturer(ICameraEnumerator enumerator)
{
var deviceNames = enumerator.GetDeviceNames();
string backFacingDeviceName = null;
//
foreach (var item in deviceNames)
{
// 找到前置摄像头。
if (enumerator.IsFrontFacing(item))
{
if (_cameraFrontFacingName == null)
{
_cameraFrontFacingName = item;
}
}
// 后摄像头。
else if (enumerator.IsBackFacing(item))
{
if (_cameraBackFacingName == null)
{
_cameraBackFacingName = item;
}
}
else
{
// 未找到前置摄像头,尝试寻找其他摄像头。
backFacingDeviceName = item;
}
// 如果前摄像头和后摄像头都找到了就停止。
if (_cameraFrontFacingName != null && _cameraBackFacingName != null)
{
break;
}
}
if (_cameraFrontFacingName != null)
{
// 默认使用前置摄像头。
_isCameraFrontFacing = true;
return enumerator.CreateCapturer(_cameraFrontFacingName, null);
}
if (_cameraBackFacingName != null)
{
return enumerator.CreateCapturer(_cameraBackFacingName, null);
}
if (backFacingDeviceName == null)
{
throw new Exception("Camera device not found.");
}
else
{
// 如果没有前置摄像头就创建后置摄像头。
return enumerator.CreateCapturer(backFacingDeviceName, null);
}
}
private void ConfigureSurfaceView(SurfaceViewRenderer surfaceView)
{
// 初始化视频渲染。
surfaceView.Init(_eglBase.EglBaseContext, null);
// 设置填充模式(ScaleAspectFill 表示将视频按比例填充到 View 中)。
surfaceView.SetScalingType(RendererCommon.ScalingType.ScaleAspectFill);
// 让视频图像按纵轴反转显示,之所以这样做,是因为采集的视频图像与我们眼睛看到的内容正好相反。
// 前摄像头为 true,后摄像头为 false,打开默认未摄像头所里这设置为 true。
surfaceView.SetMirror(true);
// 设置是否打开硬件视频拉伸功能,这里不开启。由于远程端的 View 与本地视频的 View 设置的参数是一样的。
surfaceView.SetEnableHardwareScaler(false);
}
- 在进行 WebRTC 通信之前,必须在两个设备之间建立一个信令通道,信令服务器程序可以使用 DotNetty 来实现,主要时用于传递双方的 Offer 和 Answer。
- 在通信建立之后,使用 PeerConnection 对象创建一个 Offer 和 Answer,通过信令通道传递双方的媒体信息。
internal void CreateOffer()
{
_messageType = MessageType.Offer;
_peerConnection.CreateOffer(this, _mediaConstraints);
}
internal void CreateAnswer(string sdp)
{
LogUtility.Debug(LogTag, $"收到远程端发送的 Offer,生成 Offer 应答, sdp: { sdp }");
_messageType = MessageType.Answer;
// 保存远程端的 sdp。
_peerConnection.SetRemoteDescription(new SetSdpObserver(false), new SessionDescription(SessionDescription.Type.Offer, sdp));
// 创建本地 sdp
_peerConnection.CreateAnswer(this, _mediaConstraints);
}
internal void SetRemoteDescription(string sdp)
{
LogUtility.Debug(LogTag, $"收到 Offer 应答,设置远程 sdp: { sdp }");
// 设置远程 sdp。
_peerConnection.SetRemoteDescription(new SetSdpObserver(false), new SessionDescription(SessionDescription.Type.Answer, sdp));
}
internal void AddIceCandidate(string iceText)
{
LogUtility.Debug(LogTag, $"到远程端发送的 Candidate ,iceText: { iceText }");
try
{
var icc = JsonConvert.DeserializeObject<IceCandidateContent>(iceText);
var iceCandidate = new IceCandidate(sdpMid: icc.SM, sdpMLineIndex: icc.SMI, sdp: icc.S);
// 将远程端 Candidate 添加到 PeerConnection
_peerConnection.AddIceCandidate(iceCandidate);
}
catch (Exception ex)
{
LogUtility.Error(LogTag, ex.ToString());
}
}
class Observer : Java.Lang.Object, PeerConnection.IObserver
{
private const string LogTag = "Observer";
private readonly int _targetId;
private readonly RtcClient _rtcClient;
private readonly SurfaceViewRenderer _remoteSurfaceView;
internal Observer(int targetId, RtcClient rtcClient, SurfaceViewRenderer remoteSurfaceView)
{
_targetId = targetId;
_rtcClient = rtcClient;
_remoteSurfaceView = remoteSurfaceView;
}
// 和 OnAddTrack 一样,不过已经过时了,因此使用 OnAddTrack。
void PeerConnection.IObserver.OnAddStream(MediaStream mediaStream)
{
LogUtility.Debug(LogTag, "OnAddStream");
}
// 当收到远程端音视频流会触发该函数。
void PeerConnection.IObserver.OnAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams)
{
LogUtility.Debug(LogTag, "OnAddTrack");
if (mediaStreams == null || mediaStreams.Length == 0)
{
return;
}
// 获取远程端发送过来的媒体数据流。
var mediaStream = mediaStreams[0];
// 获取视频轨。
var videoTracks = mediaStream.VideoTracks;
if (videoTracks.Count > 0 && videoTracks[0] is VideoTrack videoTrack && _remoteSurfaceView != null)
{
LogUtility.Debug(LogTag, "添加远程端视频轨");
if (videoTrack.Enabled())
{
// 将视频渲染到视图上。
videoTrack.AddSink(_remoteSurfaceView);
}
}
LogUtility.Debug(LogTag, "与对方已建立连接");
}
void PeerConnection.IObserver.OnDataChannel(DataChannel dataChannel)
{
LogUtility.Debug(LogTag, "OnDataChannel");
}
// 收集 Candidate 之后会回调该函数。
void PeerConnection.IObserver.OnIceCandidate(IceCandidate ice)
{
LogUtility.Debug(LogTag, $"收集 Candidate, serverUr: { ice.ServerUrl }, sdp: { ice.Sdp }, sdpMid: { ice.SdpMid }, sdpMLineIndex: { ice.SdpMLineIndex }");
var iceCandidateContent = new IceCandidateContent
{
S = ice.Sdp,
SM = ice.SdpMid,
SMI = ice.SdpMLineIndex
};
LogUtility.Debug(LogTag, $"向远程端发送 Candidate 消息");
// 通过信令通道向远程端发送 Candidate 消息。
try
{
_rtcClient.SendMessageAndFlushAsync(_targetId, MessageType.Candidate, JsonConvert.SerializeObject(iceCandidateContent)).ConfigureAwait(false);
}
catch (Exception ex)
{
LogUtility.Error(LogTag, ex.ToString());
}
}
void PeerConnection.IObserver.OnIceCandidatesRemoved(IceCandidate[] p0)
{
LogUtility.Debug(LogTag, "OnIceCandidatesRemoved");
}
void PeerConnection.IObserver.OnIceConnectionChange(PeerConnection.IceConnectionState p0)
{
LogUtility.Debug(LogTag, "OnIceConnectionChange");
}
void PeerConnection.IObserver.OnIceConnectionReceivingChange(bool p0)
{
LogUtility.Debug(LogTag, "OnIceConnectionReceivingChange");
}
void PeerConnection.IObserver.OnIceGatheringChange(PeerConnection.IceGatheringState p0)
{
LogUtility.Debug(LogTag, "OnIceGatheringChange");
}
void PeerConnection.IObserver.OnRemoveStream(MediaStream p0)
{
LogUtility.Debug(LogTag, "OnRemoveStream");
}
void PeerConnection.IObserver.OnRenegotiationNeeded()
{
LogUtility.Debug(LogTag, "OnRenegotiationNeeded");
}
void PeerConnection.IObserver.OnSignalingChange(PeerConnection.SignalingState p0)
{
LogUtility.Debug(LogTag, "OnSignalingChange");
}
}
class SetSdpObserver : Java.Lang.Object, ISdpObserver
{
private const string LogTag = "SetSdpObserver";
private bool _isLocal;
internal SetSdpObserver(bool isLocal)
{
_isLocal = isLocal;
}
void ISdpObserver.OnCreateFailure(string p0)
{
LogUtility.Debug(LogTag, "OnCreateFailure");
}
void ISdpObserver.OnCreateSuccess(SessionDescription sessionDescription)
{
LogUtility.Debug(LogTag, "OnCreateSuccess");
}
void ISdpObserver.OnSetFailure(string error)
{
LogUtility.Debug(LogTag, $"设置 { (_isLocal ? "本地" : "远程") } Description 失败, error: { error }");
}
void ISdpObserver.OnSetSuccess()
{
LogUtility.Debug(LogTag, $"设置 { (_isLocal ? "本地" : "远程") } Description 成功");
}
}
总结
以上就是一个简单的 WebRTC 在 Xamarin.Android 项目中的集成步骤与代码示例,如有疑问请通过微信公众号私信我。后续会写一篇 “在 Xamarin.Android 中实现一对一音视频通话” 的文章,想了解更多 .NetCore 、Xamarin、即时通讯、实时音视频通信领域技术文章分享,可以关注微信公众号或者 GitHub。
微信公众号:KeisoftCN
macroecho · GitHub