如何在 Xamarin.Android 中集成 WebRTC

前言

        本章主要介绍如何 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

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值