Unity Metaverse(八)、RTC Engine 基于Agora声网SDK实现音视频通话


简介

本文介绍如何在Unity中接入声网SDK,它可以应用的场景有许多,例如直播、电商、游戏、社交等,音视频通话是其实时互动的基础能力。

声网

如下图所示,可以在官网中选择Unity SDK进行下载,也可以到Unity Asset Store资源商店中搜索Agora SDK进行下载导入。

官网 Unity SDK下载

Unity Asset Store - Agora SDK

创建应用

在官网中前往Console控制台创建应用,以便获取AppID等信息,鉴权机制可以先选择无证书模式,测试阶段先略过Token鉴权。

Console

也可以在安全模式下使用临时Token生成器:

临时Token

构建应用场景

以视频通话为例,将用户的视频流显示在其Avatar人物实例的HUD头显上方,视频流的显示可以使用模型面片也可以使用Raw Image作为载体,为其添加Render Texture来显示视频,以Raw Image为例,首先制作一个Prefab预制件,当用户推送本地的视频流时,加载该预制件资产并Instantiate实例化、设置在用户Avatar人物实例头显上方。当用户停止视频流发布时,将上述预制件的实例进行销毁。

Prefab 预制件


/// <summary>
/// 视频流发布成功事件
/// </summary>
/// <param name="uid">RTC UserID 传0表示本地用户</param>
/// <param name="channelId">频道ID</param>
public void OnVideoPublishSuccessed(uint uid, string channelId)
{
    
    
    //加载显示在Avatar头部上方的视频显示物体
    Main.Resource.LoadAssetAsync<GameObject>(AssetPathUtility.GetPrefabAssetPath("AvatarHUDCameraDisplay"),
        onCompleted: (isSuccessed, asset) =>
        {
    
    
            //加载成功
            if (isSuccessed)
            {
    
    
                //实例化
                var instance = Instantiate(asset);
                //父级设为Avatar人物实例HUD下的Canvas画布
                instance.transform.SetParent(HUD.Canvas.transform, false);
                //重置旋转、缩放
                instance.transform.localRotation = Quaternion.identity;
                instance.transform.localScale = Vector3.one;
                //获取RawImage组件
                var rawImage = instance.GetComponentInChildren<RawImage>();
                //添加VideoSurface组件 用其显示视频
                var videoSurface = rawImage.gameObject.AddComponent<VideoSurface>();
                //Uid传入0代表本地视频
                videoSurface.SetForUser(uid, channelId, IsOwn ? VIDEO_SOURCE_TYPE.VIDEO_SOURCE_CAMERA_PRIMARY : VIDEO_SOURCE_TYPE.VIDEO_SOURCE_REMOTE);
                //缓存起来 停止发布时将其销毁
                videoDisplayer = instance;
            }
        });
}

/// <summary>
/// 视频流停止发布事件
/// </summary>
public void OnVideoPublishStopped()
{
    
    
    if (videoDisplayer != null)
    {
    
    
        Destroy(videoDisplayer);
        videoDisplayer = null;
    }
}

API调用与回调事件

首先需要调用Initialize接口来初始化RTC Engine实例:

/// <summary>
/// 初始化
/// </summary>
/// <returns>0:初始化成功  小于0:初始化失败
///     -1:     一般性的错误(未明确归类)。
///     -2:     设置了无效的参数。
///     -7:     SDK 初始化失败。
///     -22:    资源申请失败。当 app 占用资源过多,或系统资源耗尽时,SDK 分配资源失败,会返回该错误。
///     -101:   App ID 无效。
/// </returns>
public int Initialize()
{
    
    
    Main.Log.Info("【RTC Engine】开始初始化...");
    //创建IRtcEngine对象
    engine = Agora.Rtc.RtcEngine.CreateAgoraRtcEngine();
    //IRtcEngine实例的配置
    RtcEngineContext context = new RtcEngineContext(appId, 0,
        CHANNEL_PROFILE_TYPE.CHANNEL_PROFILE_LIVE_BROADCASTING,
        AUDIO_SCENARIO_TYPE.AUDIO_SCENARIO_DEFAULT,
        AREA_CODE.AREA_CODE_GLOB);
    //初始化IRtcEngine
    int code = engine.Initialize(context);
    if (code == 0)
    {
    
    
        Main.Log.Info("【RTC Engine】初始化成功");
        //添加回调事件处理器
        if (engine.InitEventHandler(new RtcEngineEventHandler()) == 0)
            Main.Log.Info("【RTC Engine】添加回调事件处理器成功");
        else
            Main.Log.Info("【RTC Engine】添加回调事件处理器失败");
    }
    else
        Main.Log.Info("【RTC Engine】初始化失败:{0}", code);

    //默认开启音频和视频模块
    engine.EnableAudio();
    engine.EnableVideo();

    engine.SetVideoEncoderConfiguration(new VideoEncoderConfiguration()
    {
    
    
        dimensions = new VideoDimensions(160, 100),
        frameRate = 15,
        bitrate = 0,
    });
    engine.SetChannelProfile(CHANNEL_PROFILE_TYPE.CHANNEL_PROFILE_COMMUNICATION);
    engine.SetClientRole(CLIENT_ROLE_TYPE.CLIENT_ROLE_BROADCASTER);
    return code;
}

其中InitEventHandler是为了添加RTC的回调事件处理器,该处理器继承抽象类IRtcEngineEventHandler并重写各虚函数以实现应用自身的业务逻辑,在回调方法中不应做耗时或者调用可能会引起阻塞的API,否则可能影响SDK的运行。本文使用了事件系统将业务需要订阅的事件进行了分发:

/* =======================================================
 *  Unity版本:2020.3.16f1c1
 *  作 者:CoderZ
 *  邮 箱:[email protected]
 *  创建时间:2023-05-07 14:14:38
 *  当前版本:1.0.0
 *  主要功能:RTC事件处理器
 *  详细描述:基于Agora声网SDK v4.1.0
 *  修改记录:
 * =======================================================*/

using Agora.Rtc;
using SK.Framework;

namespace Metaverse
{
    
    
    public class RtcEngineEventHandler : IRtcEngineEventHandler
    {
    
    
        #region >> 核心回调
        /// <summary>
        /// 直播场景下用户角色已切换回调
        /// 
        /// CLIENT_ROLE_TYPE 直播场景里的用户角色:
        ///     CLIENT_ROLE_BROADCASTER     1:主播。主播可以发流也可以收流。
        ///     CLIENT_ROLE_AUDIENCE        2:(默认)观众。观众只能收流不能发流。   
        /// </summary>
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        /// <param name="oldRole">切换前的角色</param>
        /// <param name="newRole">切换后的角色</param>
        /// <param name="newRoleOptions">切换后的角色属性   
        ///     AUDIENCE_LATENCY_LEVEL_TYPE:直播频道中观众的延时级别。该枚举仅在用户角色设为CLIENT_ROLE_AUDIENCE时才生效。
        ///         AUDIENCE_LATENCY_LEVEL_LOW_LATENCY          1: 低延时。
        ///         AUDIENCE_LATENCY_LEVEL_ULTRA_LOW_LATENCY    2:(默认)超低延时。
        /// </param>
        public override void OnClientRoleChanged(RtcConnection connection, CLIENT_ROLE_TYPE oldRole, CLIENT_ROLE_TYPE newRole, ClientRoleOptions newRoleOptions)
        {
    
    
            Main.Log.Info("【Agora RTC】切换用户角色:{0} >> {1}", oldRole, newRole);
        }

        /// <summary>
        /// 直播场景下切换用户角色失败回调
        /// </summary>
        /// 
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        /// <param name="reason">切换用户角色失败的原因
        ///     CLIENT_ROLE_CHANGE_FAILED_TOO_MANY_BROADCASTERS     1:频道内主播人数达到上限。  注:该枚举仅在开启128人功能后报告。主播人数的上限根据开启128人功能时实际配置的人数而定。
        ///     CLIENT_ROLE_CHANGE_FAILED_NOT_AUTHORIZED            2:请求被服务端拒绝。建议提示用户重新尝试切换用户角色。
        ///     CLIENT_ROLE_CHANGE_FAILED_REQUEST_TIME_OUT          3:请求超时。建议提示用户检查网络连接状况后重新尝试切换用户角色。
        ///     CLIENT_ROLE_CHANGE_FAILED_CONNECTION_FAILED         4:网络连接断开。可根据OnConnectionStateChanged报告的reason,排查网络连接失败的具体原因。
        /// </param>
        /// <param name="currentRole">当前用户角色
        ///     CLIENT_ROLE_BROADCASTER     1:主播。主播可以发流也可以收流。
        ///     CLIENT_ROLE_AUDIENCE        2:(默认)观众。观众只能收流不能发流。</param>
        public override void OnClientRoleChangeFailed(RtcConnection connection, CLIENT_ROLE_CHANGE_FAILED_REASON reason, CLIENT_ROLE_TYPE currentRole)
        {
    
    
            Main.Log.Info("【Agora RTC】切换用户角色失败,当前角色:{0}  失败原因:{1}", currentRole, reason);
        }

        /// <summary>
        /// 发生错误回调
        /// 
        /// 该回调方法表示SDK运行时出现了(网络或媒体相关的)错误。
        /// 通常情况下,SDK上报的错误意味着SDK无法自动恢复,需要APP干预或提示用户。
        /// 比如启动通话失败时,SDK会上报ERR_START_CALL(1002) 错误。App可以提示用户启动通话失败,并调用LeaveChannel退出频道。
        /// </summary>
        /// <param name="err">错误代码</param>
        /// <param name="msg">错误描述</param>
        public override void OnError(int err, string msg)
        {
    
    
            Main.Log.Info("【Agora RTC】发生错误,错误代码:{0}  错误描述:{1}", err, msg);
            //抛出事件
            Main.Events.Publish(RtcEngineErrorEventArgs.Allocate(err, msg));
        }

        /// <summary>
        /// 加入频道成功回调
        /// </summary>
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        /// <param name="elapsed">从本地用户调用JoinChannel[2/2]到SDK触发此回调所经过的时间 单位毫秒</param>
        public override void OnJoinChannelSuccess(RtcConnection connection, int elapsed)
        {
    
    
            Main.Log.Info("【Agora RTC】加入频道,频道ID:{0}  Elapsed:{1}", connection.channelId, elapsed);
            //抛出事件
            Main.Events.Publish(RtcEngineJoinChannelEventArgs.Allocate(true, connection, elapsed));
        }

        /// <summary>
        /// 离开频道回调
        /// </summary>
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        /// <param name="stats">通话的统计数据
        ///     duration:               本地用户通话时长(秒),累计值。
        ///     txBytes:                发送字节数(bytes)。
        ///     rxBytes:                接收字节数(bytes)。
        ///     txAudioBytes:           发送音频字节数(bytes),累计值。
        ///     txVideoBytes:           发送视频字节数(bytes),累计值。
        ///     rxAudioBytes:           接收音频字节数(bytes),累计值。
        ///     rxVideoBytes:           接收视频字节数(bytes),累计值。
        ///     txKBitRate:             发送码率(Kbps)。
        ///     rxKBitRate:             接收码率(Kbps)。
        ///     rxAudioKBitRate:        音频接收码率 (Kbps)。
        ///     txAudioKBitRate:        音频包的发送码率 (Kbps)。
        ///     rxVideoKBitRate:        视频接收码率 (Kbps)。
        ///     txVideoKBitRate:        视频发送码率 (Kbps)。
        ///     lastmileDelay:          客户端-接入服务器延时 (毫秒)。
        ///     txPacketLossRate:       使用抗丢包技术前,客户端上行发送到服务器丢包率 (%)。
        ///     rxPacketLossRate:       使用抗丢包技术前,服务器下行发送到客户端丢包率 (%)。
        ///     userCount:              当前频道内的用户人数。
        ///     cpuAppUsage:            当前App的CPU使用率 (%)。 注:OnLeaveChannel回调中报告恒为0。自Android8.1起,因系统限制,可能无法通过该属性获取CPU使用率。
        ///     cpuTotalUsage:          当前系统的CPU使用率 (%)。对于Windows平台,在多核环境中该成员指多核CPU的平均使用率。计算方式为(100 - 任务管理中显示的系统空闲进程 CPU)/100。
        ///     connectTimeMs:          从开始建立连接到成功连接的时间(毫秒)。如报告0,则表示无效。
        ///     gatewayRtt:             客户端到本地路由器的往返时延 (ms)。 注:在Android平台上,请确保已在项目AndroidManifest.xml文件的</application>后面添加android.permission.ACCESS_WIFI_STATE权限。
        ///     memoryAppUsageRatio:    当前App的内存占比 (%)。 注:该值仅作参考。受系统限制可能无法获取。
        ///     memoryTotalUsageRatio:  当前系统的内存占比 (%)。 注:该值仅作参考。受系统限制可能无法获取。
        ///     memoryAppUsageInKbytes: 当前App的内存大小 (KB)。 注:该值仅作参考。受系统限制可能无法获取。
        /// </param>
        public override void OnLeaveChannel(RtcConnection connection, RtcStats stats)
        {
    
    
            Main.Log.Info("【Agora RTC】退出频道:{0}", connection.channelId);
            //抛出事件
            Main.Events.Publish(RtcEngineLevelChannelEventArgs.Allocate(connection, stats));
        }

        /// <summary>
        /// 重新加入频道成功回调
        /// 当用户由于网络问题失去与服务器的连接时,SDK会自动尝试重新连接,并在重新连接时触发此回调。
        /// </summary>
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        /// <param name="elapsed">从本地用户调用JoinChannel[1/2]或JoinChannel[2/2]方法到触发此回调所经过的时间 单位毫秒</param>
        public override void OnRejoinChannelSuccess(RtcConnection connection, int elapsed)
        {
    
    
            Main.Log.Info("【Agora RTC】重新加入频道,频道ID:{0}  Elapsed:{1}", connection.channelId, elapsed);
            //抛出事件
            Main.Events.Publish(RtcEngineRejoinChannelEventArgs.Allocate(connection, elapsed));
        }

        /// <summary>
        /// Token已过期回调
        /// 
        /// 在通话过程中如果Token已失效,SDK会触发该回调,提醒App更新Token。
        /// 当收到该回调时,需要重新在服务端生成新的Token,然后调用JoinChannel[2/2]重新加入频道。
        /// </summary>
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        public override void OnRequestToken(RtcConnection connection)
        {
    
    
            Main.Log.Info("【Agora RTC】Token已过期");
        }

        /// <summary>
        /// 当前通话统计回调
        /// SDK定期向App报告当前通话的统计信息,每两秒触发一次。
        /// </summary>
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        /// <param name="stats">通话的统计数据
        ///     duration:               本地用户通话时长(秒),累计值。
        ///     txBytes:                发送字节数(bytes)。
        ///     rxBytes:                接收字节数(bytes)。
        ///     txAudioBytes:           发送音频字节数(bytes),累计值。
        ///     txVideoBytes:           发送视频字节数(bytes),累计值。
        ///     rxAudioBytes:           接收音频字节数(bytes),累计值。
        ///     rxVideoBytes:           接收视频字节数(bytes),累计值。
        ///     txKBitRate:             发送码率(Kbps)。
        ///     rxKBitRate:             接收码率(Kbps)。
        ///     rxAudioKBitRate:        音频接收码率 (Kbps)。
        ///     txAudioKBitRate:        音频包的发送码率 (Kbps)。
        ///     rxVideoKBitRate:        视频接收码率 (Kbps)。
        ///     txVideoKBitRate:        视频发送码率 (Kbps)。
        ///     lastmileDelay:          客户端-接入服务器延时 (毫秒)。
        ///     txPacketLossRate:       使用抗丢包技术前,客户端上行发送到服务器丢包率 (%)。
        ///     rxPacketLossRate:       使用抗丢包技术前,服务器下行发送到客户端丢包率 (%)。
        ///     userCount:              当前频道内的用户人数。
        ///     cpuAppUsage:            当前App的CPU使用率 (%)。 注:OnLeaveChannel回调中报告恒为0。自Android8.1起,因系统限制,可能无法通过该属性获取CPU使用率。
        ///     cpuTotalUsage:          当前系统的CPU使用率 (%)。对于Windows平台,在多核环境中该成员指多核CPU的平均使用率。计算方式为(100 - 任务管理中显示的系统空闲进程 CPU)/100。
        ///     connectTimeMs:          从开始建立连接到成功连接的时间(毫秒)。如报告0,则表示无效。
        ///     gatewayRtt:             客户端到本地路由器的往返时延 (ms)。 注:在Android平台上,请确保已在项目AndroidManifest.xml文件的</application>后面添加android.permission.ACCESS_WIFI_STATE权限。
        ///     memoryAppUsageRatio:    当前App的内存占比 (%)。 注:该值仅作参考。受系统限制可能无法获取。
        ///     memoryTotalUsageRatio:  当前系统的内存占比 (%)。 注:该值仅作参考。受系统限制可能无法获取。
        ///     memoryAppUsageInKbytes: 当前App的内存大小 (KB)。 注:该值仅作参考。受系统限制可能无法获取。
        /// </param>
        public override void OnRtcStats(RtcConnection connection, RtcStats stats)
        {
    
    

        }

        /// <summary>
        /// Token服务将在30s内过期回调。
        /// 
        /// 在通话过程中如果Token即将失效,SDK会提前30秒触发该回调,提醒App更新Token。
        /// 当收到该回调时,需要重新在服务端生成新的Token,然后调用RenewToken将新生成的Token传给 SDK。
        /// </summary>
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        /// <param name="token">即将服务失效的Token</param>
        public override void OnTokenPrivilegeWillExpire(RtcConnection connection, string token)
        {
    
    
            Main.Log.Info("【Agora RTC】Token将在30秒内过期:{0}", token);
        }

        /// <summary>
        /// 远端用户信息已更新回调
        /// 远端用户加入频道后,SDK会获取到该远端用户的UID和UserAccount,然后缓存一个包含了远端用户UID和UserAccount的Mapping表,并在本地触发该回调。
        /// </summary>
        /// <param name="uid">远端用户ID</param>
        /// <param name="info">标识用户信息的UserInfo对象,包含用户UID和UserAccount</param>
        public override void OnUserInfoUpdated(uint uid, Agora.Rtc.UserInfo info)
        {
    
    
            Main.Log.Info("【Agora RTC】远端用户信息更新,Uid:{0}  UserAccount:{1}", uid, info.userAccount);
            //抛出事件
            Main.Events.Publish(RtcEngineUserInfoUpdated.Allocate(uid, info));
        }

        /// <summary>
        /// 远端用户(通信场景)/主播(直播场景)加入当前频道回调
        /// 
        /// 通信场景下,该回调提示有远端用户加入了频道,并返回新加入用户的ID;
        ///     如果加入之前,已经有其他用户在频道中了,新加入的用户也会收到这些已有用户加入频道的回调。
        /// 直播场景下,该回调提示有主播加入了频道,并返回该主播的ID。
        ///     如果在加入之前,已经有主播在频道中了,新加入的用户也会收到已有主播加入频道的回调。声网建议连麦主播不超过17人。
        ///     
        /// 该回调在如下情况下会被触发:
        ///     远端用户/主播调用JoinChannelByKey方法加入频道;
        ///     远端用户加入频道后调用SetClientRole将用户角色改变为主播;
        ///     远端用户/主播网络中断后重新加入频道。
        ///     
        /// 直播场景下:
        ///     主播间能相互收到新主播加入频道的回调,并能获得该主播的uid;
        ///     观众也能收到新主播加入频道的回调,并能获得该主播的uid;
        ///     当Web端加入直播频道时,只要Web端有推流,SDK会默认该Web端为主播,并触发该回调。
        /// </summary>
        /// 
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        /// <param name="remoteUid">新加入频道的远端用户/主播ID</param>
        /// <param name="elapsed">从本地用户调用JoinChannelByKey到该回调触发的延迟 单位毫秒</param>
        public override void OnUserJoined(RtcConnection connection, uint remoteUid, int elapsed)
        {
    
    
            Main.Log.Info("【Agora RTC】用户加入当前频道,Uid:{0}  Elapsed:{1}", remoteUid, elapsed);
            //抛出事件
            Main.Events.Publish(RtcEngineUserJoinedEventArgs.Allocate(connection, remoteUid, elapsed));
        }

        /// <summary>
        /// 远端用户(通信场景)/主播(直播场景)离开当前频道回调
        /// 
        /// 提示有远端用户/主播离开了频道(或掉线)。用户离开频道有两个原因,即正常离开和超时掉线:
        ///     正常离开的时候,远端用户/主播会发送类似“再见”的消息。接收此消息后,判断用户离开频道。
        ///     超时掉线的依据是,在一定时间内(通信场景为20秒,直播场景稍有延时),用户没有收到对方的任何数据包,则判定为对方掉线。
        ///         在网络较差的情况下,有可能会误报。建议使用声网RTM SDK来做可靠的掉线检测。
        /// </summary>
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        /// <param name="remoteUid">离线用户或主播的用户ID</param>
        /// <param name="reason">离线原因
        ///     USER_OFFLINE_QUIT               0:用户主动离开
        ///     USER_OFFLINE_DROPPED            1:因过长时间收不到对方数据包,超时掉线  注:由于SDK使用的是不可靠通道,也有可能对方主动离开频道,但是本地没收到对方离开消息而误判为超时掉线。
        ///     USER_OFFLINE_BECOME_AUDIENCE    2:用户身份从主播切换为观众
        /// </param>
        public override void OnUserOffline(RtcConnection connection, uint remoteUid, USER_OFFLINE_REASON_TYPE reason)
        {
    
    
            Main.Log.Info("【Agora RTC】用户退出当前频道,Uid:{0}  Reason:{1}", remoteUid, reason);
            //抛出事件
            Main.Events.Publish(RtcEngineUserOfflineEventArgs.Allocate(connection, remoteUid, reason));
        }

        #endregion

        #region >> 音视频流管理回调
        /// <summary>
        /// 音频订阅状态发生改变回调
        /// 
        /// STREAM_SUBSCRIBE_STATE 订阅状态:
        ///     SUB_STATE_IDLE              0:加入频道后的初始订阅状态。
        ///     SUB_STATE_NO_SUBSCRIBED     1:订阅失败。可能是因为:
        ///                                     远端用户:
        ///                                         调用MuteLocalAudioStream(true)或MuteLocalVideoStream(true)停止发送本地媒体流。
        ///                                         调用DisableAudio或DisableVideo关闭本地音频或视频模块。
        ///                                         调用EnableLocalAudio(false)或EnableLocalVideo(false)关闭本地音频或视频采集。
        ///                                         用户角色为观众。
        ///                                     本地用户调用以下方法停止接收远端媒体流:
        ///                                         调用MuteRemoteAudioStream(true)、MuteAllRemoteAudioStreams(true)停止接收远端音频流。
        ///                                         调用MuteRemoteVideoStream(true)、MuteAllRemoteVideoStreams(true)停止接收远端视频流。
        ///     SUB_STATE_SUBSCRIBING       2:正在订阅。
        ///     SUB_STATE_SUBSCRIBED        3:收到了远端流,订阅成功。
        /// </summary>
        /// 
        /// <param name="channel">频道名</param>
        /// <param name="uid">用户的ID</param>
        /// <param name="oldState">之前的订阅状态</param>
        /// <param name="newState">当前的订阅状态</param>
        /// <param name="elapseSinceLastState">两次状态变化时间间隔(毫秒)</param>
        public override void OnAudioSubscribeStateChanged(string channel, uint uid, STREAM_SUBSCRIBE_STATE oldState, STREAM_SUBSCRIBE_STATE newState, int elapseSinceLastState)
        {
    
    
            Main.Log.Info("【Agora RTC】音频订阅状态变更,Uid:{0}  {1} >> {2}", uid, oldState, newState);
        }

        /// <summary>
        /// 跨频道媒体流转发事件回调
        /// </summary>
        /// <param name="code">跨频道媒体流转发事件码
        ///     0:网络中断导致用户与服务器连接断开。
        ///     1:用户与服务器建立连接。
        ///     2:用户已加入源频道。
        ///     3:用户已加入源频道。
        ///     4:SDK开始向目标频道发送数据包。
        ///     5:服务器收到了频道发送的视频流。
        ///     6:服务器收到了频道发送的音频流。
        ///     7:目标频道已更新。
        ///     9:目标频道未发生改变,即目标频道更新失败。
        ///     10:目标频道名为NULL。
        ///     11:视频属性已发送至服务器。
        ///     12:暂停向目标频道转发媒体流成功。
        ///     13:暂停向目标频道转发媒体流失败。
        ///     14:恢复向目标频道转发媒体流成功。
        ///     15:恢复向目标频道转发媒体流失败。
        /// </param>
        public override void OnChannelMediaRelayEvent(int code)
        {
    
    
            Main.Log.Info("【Agora RTC】跨频道媒体流转发,Code:{0}", code);
        }

        /// <summary>
        /// 跨频道媒体流转发状态发生改变回调
        /// 当跨频道媒体流转发状态发生改变时,SDK会触发该回调,并报告当前的转发状态以及相关的错误信息。
        /// </summary>
        /// <param name="state">跨频道媒体流转发状态
        ///     0:初始状态。在成功调用StopChannelMediaRelay停止跨频道媒体流转发后,OnChannelMediaRelayStateChanged会回调该状态。
        ///     1:SDK尝试跨频道。
        ///     2:源频道主播成功加入目标频道。
        ///     3:发生异常
        /// </param>
        /// <param name="code">跨频道媒体流转发出错的错误码
        ///     0:一切正常。
        ///     1:服务器回应出错。
        ///     2:服务器无回应。 可以调用LeaveChannel[1/2]方法离开频道。 该错误也可能是由于当前的AppID未开启跨频道连麦导致的。可以联系技术支持申请开通跨频道连麦。
        ///     3:SDK无法获取服务,可能是因为服务器资源有限导致。
        ///     4:发起跨频道转发媒体流请求失败。
        ///     5:接受跨频道转发媒体流请求失败。
        ///     6:服务器接收跨频道转发媒体流失败。
        ///     7:服务器发送跨频道转发媒体流失败。
        ///     8:SDK因网络质量不佳与服务器断开。你可以调用LeaveChannel[1/2]方法离开当前频道。
        ///     9:服务器内部出错。
        ///     10:源频道的Token已过期。
        ///     11:目标频道的Token已过期。
        /// </param>
        public override void OnChannelMediaRelayStateChanged(int state, int code)
        {
    
    
            Main.Log.Info("【Agora RTC】跨频道媒体流转发状态变更,State:{0}  Code:{1}", state, code);
        }
        #endregion

        #region >> 音频处理回调
        /// <summary>
        /// 监测到远端最活跃用户回调
        /// 
        /// 成功调用EnableAudioVolumeIndication后,SDK会持续监测音量最大的远端用户,并统计该用户被判断为音量最大者的次数。
        /// 当前时间段内,该次数累积最多的远端用户为最活跃的用户。
        /// 
        /// 当频道内用户数量大于或等于2且有远端活跃用户时,SDK会触发该回调并报告远端最活跃用户的uid。
        ///     如果远端最活跃用户一直是同一位用户,则SDK不会再次触发OnActiveSpeaker回调。
        ///     如果远端最活跃用户有变化,则SDK会再次触发该回调并报告新的远端最活跃用户的uid。
        /// </summary>
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        /// <param name="uid">远端最活跃用户的ID</param>
        public override void OnActiveSpeaker(RtcConnection connection, uint uid)
        {
    
    
            Main.Log.Info("【Agora RTC】远端最活跃用户,Uid:{0}", uid);
            //抛出事件
            Main.Events.Publish(RtcEngineActiveSpeakerEventArgs.Allocate(connection, uid));
        }

        /// <summary>
        /// 音乐文件的播放状态已改变回调
        /// 该回调在音乐文件播放状态发生改变时触发,并报告当前的播放状态和错误码。
        /// </summary>
        /// <param name="state">音乐文件播放状态
        ///     AUDIO_MIXING_STATE_PLAYING      710: 音乐文件正常播放。
        ///     AUDIO_MIXING_STATE_PAUSED       711: 音乐文件暂停播放。
        ///     AUDIO_MIXING_STATE_STOPPED      713: 音乐文件停止播放。 该状态可能由以下原因导致:
        ///                                                                 AUDIO_MIXING_REASON_ALL_LOOPS_COMPLETED(723)
        ///                                                                 AUDIO_MIXING_REASON_STOPPED_BY_USER(724)
        ///     AUDIO_MIXING_STATE_FAILED       714: 音乐文件播放出错。 该状态可能由以下原因导致:
        ///                                                                 AUDIO_MIXING_REASON_CAN_NOT_OPEN(701)
        ///                                                                 AUDIO_MIXING_REASON_TOO_FREQUENT_CALL(702)
        ///                                                                 AUDIO_MIXING_REASON_INTERRUPTED_EOF(703)
        /// </param>
        /// <param name="reason">错误码
        ///     AUDIO_MIXING_REASON_OK                      0: 成功打开音乐文件。
        ///     AUDIO_MIXING_REASON_CAN_NOT_OPEN            701: 音乐文件打开出错。例如,本地音乐文件不存在、文件格式不支持或无法访问在线音乐文件URL。
        ///     AUDIO_MIXING_REASON_TOO_FREQUENT_CALL       702: 音乐文件打开太频繁。如需多次调用startAudioMixing,请确保调用间隔大于500ms。
        ///     AUDIO_MIXING_REASON_INTERRUPTED_EOF         703: 音乐文件播放中断。
        ///     AUDIO_MIXING_REASON_ONE_LOOP_COMPLETED      721: 音乐文件完成一次循环播放。
        ///     AUDIO_MIXING_REASON_ALL_LOOPS_COMPLETED     723: 音乐文件完成所有循环播放。
        ///     AUDIO_MIXING_REASON_STOPPED_BY_USER         724: 成功调用StopAudioMixing停止播放音乐文件。
        /// </param>
        public override void OnAudioMixingStateChanged(AUDIO_MIXING_STATE_TYPE state, AUDIO_MIXING_REASON_TYPE reason)
        {
    
    
            Main.Log.Info("【Agora RTC】音乐文件播放状态变更,State:{0}  Reason:{1}", state, reason);
        }

        /// <summary>
        /// 音频发布状态改变回调
        /// 
        /// STREAM_PUBLISH_STATE 发布状态:
        ///     PUB_STATE_IDLE          0:加入频道后的初始发布状态。
        ///     PUB_STATE_NO_PUBLISHED  1:发布失败。可能是因为:
        ///                                    本地用户调用MuteLocalAudioStream(true)或MuteLocalVideoStream(true)停止发送本地媒体流。
        ///                                    本地用户调用DisableAudio或DisableVideo关闭本地音频或视频模块。
        ///                                    本地用户调用EnableLocalAudio(false)或EnableLocalVideo(false)关闭本地音频或视频采集。
        ///                                    本地用户角色为观众。
        ///     PUB_STATE_PUBLISHING    2:正在发布。
        ///     PUB_STATE_PUBLISHED     3:发布成功。
        /// </summary>
        /// 
        /// <param name="channel">频道名</param>
        /// <param name="oldState">之前的发布状态</param>
        /// <param name="newState">当前的发布状态</param>
        /// <param name="elapseSinceLastState">两次状态变化时间间隔(毫秒)</param>
        public override void OnAudioPublishStateChanged(string channel, STREAM_PUBLISH_STATE oldState, STREAM_PUBLISH_STATE newState, int elapseSinceLastState)
        {
    
    
            Main.Log.Info("【Agora RTC】音频发布状态变更,Channel:{0}  {1} >> {2}", channel, oldState, newState);
            //抛出事件
            Main.Events.Publish(RtcEngineAudioPublishStateChangedEventArgs.Allocate(channel, oldState, newState, elapseSinceLastState));
        }

        /// <summary>
        /// 远端声音质量回调
        /// 
        /// 该回调描述远端用户在通话中的音频质量,针对每个远端用户/主播每2秒触发一次。
        /// 如果远端同时存在多个用户/主播,该回调每2秒会被触发多次。
        /// </summary>
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        /// <param name="remoteUid">远端用户ID,指定是谁发的音频流</param>
        /// <param name="quality">语音质量
        ///     QUALITY_UNKNOWN     0:网络质量未知
        ///     QUALITY_EXCELLENT   1:网络质量极好
        ///     QUALITY_GOOD        2:用户主观感觉和QUALITY_EXCELLENT差不多,但码率可能略低于QUALITY_EXCELLENT
        ///     QUALITY_POOR        3:用户主观感受有瑕疵但不影响沟通
        ///     QUALITY_BAD         4:勉强能够沟通但不顺畅
        ///     QUALITY_VBAD        5:网络质量非常差,基本不能沟通
        ///     QUALITY_DOWN        6:完全无法沟通
        ///     QUALITY_UNSUPPORTED 7:暂时无法检测网络质量(未使用)
        ///     QUALITY_DETECTING   8:网络质量检测已开始还没完成 </param>
        /// <param name="delay">音频包从发送端到接收端的延迟,包括声音采样前处理、网络传输、网络抖动缓冲引起的延迟 单位毫秒</param>
        /// <param name="lost">音频包从发送端到接收端的丢包率 (%)。</param>
        public override void OnAudioQuality(RtcConnection connection, uint remoteUid, int quality, ushort delay, ushort lost)
        {
    
    

        }

        /// <summary>
        /// 音频路由已发生变化回调
        /// 注:该回调仅适用于Android、IOS和MacOS平台
        /// </summary>
        /// <param name="routing">当前的音频路由
        ///     ROUTE_DEFAULT           -1:使用默认的音频路由。
        ///     ROUTE_HEADSET           0:音频路由为带麦克风的耳机。
        ///     ROUTE_EARPIECE          1:音频路由为带麦克风的耳机。
        ///     ROUTE_HEADSETNOMIC      2:音频路由为不带麦克风的耳机。
        ///     ROUTE_SPEAKERPHONE      3:音频路由为设备自带的扬声器。
        ///     ROUTE_LOUDSPEAKER       4:音频路由为外接的扬声器。(仅适用于IOS和MacOS)
        ///     ROUTE_HEADSETBLUETOOTH  5:音频路由为蓝牙耳机。
        ///     ROUTE_HDMI              6:音频路由为 HDMI 外围设备。(仅适用于MacOS)
        ///     ROUTE_USB               7:音频路由为 USB 外围设备。(仅适用于MacOS)
        ///     ROUTE_DISPLAYPORT       8:音频路由为 DisplayPort 外围设备。(仅适用于MacOS)
        ///     ROUTE_AIRPLAY           9:音频路由为 Apple AirPlay。(仅适用于MacOS)
        /// </param>
        public override void OnAudioRoutingChanged(int routing)
        {
    
    
            Main.Log.Info("【Agora RTC】音频路由变更,Routing:{0}", routing);
        }

        /// <summary>
        /// 音量信息回调
        /// 
        /// 默认情况下,此回调是禁用的,可以通过调用EnableAudioVolumeIndication来启用它。
        /// 一旦启用了这个回调,并且用户在通道中发送流,SDK就会根据EnableAudioVolumeIndication中设置的时间间隔触发OnAudioVolumeIndication回调。
        /// SDK同时触发两个独立的OnAudioVolumeIndication回调,分别报告发送流的本地用户和瞬时音量最高的远程用户(最多三个)的音量信息。
        /// 启用此回调后,如果本地用户调用MuteLocalAudioStream方法进行静音,SDK将继续报告本地用户的音量指示。
        /// 在该通道中音量最高的三个用户之一的远程用户停止发布音频流20秒后,回调将排除该用户的信息;在所有远程用户停止发布音频流20秒后,SDK停止触发远程用户的回调。
        /// </summary>
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        /// <param name="speakers">通话用户的音量信息 空数组表示此时没有远程用户在通道中或发送流。
        /// 用户音量信息:
        ///     uid:        用户ID。在本地用户的回调中,uid为0。在远端用户的回调中,uid为瞬时音量最高的远端用户(最多 3 位)的ID。
        ///     volume:     用户的音量,取值范围为[0,255]。如果用户将自己静音(将MuteLocalAudioStream设为true),但开启了音频采集,该值表示本地采集信号的音量。
        ///     vad:        本地用户的人声状态。0:本地无人声。1:本地有人声。注:无法报告远端用户的人声状态。对于远端用户值始终为1。
        ///                     如需使用此参数,请在调用EnableAudioVolumeIndication时设置reportVad为true。
        ///     voicePitch: 本地用户的人声音调(Hz)。取值范围为 [0.0,4000.0]。注:无法报告远端用户的人声音调。对于远端用户,值始终为0.0。
        /// </param>
        /// <param name="speakerNumber">用户总数 在本地用户回调中,如果本地用户正在发送流,则speakerNumber的值为1。
        ///     在远程用户回调中,speakerNumber的取值范围为[0,3]。如果发送流的远程用户数大于等于3,则speakerNumber的值为3。</param>
        /// <param name="totalVolume">扬声器的音量 取值范围为0(最低)到255(最高)。
        ///     在本地用户的回调中,totalVolume是发送流的本地用户的音量。在远程用户的回调中,totalVolume是瞬时音量最高的远程用户(最多三个)的音量之和。</param>
        public override void OnAudioVolumeIndication(RtcConnection connection, AudioVolumeInfo[] speakers, uint speakerNumber, int totalVolume)
        {
    
    

        }

        /// <summary>
        /// 已发布本地音频首帧回调
        /// 
        /// SDK 会在以下时机触发该回调:
        ///     开启本地音频的情况下,调用JoinChannel[2/2]成功加入频道后。
        ///     调用MuteLocalAudioStream(true),再调用MuteLocalAudioStream(false)后。
        ///     调用DisableAudio,再调用EnableAudio后。
        ///     调用PushAudioFrame成功向SDK推送音频帧后。
        /// </summary>
        /// 
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        /// <param name="elapsed">从调用JoinChannel[2/2]方法到触发该回调的时间间隔 单位毫秒</param>
        public override void OnFirstLocalAudioFramePublished(RtcConnection connection, int elapsed)
        {
    
    
            Main.Log.Info("【Agora RTC】已发布本地音频首帧,频道ID:{0}  Elapsed:{1}", connection.channelId, elapsed);
            //抛出事件
            Main.Events.Publish(RtcEngineFirstLocalAudioFramePublishedEventArgs.Allocate(connection, elapsed));
        }

        /// <summary>
        /// 本地音频状态发生改变回调
        /// 
        /// 本地音频的状态发生改变时(包括本地麦克风采集状态和音频编码状态),SDK会触发该回调报告当前的本地音频状态。
        /// 在本地音频出现故障时,该回调可以帮助了解当前音频的状态以及出现故障的原因,方便排查问题。
        /// </summary>
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        /// <param name="state">当前的本地音频状态
        ///     LOCAL_AUDIO_STREAM_STATE_STOPPED    0:本地音频默认初始状态。
        ///     LOCAL_AUDIO_STREAM_STATE_RECORDING  1:本地音频采集设备启动成功。
        ///     LOCAL_AUDIO_STREAM_STATE_ENCODING   2:本地音频首帧编码成功。
        ///     LOCAL_AUDIO_STREAM_STATE_FAILED     3:本地音频启动失败。
        /// </param>
        /// <param name="error">本地音频出错原因
        ///     LOCAL_AUDIO_STREAM_ERROR_OK                     0:本地音频状态正常。
        ///     LOCAL_AUDIO_STREAM_ERROR_FAILURE                1:本地音频出错原因不明确。建议提示用户尝试重新加入频道。
        ///     LOCAL_AUDIO_STREAM_ERROR_DEVICE_NO_PERMISSION   2:没有权限启动本地音频采集设备。请提示用户开启权限。 废弃:改用OnPermissionError回调中的RECORD_AUDIO。
        ///     LOCAL_AUDIO_STREAM_ERROR_DEVICE_BUSY            3:本地音频采集设备已经在使用中。请提示用户检查麦克风是否被其他应用占用。
        ///                                                         麦克风空闲约5秒后本地音频采集会自动恢复,也可以在麦克风空闲后尝试重新加入频道。(仅适用于Android和IOS)
        ///     LOCAL_AUDIO_STREAM_ERROR_RECORD_FAILURE         4:本地音频采集失败。
        ///     LOCAL_AUDIO_STREAM_ERROR_ENCODE_FAILURE         5:本地音频编码失败。
        ///     LOCAL_AUDIO_STREAM_ERROR_NO_RECORDING_DEVICE    6:无本地音频采集设备。请提示用户在设备的控制面板中检查麦克风是否与设备连接正常,检查麦克风是否正常工作。(仅适用于Windows)
        ///     LOCAL_AUDIO_STREAM_ERROR_NO_PLAYOUT_DEVICE      7:无本地音频播放设备。请提示用户在设备的控制面板中检查扬声器是否与设备连接正常,检查扬声器是否正常工作。(仅适用于Windows)
        ///     LOCAL_AUDIO_STREAM_ERROR_INTERRUPTED            8:本地音频采集被系统来电、Siri、闹钟中断。如需恢复本地音频采集,请用户中止电话、Siri、闹钟。(仅适用于Android和IOS)
        ///     LOCAL_AUDIO_STREAM_ERROR_RECORD_INVALID_ID      9:本地音频采集设备的ID无效。请检查音频采集设备ID。(仅适用于Windows)
        ///     LOCAL_AUDIO_STREAM_ERROR_PLAYOUT_INVALID_ID     10:本地音频播放设备的ID无效。请检查音频播放设备ID。(仅适用于Windows)
        /// </param>
        public override void OnLocalAudioStateChanged(RtcConnection connection, LOCAL_AUDIO_STREAM_STATE state, LOCAL_AUDIO_STREAM_ERROR error)
        {
    
    
            Main.Log.Info("【Agora RTC】本地音频流状态变更,State:{0}  Error:{1}", state, error);
            //抛出事件
            Main.Events.Publish(RtcEngineLocalAudioStateChangedEventArgs.Allocate(connection, state, error));
        }

        /// <summary>
        /// 通话中本地音频流的统计信息回调
        /// SDK每2秒触发该回调一次。
        /// </summary>
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        /// <param name="stats">本地音频流统计数据
        ///     numChannels:        声道数。
        ///     sentSampleRate:     发送本地音频的采样率,单位为Hz。
        ///     sentBitrate:        发送本地音频的码率平均值,单位为 Kbps。
        ///     txPacketLossRate:   弱网对抗前本端到声网边缘服务器的丢包率 (%)。
        ///     internalCodec:      内部的payload类型。
        ///     audioDeviceDelay:   播放或录制音频时,音频设备模块的延时。
        /// </param>
        public override void OnLocalAudioStats(RtcConnection connection, LocalAudioStats stats)
        {
    
    

        }

        /// <summary>
        /// 远端音频流状态发生改变回调
        /// 
        /// 远端用户(通信场景)或主播(直播场景)的音频状态发生改变时,SDK会触发该回调向本地用户报告当前的远端音频流状态。
        /// 注:频道内的用户(通信场景)或主播(直播场景)人数超过17人时,该回调可能不准确。
        /// </summary>
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        /// <param name="remoteUid">远端用户ID</param>
        /// <param name="state">远端音频流状态
        ///     REMOTE_AUDIO_STATE_STOPPED      0:远端音频默认初始状态。在REMOTE_AUDIO_REASON_LOCAL_MUTED、REMOTE_AUDIO_REASON_REMOTE_MUTED或REMOTE_AUDIO_REASON_REMOTE_OFFLINE的情况下,会报告该状态。
        ///     REMOTE_AUDIO_STATE_STARTING     1:本地用户已接收远端音频首包。
        ///     REMOTE_AUDIO_STATE_DECODING     2:远端音频流正在解码,正常播放。在REMOTE_AUDIO_REASON_NETWORK_RECOVERY、REMOTE_AUDIO_REASON_LOCAL_UNMUTED或REMOTE_AUDIO_REASON_REMOTE_UNMUTED的情况下,会报告该状态。
        ///     REMOTE_AUDIO_STATE_FROZEN       3:远端音频流卡顿。在REMOTE_AUDIO_REASON_NETWORK_CONGESTION的情况下,会报告该状态。
        ///     REMOTE_AUDIO_STATE_FAILED       4:远端音频流播放失败。在REMOTE_AUDIO_REASON_INTERNAL的情况下,会报告该状态。
        /// </param>
        /// <param name="reason">远端音频流状态改变的具体原因
        ///     REMOTE_AUDIO_REASON_INTERNAL            0:音频状态发生改变时,会报告该原因。
        ///     REMOTE_AUDIO_REASON_NETWORK_CONGESTION  1:网络阻塞。
        ///     REMOTE_AUDIO_REASON_NETWORK_RECOVERY    2:网络恢复正常。
        ///     REMOTE_AUDIO_REASON_LOCAL_MUTED         3:本地用户停止接收远端音频流或本地用户禁用音频模块。
        ///     REMOTE_AUDIO_REASON_LOCAL_UNMUTED       4:本地用户恢复接收远端音频流或本地用户启动音频模块。
        ///     REMOTE_AUDIO_REASON_REMOTE_MUTED        5:远端用户停止发送音频流或远端用户禁用音频模块。
        ///     REMOTE_AUDIO_REASON_REMOTE_UNMUTED      6:远端用户恢复发送音频流或远端用户启用音频模块。
        ///     REMOTE_AUDIO_REASON_REMOTE_OFFLINE      7:远端用户离开频道。
        /// </param>
        /// <param name="elapsed">从本地用户调用JoinChannel[2/2]方法到发生本事件经历的时间 单位毫秒</param>
        public override void OnRemoteAudioStateChanged(RtcConnection connection, uint remoteUid, REMOTE_AUDIO_STATE state, REMOTE_AUDIO_STATE_REASON reason, int elapsed)
        {
    
    
            Main.Log.Info("【Agora RTC】远端音频流状态变更,Uid:{0}  State:{1}  Reason:{2}", remoteUid, state, reason);
            //抛出事件
            Main.Events.Publish(RtcEngineRemoteAudioStateChangedEventArgs.Allocate(connection, remoteUid, state, reason, elapsed));
        }

        /// <summary>
        /// 远端音频流状态
        /// 远端用户(通信场景)或主播(直播场景)的音频状态发生改变时,SDK会触发该回调向本地用户报告当前的远端音频流状态。
        /// </summary>
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        /// <param name="stats">远端用户的音频流统计数据
        ///     uid:                    远端用户的用户 ID。
        ///     quality:                远端用户发送的音频流质量。详见QUALITY_TYPE。
        ///     networkTransportDelay:  音频发送端到接收端的网络延迟(毫秒)。
        ///     jitterBufferDelay:      音频接收端到网络抖动缓冲的网络延迟(毫秒)。注:当接收端为观众且ClientRoleOptions的audienceLatencyLevel为1时,该参数不生效。
        ///     audioLossRate:          统计周期内的远端音频流的丢帧率 (%)。
        ///     numChannels:            声道数。
        ///     receivedSampleRate:     统计周期内接收到的远端音频流的采样率。
        ///     receivedBitrate:        接收到的远端音频流在统计周期内的平均码率(Kbps)。
        ///     totalFrozenTime:        远端用户在加入频道后发生音频卡顿的累计时长(毫秒)。通话过程中,音频丢帧率达到4%即记为一次音频卡顿。
        ///     frozenRate:             音频卡顿的累计时长占音频总有效时长的百分比 (%)。音频有效时长是指远端用户加入频道后音频未被停止发送或禁用的时长。
        ///     totalActiveTime:        远端用户在音频通话开始到本次回调之间的有效时长(毫秒)。有效时长是指去除了远端用户进入静音状态的总时长。
        ///     publishDuration:        远端音频流的累计发布时长(毫秒)。
        ///     qoeQuality:             接收远端音频时,本地用户的主观体验质量。0: 主观体验质量较好。1: 主观体验质量较差。
        ///     qualityChangedReason:   接收远端音频时,本地用户主观体验质量较差的原因。详见EXPERIENCE_POOR_REASON:
        ///                                     EXPERIENCE_REASON_NONE          0: 无原因,说明主观体验质量较好。
        ///                                     REMOTE_NETWORK_QUALITY_POOR     1: 远端用户的网络较差。
        ///                                     LOCAL_NETWORK_QUALITY_POOR      2: 本地用户的网络较差。
        ///                                     WIRELESS_SIGNAL_POOR            4: 本地用户的Wi-FI或者移动数据网络信号弱。
        ///                                     WIFI_BLUETOOTH_COEXIST          8: 本地用户同时开启Wi-Fi和蓝牙,二者信号互相干扰,导致音频传输质量下降。
        ///     mosValue:               统计周期内,声网实时音频MOS(平均主观意见分)评估方法对接收到的远端音频流的质量评分。
        ///                                 返回值范围为[0,500]。返回值除以100即可得到MOS分数,范围为[0,5]分,分数越高,音频质量越好。
        ///                                 声网实时音频 MOS 评分对应的主观音质感受如下:
        ///                                     大于4分	音频质量佳,清晰流畅。
        ///                                     3.5-4分	音频质量较好,偶有音质损伤,但依然清晰。
        ///                                     3-3.5分	音频质量一般,偶有卡顿,不是非常流畅,需要一点注意力才能听清。
        ///                                     2.5-3分	音频质量较差,卡顿频繁,需要集中精力才能听清。
        ///                                     2-2.5分	音频质量很差,偶有杂音,部分语义丢失,难以交流。
        ///                                     小于2分	音频质量非常差,杂音频现,大量语义丢失,完全无法交流。
        /// </param>
        public override void OnRemoteAudioStats(RtcConnection connection, RemoteAudioStats stats)
        {
    
    

        }

        /// <summary>
        /// 虚拟节拍器状态发生改变回调
        /// 
        /// 虚拟节拍器状态发生改变时,SDK会触发该回调报告当前的虚拟节拍器状态。
        /// 在虚拟节拍器出现故障时,该回调可以帮助了解当前虚拟节拍的状态以及出现故障的原因,方便排查问题。
        /// 
        /// 该回调仅适用于Android和IOS。
        /// </summary>
        /// <param name="state">当前的虚拟节拍器状态
        ///     RHYTHM_PLAYER_STATE_IDLE        810:虚拟节拍器未开启或已关闭。
        ///     RHYTHM_PLAYER_STATE_OPENING     811:正在打开节拍音频文件。
        ///     RHYTHM_PLAYER_STATE_DECODING    812:正在解码节拍音频文件。
        ///     RHYTHM_PLAYER_STATE_PLAYING     813:正在播放节拍音频文件。
        ///     RHYTHM_PLAYER_STATE_FAILED      814:开启虚拟节拍器失败。可以通过报告的错误码errorCode排查错误原因,也可以重新尝试开启虚拟节拍。
        /// </param>
        /// <param name="errorCode">当前的虚拟节拍器状态
        ///     RHYTHM_PLAYER_ERROR_OK                          0:正常播放节拍音频文件,没有错误。
        ///     RHYTHM_PLAYER_ERROR_FAILED                      1:一般性错误,没有明确原因。
        ///     RHYTHM_PLAYER_ERROR_CAN_NOT_OPEN                801:打开节拍音频文件出错。
        ///     RHYTHM_PLAYER_ERROR_CAN_NOT_PLAY                802:播放节拍音频文件出错。
        ///     RHYTHM_PLAYER_ERROR_FILE_OVER_DURATION_LIMIT    803:节拍音频文件时长超出限制。最大时长为1.2秒。
        /// </param>
        public override void OnRhythmPlayerStateChanged(RHYTHM_PLAYER_STATE_TYPE state, RHYTHM_PLAYER_ERROR_TYPE errorCode)
        {
    
    
            Main.Log.Info("【Agora RTC】虚拟节拍器状态变更,State:{0}  ErrorCode:{1}", state, errorCode);
        }

        /// <summary>
        /// 远端用户(通信场景)/主播(直播场景)停止或恢复发送音频流回调
        /// 该回调是由远端用户调用MuteLocalAudioStream方法关闭或开启音频发送触发的。
        /// </summary>
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        /// <param name="remoteUid">远端用户ID</param>
        /// <param name="muted">true:该用户已将音频静音  false:该用户取消了音频静音</param>
        public override void OnUserMuteAudio(RtcConnection connection, uint remoteUid, bool muted)
        {
    
    
            Main.Log.Info("【Agora RTC】远端用户停止或恢复发送音频流,Uid:{0}  IsMute:{1}", remoteUid, muted);
            //抛出事件
            Main.Events.Publish(RtcEngineUserMuteAudioEventArgs.Allocate(connection, remoteUid, muted));
        }
        #endregion

        #region >> 音效处理回调
        /// <summary>
        /// 本地音效文件播放已结束回调
        /// 当播放音效结束后,会触发该回调。
        /// </summary>
        /// <param name="soundId">指定音效的ID 每个音效均有唯一的ID</param>
        public override void OnAudioEffectFinished(int soundId)
        {
    
    
            Main.Log.Info("【Agora RTC】本地音效文件播放结束,SoundId:{0}", soundId);
        }
        #endregion

        #region >> 视频处理回调
        /// <summary>
        /// 报告本地人脸检测结果
        /// 
        /// 调用EnableFaceDetection(true)开启本地人脸检测后,可以通过该回调实时获取以下人脸检测的信息:
        ///     摄像头采集的画面大小
        ///     人脸在view中的位置
        ///     人脸距设备屏幕的距离
        /// 其中,人脸距设备屏幕的距离由 SDK 通过摄像头采集的画面大小和人脸在 view 中的位置拟合计算得出。
        /// 
        /// 注:
        ///     该回调仅适用于Android和IOS平台。
        ///     当检测到摄像头前的人脸消失时,该回调会立刻触发;在无人脸的状态下,该回调触发频率会降低,以节省设备耗能。
        ///     当人脸距离设备屏幕过近时,SDK不会触发该回调。
        /// </summary>
        /// <param name="imageWidth">摄像头采集画面的宽度 (px)</param>
        /// <param name="imageHeight">摄像头采集画面的高度 (px)</param>
        /// <param name="vecRectangle">检测到的人脸信息</param>
        /// <param name="vecDistance">人脸和设备屏幕之间的距离 (cm)</param>
        /// <param name="numFaces">检测的人脸数量。如果为0,则表示没有检测到人脸</param>
        public override void OnFacePositionChanged(int imageWidth, int imageHeight, Rectangle vecRectangle, int[] vecDistance, int numFaces)
        {
    
    

        }

        /// <summary>
        /// 已发布本地视频首帧回调
        /// 
        /// SDK会在以下三种时机触发该回调
        ///     开启本地视频的情况下,调用JoinChannelByKey成功加入频道后;
        ///     调用MuteLocalVideoStream(true),再调用MuteLocalVideoStream(false)后;
        ///     调用DisableVideo,再调用EnableVideo后。
        ///     
        /// </summary>
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        /// <param name="elapsed">从调用JoinChannelByKey到触发该回调的时间间隔 单位毫秒</param>
        public override void OnFirstLocalVideoFramePublished(RtcConnection connection, int elapsed)
        {
    
    
            Main.Log.Info("【Agora RTC】已发布本地视频首帧,频道ID:{0}", connection.channelId);
            //抛出事件
            Main.Events.Publish(RtcEngineLocalVideoFramePublishedEventArgs.Allocate(connection, elapsed));
        }

        /// <summary>
        /// 已接收到远端视频并完成解码回调
        /// 
        /// SDK 会在以下时机触发该回调:
        ///     远端用户首次上线后发送视频。
        ///     远端用户视频离线再上线后发送视频。出现这种中断的可能原因包括:
        ///         远端用户离开频道。
        ///         远端用户掉线。
        ///         远端用户调用 MuteLocalVideoStream 方法停止发送本地视频流。
        ///         远端用户调用 DisableVideo 方法关闭视频模块。
        /// </summary>
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        /// <param name="remoteUid">远端用户ID</param>
        /// <param name="width">视频流宽(px)</param>
        /// <param name="height">视频流高(px)</param>
        /// <param name="elapsed">从本地调用JoinChannel[2/2]开始到该回调触发的延迟(毫秒)</param>
        public override void OnFirstRemoteVideoDecoded(RtcConnection connection, uint remoteUid, int width, int height, int elapsed)
        {
    
    
            Main.Log.Info("【Agora RTC】已接收到远端视频并完成解码,Uid:{0}  Elapsed:{1}", remoteUid, elapsed);
            //抛出事件
            Main.Events.Publish(RtcEngineFirstRemoteVideoDecodedEventArgs.Allocate(connection, remoteUid, width, height, elapsed));
        }

        /// <summary>
        /// 已显示首帧远端视频回调
        /// 第一帧远端视频显示在视图上时,触发此调用。 App可在此调用中获知出图时间(elapsed)。
        /// </summary>
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        /// <param name="remoteUid">用户ID,指定是哪个用户的视频流。</param>
        /// <param name="width">视频流宽(px)</param>
        /// <param name="height">视频流高(px)</param>
        /// <param name="elapsed">从本地调用JoinChannelByKey到发生此事件过去的时间 单位毫秒</param>
        public override void OnFirstRemoteVideoFrame(RtcConnection connection, uint remoteUid, int width, int height, int elapsed)
        {
    
    
            Main.Log.Info("【Agora RTC】已显示首帧远端视频,Uid:{0}  Elapsed:{1}", remoteUid, elapsed);
            //抛出事件
            Main.Events.Publish(RtcEngineFirstRemoteVideoFrameEventArgs.Allocate(connection, remoteUid, width, height, elapsed));
        }

        /// <summary>
        /// 本地视频状态发生改变回调
        /// 
        /// 本地视频的状态发生改变时,SDK会触发该回调返回当前的本地视频状态。
        /// 在本地视频出现故障时,可以通过该回调了解当前视频的状态以及出现故障的原因,方便排查问题。
        /// 
        /// SDK会在如下情况触发该回调,状态为 LOCAL_VIDEO_STREAM_STATE_FAILED,错误码为 LOCAL_VIDEO_STREAM_ERROR_CAPTURE_FAILURE:
        ///     应用退到后台,系统回收摄像头。
        ///     摄像头正常启动,但连续4秒都没有输出采集的视频。
        ///     
        /// 摄像头输出采集的视频帧时,如果连续15帧中,所有视频帧都一样,SDK 触发该回调,状态为 LOCAL_VIDEO_STREAM_STATE_CAPTURING,错误码为 LOCAL_VIDEO_STREAM_ERROR_CAPTURE_FAILURE。
        /// 注意,帧重复检测仅针对分辨率大于200×200、帧率大于等于10fps、码率小于20Kbps 的视频帧。
        /// </summary>
        /// 
        /// <param name="source">视频源类型
        ///     VIDEO_SOURCE_CAMERA_PRIMARY     0:(默认)第一个摄像头。
        ///     VIDEO_SOURCE_CAMERA             0:摄像头。
        ///     VIDEO_SOURCE_CAMERA_SECONDARY   1:第二个摄像头。
        ///     VIDEO_SOURCE_SCREEN_PRIMARY     2:第一个屏幕。
        ///     VIDEO_SOURCE_SCREEN             2:屏幕。
        ///     VIDEO_SOURCE_SCREEN_SECONDARY   3:第二个屏幕。
        ///     VIDEO_SOURCE_CUSTOM             4:自定义的视频源。
        ///     VIDEO_SOURCE_MEDIA_PLAYER       5:媒体播放器共享的视频源。
        ///     VIDEO_SOURCE_RTC_IMAGE_PNG      6:视频源为PNG图片。
        ///     VIDEO_SOURCE_RTC_IMAGE_JPEG     7:视频源为JPEG图片。
        ///     VIDEO_SOURCE_RTC_IMAGE_GIF      8:视频源为GIF图片。
        ///     VIDEO_SOURCE_REMOTE             9:视频源为网络获取的远端视频。
        ///     VIDEO_SOURCE_TRANSCODED         10:转码后的视频源。
        ///     VIDEO_SOURCE_UNKNOWN            100:转码后的视频源。</param>
        /// <param name="state">本地视频状态
        ///     LOCAL_VIDEO_STREAM_STATE_STOPPED    0:本地视频默认初始状态
        ///     LOCAL_VIDEO_STREAM_STATE_CAPTURING  1:本地视频采集设备启动成功  调用StartScreenCaptureByWindowId方法共享窗口且共享窗口为最大化时,也会报告该状态。
        ///     LOCAL_VIDEO_STREAM_STATE_ENCODING   2:本地视频首帧解码成功
        ///     LOCAL_VIDEO_STREAM_STATE_FAILED     3:本地视频启动失败 </param>
        /// <param name="errorCode">本地视频出错原因
        ///     LOCAL_VIDEO_STREAM_ERROR_OK                                     0:本地视频状态正常
        ///     LOCAL_VIDEO_STREAM_ERROR_FAILURE                                1:出错原因不明确
        ///     LOCAL_VIDEO_STREAM_ERROR_DEVICE_NO_PERMISSION                   2:没有权限启动本地视频采集设备
        ///     LOCAL_VIDEO_STREAM_ERROR_DEVICE_BUSY                            3:本地视频采集设备正在使用中
        ///     LOCAL_VIDEO_STREAM_ERROR_CAPTURE_FAILURE                        4:本地视频采集失败,建议检查采集设备是否正常工作
        ///     LOCAL_VIDEO_STREAM_ERROR_ENCODE_FAILURE                         5:本地视频解码失败
        ///     LOCAL_VIDEO_STREAM_ERROR_CAPTURE_INBACKGROUND                   6:IOS- 应用处于后台,无法正常进行视频采集
        ///     LOCAL_VIDEO_STREAM_ERROR_CAPTURE_MULTIPLE_FOREGROUND_APPS       7:IOS- 应用窗口处于侧拉、分屏、画中画模式,无法正常进行视频采集。
        ///     LOCAL_VIDEO_STREAM_ERROR_DEVICE_NOT_FOUND                       8:找不到本地视频采集设备
        ///     LOCAL_VIDEO_STREAM_ERROR_DEVICE_DISCONNECTED                    9:本地视频设备已断开连接
        ///     LOCAL_VIDEO_STREAM_ERROR_DEVICE_INVALID_ID                      10:MacOS、Windows- SDK无法在视频设备列表中找到该视频设备,请检查视频设备ID是否有效
        ///     LOCAL_VIDEO_STREAM_ERROR_SCREEN_CAPTURE_WINDOW_MINIMIZED        11:调用StartScreenCaptureByWindowId方法共享窗口时,共享窗口处于最小化的状态
        ///     LOCAL_VIDEO_STREAM_ERROR_SCREEN_CAPTURE_WINDOW_CLOSED           12:该错误码表示通过窗口ID共享的窗口已关闭,或通过窗口ID共享的全屏窗口已退出全屏。 
        ///                                                                             退出全屏模式后,远端用户将无法看到共享的窗口。为避免远端用户看到黑屏,建议立即结束本次共享。
        ///                                                                         报告该错误码的常见场景:
        ///                                                                             本地用户关闭共享的窗口时;
        ///                                                                             本地用户先放映幻灯片,然后共享放映中的幻灯片,结束放映时;
        ///                                                                             本地用户先全屏查看网页视频或网页文档,然后共享网页视频或网页文档,结束全屏时。
        ///     LOCAL_VIDEO_STREAM_ERROR_SCREEN_CAPTURE_WINDOW_OCCLUDED         13:Windows- 待共享的窗口被其它窗口遮住,被遮挡住的部分在共享时会被SDK涂黑
        ///     LOCAL_VIDEO_STREAM_ERROR_SCREEN_CAPTURE_WINDOW_NOT_SUPPORTED    20:Windows- SDK不支持共享该类型的窗口
        ///     LOCAL_VIDEO_STREAM_ERROR_DEVICE_SYSTEM_PRESSURE                 101:系统压力过大导致当前视频采集设备不可用。</param>
        public override void OnLocalVideoStateChanged(VIDEO_SOURCE_TYPE source, LOCAL_VIDEO_STREAM_STATE state, LOCAL_VIDEO_STREAM_ERROR errorCode)
        {
    
    
            Main.Log.Info("【Agora RTC】本地视频状态变更,Source:{0}  State:{1}  ErrorCode:{2}", source, state, errorCode);
            //抛出事件
            Main.Events.Publish(RtcEngineLocalVideoStateChangedEventArgs.Allocate(source, state, errorCode));
        }

        /// <summary>
        /// 本地视频流统计信息回调
        /// 该回调描述本地设备发送视频流的统计信息,每2秒触发一次。
        /// </summary>
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        /// <param name="stats">本地视频流统计信息
        ///     uid:                            本地用户的ID。
        ///     sentBitrate:                    实际发送码率 (Kbps) 注:不包含丢包后重传视频等的发送码率。
        ///     sentFrameRate:                  实际发送帧率 (fps)。注:不包含丢包后重传视频等的发送帧率。
        ///     captureFrameRate:               本地视频采集帧率 (fps)。
        ///     captureFrameWidth:              本地视频采集宽度 (px)。
        ///     captureFrameHeight:             本地视频采集高度 (px)。
        ///     regulatedCaptureFrameRate:      SDK内置的视频采集适配器(regulator)调整后的摄像头采集视频帧率(fps)。Regulator根据视频编码配置对摄像头采集视频的帧率进行调整。
        ///     regulatedCaptureFrameWidth:     SDK内置的视频采集适配器(regulator)调整后的摄像头采集视频宽度(px)。Regulator根据视频编码配置对摄像头采集视频的宽高进行调整。
        ///     regulatedCaptureFrameHeight:    SDK内置的视频采集适配器(regulator)调整后的摄像头采集视频高度(px)。Regulator根据视频编码配置对摄像头采集视频的宽高进行调整。
        ///     encoderOutputFrameRate:         本地视频编码器的输出帧率,单位为 fps。
        ///     rendererOutputFrameRate:        本地视频渲染器的输出帧率,单位为 fps。
        ///     targetBitrate:                  当前编码器的目标编码码率 (Kbps),该码率为SDK根据当前网络状况预估的一个值。
        ///     targetFrameRate:                当前编码器的目标编码帧率 (fps)。
        ///     qualityAdaptIndication:         统计周期内本地视频质量(基于目标帧率和目标码率)的自适应情况。详见QUALITY_ADAPT_INDICATION:
        ///                                         ADAPT_NONE              0:本地视频质量不变。
        ///                                         ADAPT_UP_BANDWIDTH      1:因网络带宽增加,本地视频质量改善。
        ///                                         ADAPT_DOWN_BANDWIDTH    2:因网络带宽减少,本地视频质量变差。
        ///     encodedBitrate:                 视频编码码率(Kbps)。注:不包含丢包后重传视频等的编码码率。
        ///     encodedFrameHeight:             视频编码高度(px)。
        ///     encodedFrameWidth:              视频编码宽度(px)。
        ///     encodedFrameCount:              视频发送的帧数,累计值。
        ///     codecType:                      视频的编码类型。详见VIDEO_CODEC_TYPE:
        ///                                         VIDEO_CODEC_VP8             1:标准 VP8。
        ///                                         VIDEO_CODEC_H264            2:标准 H.264。
        ///                                         VIDEO_CODEC_H265            3:标准 H.265。
        ///                                         VIDEO_CODEC_GENERIC         6:Generic。本类型主要用于传输视频裸数据(比如用户已加密的视频帧),该类型视频帧以回调的形式返回给用户,需要用户自行解码与渲染。
        ///                                         VIDEO_CODEC_GENERIC_JPEG    20:Generic JPEG。本类型所需的算力较小,可用于算力有限的 IoT 设备。
        ///     txPacketLossRate:               弱网对抗前本端到声网边缘服务器的视频丢包率 (%)。
        ///     captureFrameRate:               本地视频采集帧率 (fps)。
        ///     captureBrightnessLevel:         本地采集的画质亮度级别。详见CAPTURE_BRIGHTNESS_LEVEL_TYPE:
        ///                                         CAPTURE_BRIGHTNESS_LEVEL_INVALID    -1: SDK未检测出本地采集的画质亮度级别。请等待几秒,通过下一次回调获取亮度级别。
        ///                                         CAPTURE_BRIGHTNESS_LEVEL_NORMAL     0: 本地采集的画质亮度正常。
        ///                                         CAPTURE_BRIGHTNESS_LEVEL_BRIGHT     1: 本地采集的画质亮度偏亮。
        ///                                         CAPTURE_BRIGHTNESS_LEVEL_DARK       2: 本地采集的画质亮度偏暗。
        ///     hwEncoderAccelerating:          本地视频编码加速类型。0:采用软件编码,未加速。1:采用硬件编码进行加速。
        /// </param>
        public override void OnLocalVideoStats(RtcConnection connection, LocalVideoStats stats)
        {
    
    

        }

        /// <summary>
        /// 远端视频状态发生改变回调
        /// 
        /// 频道内的用户(通信场景)或主播(直播场景)人数超过17人时,该回调不生效。
        /// 
        /// </summary>
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        /// <param name="remoteUid">发生视频状态改变的远端用户ID</param>
        /// <param name="state">远端视频流状态
        ///     REMOTE_VIDEO_STATE_STOPPED      0:远端视频默认初始状态  在REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3)、
        ///                                         REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5)或REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7)的情况下,会报告该状态。
        ///     REMOTE_VIDEO_STATE_STARTING     1:本地用户已接收远端视频首包。 
        ///     REMOTE_VIDEO_STATE_DECODING     2:远端视频流正在解码,正常播放  在REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY(2)、REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4)、
        ///                                         REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6)或REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9)的情况下,会报告该状态。
        ///     REMOTE_VIDEO_STATE_FROZEN       3:远端视频流卡顿  在REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1)或REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8)的情况下,会报告该状态。
        ///     REMOTE_VIDEO_STATE_FAILED       4:远端视频流播放失败  在REMOTE_VIDEO_STATE_REASON_INTERNAL(0) 的情况下,会报告该状态。
        /// </param>    
        /// <param name="reason">远端视频流状态改变的具体原因
        ///     REMOTE_VIDEO_STATE_REASON_INTERNAL                  0:视频状态发生改变时,会报告该原因
        ///     REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION        1:网络阻塞
        ///     REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY          2:网络恢复正常
        ///     REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED               3:本地用户停止接收远端视频流或本地用户禁用视频模块
        ///     REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED             4:本地用户恢复接收远端视频流或本地用户启用视频模块
        ///     REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED              5:远端用户停止发送视频流或远端用户禁用视频模块
        ///     REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED            6:远端用户恢复发送视频流或远端用户启用视频模块
        ///     REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE            7:远端用户离开频道
        ///     REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK            8:弱网情况下,远端音视频流回退为音频流
        ///     REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY   9:网络情况改善时,远端音频流恢复为音视频流
        /// </param>
        /// <param name="elapsed">从本地用户调用JoinChannelByKey方法到发生本事件经历的时间 单位毫秒</param>
        public override void OnRemoteVideoStateChanged(RtcConnection connection, uint remoteUid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed)
        {
    
    
            Main.Log.Info("【Agora RTC】远端视频状态变更,Uid:{0}  State:{1}  Reason:{2}", remoteUid, state, reason);
            //抛出事件
            Main.Events.Publish(RtcEngineRemoteVideoStateChangedEventArgs.Allocate(connection, remoteUid, state, reason, elapsed));
        }

        /// <summary>
        /// 通话中远端视频流的统计信息回调
        /// 该回调描述远端用户在通话中端到端的视频流统计信息,针对每个远端用户/主播每2秒触发一次。
        /// 如果远端同时存在多个用户/主播,该回调每2秒会被触发多次。
        /// </summary>
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        /// <param name="stats">远端用户的视频流统计数据
        ///     uid:                        用户ID,指定是哪个用户的视频流。
        ///     width:                      视频流宽(像素)。
        ///     height:                     视频流高(像素)。
        ///     receivedBitrate:            (上次统计后)接收到的码率(Kbps)。
        ///     decoderOutputFrameRate:     远端视频解码器的输出帧率,单位为 fps。
        ///     rendererOutputFrameRate:    远端视频渲染器的输出帧率,单位为 fps。
        ///     frameLossRate:              远端视频丢包率(%)。
        ///     packetLossRate:             远端视频在使用抗丢包技术之后的丢包率(%)。
        ///     rxStreamType:               视频流类型,大流或小流。0: 视频大流,即高分辨率、高码率视频流。1: 视频小流,即低分辨率、低码率视频流。
        ///     totalFrozenTime:            远端用户在加入频道后发生视频卡顿的累计时长(ms)。通话过程中,视频帧率设置不低于5fps时,连续渲染的两帧视频之间间隔超过500 ms,则记为一次视频卡顿。
        ///     frozenRate:                 远端用户在加入频道后发生视频卡顿的累计时长占视频总有效时长的百分比 (%)。视频有效时长是指远端用户加入频道后视频未被停止发送或禁用的时长。
        ///     totalActiveTime:            视频有效时长(毫秒):远端用户或主播加入频道后,既没有停止发送视频流,也没有禁用视频模块的通话时长。
        ///     publishDuration:            远端视频流的累计发布时长(毫秒)。
        ///     superResolutionType:        超分辨率的开启状态。大于0:超分辨率已开启  等于0:超分辨率未开启
        ///     avSyncTimeMs:               音频超前视频的时间 (ms)。注:如果为负值,则代表音频落后于视频。
        /// </param>
        public override void OnRemoteVideoStats(RtcConnection connection, RemoteVideoStats stats)
        {
    
    

        }

        /// <summary>
        /// 视频截图结果回调
        /// 成功调用TakeSnapshot后,SDK触发该回调报告截图是否成功和获取截图的详情。
        /// </summary>
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        /// <param name="uid">用户ID 如果为0表示本地用户</param>
        /// <param name="filePath">截图的本地保存路径</param>
        /// <param name="width">图片宽度(px)</param>
        /// <param name="height">图片高度(px)</param>
        /// <param name="errCode">截图成功的提示或失败的原因
        ///     0:截图成功
        ///     -1:截图失败:写入文件失败或JPEG编码失败;
        ///     -2:截图失败:TakeSnapshot方法调用成功后1秒内没有发现指定用户的视频流
        /// </param>
        public override void OnSnapshotTaken(RtcConnection connection, uint uid, string filePath, int width, int height, int errCode)
        {
    
    
            Main.Log.Info("【Agora RTC】视频截图结果,Uid:{0}  ErrorCode:{1}  FilePath:{2}", uid, errCode, filePath);

        }

        /// <summary>
        /// 远端用户开/关本地视频采集回调
        /// 该回调是由远端用户调用EnableLocalVideo方法开启或关闭视频采集触发的。
        /// </summary>
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        /// <param name="remoteUid">远端用户ID</param>
        /// <param name="enabled">远端用户是否启用视频采集
        ///     true: 该用户已启用视频功能。启用后,其他用户可以接收到该用户的视频流。
        ///     false: 该用户已关闭视频功能。关闭后,该用户仍然可以接收其他用户的视频流,但其他用户接收不到该用户的视频流。</param>
        public override void OnUserEnableLocalVideo(RtcConnection connection, uint remoteUid, bool enabled)
        {
    
    
            Main.Log.Info("【Agora RTC】远端用户开关本地视频采集,Uid:{0}  IsEnable:{1}", remoteUid, enabled);
            //抛出事件
            Main.Events.Publish(RtcEngineUserEnableLocalVideoEventArgs.Allocate(connection, remoteUid, enabled));
        }

        /// <summary>
        /// 远端用户开/关视频模块回调
        /// 
        /// 关闭视频功能是指该用户只能进行语音通话,不能显示、发送自己的视频,也不能接收、显示别人的视频。
        /// 该回调是由远端用户调用EnableVideo或DisableVideo方法开启或关闭视频模块触发的。
        /// </summary>
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        /// <param name="remoteUid">远端用户ID</param>
        /// <param name="enabled">true: 该用户已启用视频功能  false:该用户已关闭视频功能</param>
        public override void OnUserEnableVideo(RtcConnection connection, uint remoteUid, bool enabled)
        {
    
    
            Main.Log.Info("【Agora RTC】远端用户开关视频模块,Uid:{0}  IsEnable:{1}", remoteUid, enabled);
            //抛出事件
            Main.Events.Publish(RtcEngineUserEnableVideoEventArgs.Allocate(connection, remoteUid, enabled));
        }

        /// <summary>
        /// 远端用户取消或恢复发布视频流回调
        /// 当远端用户调用MuteLocalVideoStream取消或恢复发布视频流时,SDK会触发该回调向本地用户报告远端用户的发流状况。
        /// </summary>
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        /// <param name="remoteUid">远端用户ID</param>
        /// <param name="muted">true:该用户取消发布视频流  false:发布视频流</param>
        public override void OnUserMuteVideo(RtcConnection connection, uint remoteUid, bool muted)
        {
    
    
            Main.Log.Info("【Agora RTC】远端用户取消或恢复发布视频流,Uid:{0}  IsMute:{1}", remoteUid, muted);
            //抛出事件
            Main.Events.Publish(RtcEngineUserMuteVideoEventArgs.Allocate(connection, remoteUid, muted));
        }

        /// <summary>
        /// 视频设备变化回调
        /// 该回调提示系统视频设备状态发生改变,比如被拔出或移除。
        /// 如果设备已使用外接摄像头采集,外接摄像头被拔开后,视频会中断。
        /// </summary>
        /// <param name="deviceId">设备ID</param>
        /// <param name="deviceType">设备类型
        ///     UNKNOWN_AUDIO_DEVICE                -1:设备类型未知
        ///     AUDIO_PLAYOUT_DEVICE                0:音频播放设备
        ///     AUDIO_RECORDING_DEVICE              1:音频采集设备
        ///     VIDEO_RENDER_DEVICE                 2:视频渲染设备
        ///     VIDEO_CAPTURE_DEVICE                3:视频采集设备
        ///     AUDIO_APPLICATION_PLAYOUT_DEVICE    4:音频应用播放设备 </param>
        /// <param name="deviceState">设备状态
        ///     MEDIA_DEVICE_STATE_IDLE         0:设备处于空闲状态
        ///     MEDIA_DEVICE_STATE_ACTIVE       1:设备正在使用
        ///     MEDIA_DEVICE_STATE_DISABLED     2:设备被禁用
        ///     MEDIA_DEVICE_STATE_NOT_PRESENT  4:没有此设备
        ///     MEDIA_DEVICE_STATE_UNPLUGGED    8:设备被拔出</param>
        public override void OnVideoDeviceStateChanged(string deviceId, MEDIA_DEVICE_TYPE deviceType, MEDIA_DEVICE_STATE_TYPE deviceState)
        {
    
    
            Main.Log.Info("【Agora RTC】视频设备变化,DeviceId:{0}  DeviceType:{1}  DeviceState:{2}", deviceId, deviceType, deviceState);
            //抛出事件
            Main.Events.Publish(RtcEngineVideoDeviceStateChangedEventArgs.Allocate(deviceId, deviceType, deviceState));
        }

        /// <summary>
        /// 视频发布状态改变回调
        /// 
        /// STREAM_PUBLISH_STATE 发布状态:
        ///     PUB_STATE_IDLE          0:加入频道后的初始发布状态。
        ///     PUB_STATE_NO_PUBLISHED  1:发布失败。可能是因为:
        ///                                    本地用户调用MuteLocalAudioStream(true)或MuteLocalVideoStream(true)停止发送本地媒体流。
        ///                                    本地用户调用DisableAudio或DisableVideo关闭本地音频或视频模块。
        ///                                    本地用户调用EnableLocalAudio(false)或EnableLocalVideo(false)关闭本地音频或视频采集。
        ///                                    本地用户角色为观众。
        ///     PUB_STATE_PUBLISHING    2:正在发布。
        ///     PUB_STATE_PUBLISHED     3:发布成功。
        /// </summary>
        /// <param name="source">视频源类型
        ///     VIDEO_SOURCE_CAMERA_PRIMARY     0:(默认)第一个摄像头。
        ///     VIDEO_SOURCE_CAMERA             0:摄像头。
        ///     VIDEO_SOURCE_CAMERA_SECONDARY   1:第二个摄像头。
        ///     VIDEO_SOURCE_SCREEN_PRIMARY     2:第一个屏幕。
        ///     VIDEO_SOURCE_SCREEN             2:屏幕。
        ///     VIDEO_SOURCE_SCREEN_SECONDARY   3:第二个屏幕。
        ///     VIDEO_SOURCE_CUSTOM             4:自定义的视频源。
        ///     VIDEO_SOURCE_MEDIA_PLAYER       5:媒体播放器共享的视频源。
        ///     VIDEO_SOURCE_RTC_IMAGE_PNG      6:视频源为PNG图片。
        ///     VIDEO_SOURCE_RTC_IMAGE_JPEG     7:视频源为JPEG图片。
        ///     VIDEO_SOURCE_RTC_IMAGE_GIF      8:视频源为GIF图片。
        ///     VIDEO_SOURCE_REMOTE             9:视频源为网络获取的远端视频。
        ///     VIDEO_SOURCE_TRANSCODED         10:转码后的视频源。
        ///     VIDEO_SOURCE_UNKNOWN            100:转码后的视频源。</param>
        /// <param name="channel">频道名</param>
        /// <param name="oldState">之前的发布状态</param>
        /// <param name="newState">当前的发布状态</param>
        /// <param name="elapseSinceLastState">两次状态变化时间间隔(毫秒)</param>
        public override void OnVideoPublishStateChanged(VIDEO_SOURCE_TYPE source, string channel, STREAM_PUBLISH_STATE oldState, STREAM_PUBLISH_STATE newState, int elapseSinceLastState)
        {
    
    
            Main.Log.Info("【Agora RTC】视频发布状态变更,Channel:{0}  Source:{1}  {2} >> {3}", channel, source, oldState, newState);
            //抛出事件
            Main.Events.Publish(RtcEngineVideoPublishStateChangedEventArgs.Allocate(source, channel, oldState, newState, elapseSinceLastState));
        }

        /// <summary>
        /// 本地或远端视频大小和旋转信息发生改变回调
        /// </summary>
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        /// <param name="sourceType">视频源类型
        ///     VIDEO_SOURCE_CAMERA_PRIMARY     0:(默认)第一个摄像头。
        ///     VIDEO_SOURCE_CAMERA             0:摄像头。
        ///     VIDEO_SOURCE_CAMERA_SECONDARY   1:第二个摄像头。
        ///     VIDEO_SOURCE_SCREEN_PRIMARY     2:第一个屏幕。
        ///     VIDEO_SOURCE_SCREEN             2:屏幕。
        ///     VIDEO_SOURCE_SCREEN_SECONDARY   3:第二个屏幕。
        ///     VIDEO_SOURCE_CUSTOM             4:自定义的视频源。
        ///     VIDEO_SOURCE_MEDIA_PLAYER       5:媒体播放器共享的视频源。
        ///     VIDEO_SOURCE_RTC_IMAGE_PNG      6:视频源为PNG图片。
        ///     VIDEO_SOURCE_RTC_IMAGE_JPEG     7:视频源为JPEG图片。
        ///     VIDEO_SOURCE_RTC_IMAGE_GIF      8:视频源为GIF图片。
        ///     VIDEO_SOURCE_REMOTE             9:视频源为网络获取的远端视频。
        ///     VIDEO_SOURCE_TRANSCODED         10:转码后的视频源。
        ///     VIDEO_SOURCE_UNKNOWN            100:转码后的视频源。</param>
        /// <param name="uid">图像尺寸和旋转信息发生变化的用户的用户ID(本地用户的uid为0)</param>
        /// <param name="width">视频流的宽度(像素)</param>
        /// <param name="height">视频流的高度(像素)</param>
        /// <param name="rotation">旋转信息 [0,360) </param>
        public override void OnVideoSizeChanged(RtcConnection connection, VIDEO_SOURCE_TYPE sourceType, uint uid, int width, int height, int rotation)
        {
    
    
            Main.Log.Info("【Agora RTC】视频大小和旋转信息变更,Uid:{0}  SourceType:{1}  Width:{2}  Height:{3}  Rotation:{4}", uid, sourceType, width, height, rotation);
        }

        /// <summary>
        /// 视频订阅状态发生改变回调
        /// 
        /// STREAM_SUBSCRIBE_STATE 订阅状态:
        ///     SUB_STATE_IDLE              0:加入频道后的初始订阅状态。
        ///     SUB_STATE_NO_SUBSCRIBED     1:订阅失败。可能是因为:
        ///                                     远端用户:
        ///                                         调用MuteLocalAudioStream(true)或MuteLocalVideoStream(true)停止发送本地媒体流。
        ///                                         调用DisableAudio或DisableVideo关闭本地音频或视频模块。
        ///                                         调用EnableLocalAudio(false)或EnableLocalVideo(false)关闭本地音频或视频采集。
        ///                                         用户角色为观众。
        ///                                     本地用户调用以下方法停止接收远端媒体流:
        ///                                         调用MuteRemoteAudioStream(true)、MuteAllRemoteAudioStreams(true)停止接收远端音频流。
        ///                                         调用 MuteRemoteVideoStream(true)、 MuteAllRemoteVideoStreams(true) 停止接收远端视频流。
        ///     SUB_STATE_SUBSCRIBING       2:正在订阅。
        ///     SUB_STATE_SUBSCRIBED        3:收到了远端流,订阅成功。
        /// </summary>
        /// 
        /// <param name="channel">频道名</param>
        /// <param name="uid">用户ID</param>
        /// <param name="oldState">之前的订阅状态</param>
        /// <param name="newState">当前的订阅状态</param>
        /// <param name="elapseSinceLastState">两次状态变化时间间隔(毫秒)</param>
        public override void OnVideoSubscribeStateChanged(string channel, uint uid, STREAM_SUBSCRIBE_STATE oldState, STREAM_SUBSCRIBE_STATE newState, int elapseSinceLastState)
        {
    
    
            Main.Log.Info("【Agora RTC】视频订阅状态变更,Channel:{0}  Uid:{1}  {2} >> {3}", channel, uid, oldState, newState);
            //抛出事件
            Main.Events.Publish(RtcEngineVideoSubscibeStateChangedEventArgs.Allocate(channel, uid, oldState, newState, elapseSinceLastState));
        }
        #endregion

        #region >> 推流回调
        /// <summary>
        /// CDN推流状态改变回调
        /// 主播端直接向CDN推流后,当推流状态改变时,SDK会触发该回调报告新的状态、错误码和信息。可以据此排查问题。
        /// </summary>
        /// <param name="state">当前CDN推流状态
        ///     DIRECT_CDN_STREAMING_STATE_IDLE         0:初始状态,即推流尚未开始。
        ///     DIRECT_CDN_STREAMING_STATE_RUNNING      1:正在推流中。当调用StartDirectCdnStreaming成功推流时,SDK会返回该值。
        ///     DIRECT_CDN_STREAMING_STATE_STOPPED      2:推流已正常结束。当调用StopDirectCdnStreaming主动停止推流时,SDK会返回该值。
        ///     DIRECT_CDN_STREAMING_STATE_FAILED       3:推流失败。可以通过OnDirectCdnStreamingStateChanged回调报告的信息排查问题,然后重新推流。
        ///     DIRECT_CDN_STREAMING_STATE_RECOVERING   4:尝试重新连接声网服务器和CDN。最多尝试重连10次,如仍未成功恢复连接,则推流状态变为DIRECT_CDN_STREAMING_STATE_FAILED。
        /// </param>
        /// <param name="error">CDN推流出错的原因
        ///     DIRECT_CDN_STREAMING_ERROR_OK                   0:推流状态正常。
        ///     DIRECT_CDN_STREAMING_ERROR_FAILED               1:一般性错误,没有明确原因。可以尝试重新推流。
        ///     DIRECT_CDN_STREAMING_ERROR_AUDIO_PUBLICATION    2:音频推流出错。例如,本地音频采集设备未正常工作、被其他进程占用或没有使用权限。
        ///     DIRECT_CDN_STREAMING_ERROR_VIDEO_PUBLICATION    3:视频推流出错。例如,本地视频采集设备未正常工作、被其他进程占用或没有使用权限。
        ///     DIRECT_CDN_STREAMING_ERROR_NET_CONNECT          4:连接 CDN 失败。
        ///     DIRECT_CDN_STREAMING_ERROR_BAD_NAME             5:URL已用于推流。请使用新的URL。
        /// </param>
        /// <param name="message">状态改变对应的信息</param>
        public override void OnDirectCdnStreamingStateChanged(DIRECT_CDN_STREAMING_STATE state, DIRECT_CDN_STREAMING_ERROR error, string message)
        {
    
    
            Main.Log.Info("【Agora RTC】CDN推流状态变更,State:{0}  Error:{1}  Message:{2}", state, error, message);
        }

        /// <summary>
        /// CDN推流统计数据回调
        /// 在主播直接向CDN推流的过程中,SDK每隔1秒触发一次该回调。
        /// </summary>
        /// <param name="stats">当前推流的统计数据
        ///     videoWidth:     视频的宽度(px)。
        ///     videoHeight:    视频的高度(px)。
        ///     fps:            当前视频帧率(fps)。
        ///     videoBitrate:   当前视频码率(bps)。
        ///     audioBitrate:   当前音频码率(bps)。
        /// </param>
        public override void OnDirectCdnStreamingStats(DirectCdnStreamingStats stats)
        {
    
    

        }

        /// <summary>
        /// 旁路推流事件回调
        /// </summary>
        /// <param name="url">旁路推流URL</param>
        /// <param name="eventCode">旁路推流事件码
        ///     RTMP_STREAMING_EVENT_FAILED_LOAD_IMAGE              1: 旁路推流时,添加背景图或水印出错。
        ///     RTMP_STREAMING_EVENT_URL_ALREADY_IN_USE             2: 该推流URL已用于推流。请使用新的推流URL。
        ///     RTMP_STREAMING_EVENT_ADVANCED_FEATURE_NOT_SUPPORT   3: 功能不支持。
        ///     RTMP_STREAMING_EVENT_REQUEST_TOO_OFTEN              4: 预留参数。
        /// </param>
        public override void OnRtmpStreamingEvent(string url, RTMP_STREAMING_EVENT eventCode)
        {
    
    
            Main.Log.Info("【Agora RTC】旁路推流,Url:{0}  EventCode:{1}", url, eventCode);

        }

        /// <summary>
        /// 旁路推流状态发生改变回调
        /// 
        /// 旁路推流状态发生改变时,SDK会触发该回调,并在回调中明确状态发生改变的URL地址及当前推流状态。
        /// 该回调方便推流用户了解当前的推流状态;推流出错时,可以通过返回的错误码了解出错的原因,方便排查问题。
        /// </summary>
        /// <param name="url">推流状态发生改变的URL地址</param>
        /// <param name="state">当前的推流状态
        ///     RTMP_STREAM_PUBLISH_STATE_IDLE              0:推流未开始或已结束。
        ///     RTMP_STREAM_PUBLISH_STATE_CONNECTING        1:正在连接推流服务器和CDN服务器。
        ///     RTMP_STREAM_PUBLISH_STATE_RUNNING           2:推流正在进行。成功推流后,会返回该状态。
        ///     RTMP_STREAM_PUBLISH_STATE_RECOVERING        3:正在恢复推流。当CDN出现异常,或推流短暂中断时,SDK会自动尝试恢复推流,并返回该状态。
        ///                                                     如成功恢复推流,则进入状态RTMP_STREAM_PUBLISH_STATE_RUNNING(2)。
        ///                                                     如服务器出错或60秒内未成功恢复,则进入状态RTMP_STREAM_PUBLISH_STATE_FAILURE(4)。如果觉得60秒太长,也可以主动尝试重连。
        ///     RTMP_STREAM_PUBLISH_STATE_FAILURE           4:推流失败。失败后,可以通过返回的错误码排查错误原因。
        ///     RTMP_STREAM_PUBLISH_STATE_DISCONNECTING     5:SDK正在与推流服务器和CDN服务器断开连接。当调用StopRtmpStream方法正常结束推流时,
        ///                                                     SDK会依次报告推流状态为RTMP_STREAM_PUBLISH_STATE_DISCONNECTING、RTMP_STREAM_PUBLISH_STATE_IDLE。
        /// </param>
        /// <param name="errCode">推流错误信息
        ///     RTMP_STREAM_PUBLISH_ERROR_OK                            0:推流成功。
        ///     RTMP_STREAM_PUBLISH_ERROR_INVALID_ARGUMENT              1:参数无效。请检查输入参数是否正确。    
        ///     RTMP_STREAM_PUBLISH_ERROR_ENCRYPTED_STREAM_NOT_ALLOWED  2:推流已加密,不能推流。
        ///     RTMP_STREAM_PUBLISH_ERROR_CONNECTION_TIMEOUT            3:推流超时未成功。
        ///     RTMP_STREAM_PUBLISH_ERROR_INTERNAL_SERVER_ERROR         4:推流服务器出现错误。
        ///     RTMP_STREAM_PUBLISH_ERROR_RTMP_SERVER_ERROR             5:CDN 服务器出现错误。
        ///     RTMP_STREAM_PUBLISH_ERROR_TOO_OFTEN                     6:推流请求过于频繁。
        ///     RTMP_STREAM_PUBLISH_ERROR_REACH_LIMIT                   7:单个主播的推流地址数目达到上限10。请删掉一些不用的推流地址再增加推流地址。
        ///     RTMP_STREAM_PUBLISH_ERROR_NOT_AUTHORIZED                8:主播操作不属于自己的流。例如更新其他主播的流参数、停止其他主播的流。请检查应用代码逻辑。
        ///     RTMP_STREAM_PUBLISH_ERROR_STREAM_NOT_FOUND              9:服务器未找到这个流。
        ///     RTMP_STREAM_PUBLISH_ERROR_FORMAT_NOT_SUPPORTED          10:推流地址格式有错误。请检查推流地址格式是否正确。
        ///     RTMP_STREAM_PUBLISH_ERROR_NOT_BROADCASTER               11:用户角色不是主播,该用户无法使用推流功能。请检查应用代码逻辑。
        ///     RTMP_STREAM_PUBLISH_ERROR_TRANSCODING_NO_MIX_STREAM     13:非转码推流情况下,调用了 UpdateRtmpTranscoding 方法更新转码属性。请检查你的应用代码逻辑。
        ///     RTMP_STREAM_PUBLISH_ERROR_NET_DOWN                      14:主播的网络出错。
        ///     RTMP_STREAM_PUBLISH_ERROR_INVALID_PRIVILEGE             16:项目没有使用推流服务的权限。请参考旁路推流中的前提条件开启推流服务。
        ///     RTMP_STREAM_UNPUBLISH_ERROR_OK                          100:推流已正常结束。当结束推流后,SDK会返回该值。
        /// </param>
        public override void OnRtmpStreamingStateChanged(string url, RTMP_STREAM_PUBLISH_STATE state, RTMP_STREAM_PUBLISH_ERROR_TYPE errCode)
        {
    
    
            Main.Log.Info("【Agora RTC】旁路推流状态变更,Url:{0}  State:{1}  ErrorCode:{2}", url, state, errCode);
        }

        /// <summary>
        /// 旁路推流转码设置已被更新回调
        /// 
        /// SetLiveTranscoding方法中的直播参数LiveTranscoding更新时,OnTranscodingUpdated回调会被触发并向主播报告更新信息。
        /// 注:首次调用SetLiveTranscoding方法设置转码参数LiveTranscoding时,不会触发此回调。
        /// </summary>
        public override void OnTranscodingUpdated()
        {
    
    
            Main.Log.Info("【Agora RTC】旁路推流转码设置已被更新");
        }
        #endregion

        #region >> 音频设备管理回调
        /// <summary>
        /// 音频设备变化回调
        /// 提示系统音频设备状态发生改变,比如耳机被拔出。
        /// </summary>
        /// <param name="deviceId">设备ID</param>
        /// <param name="deviceType">设备类型
        ///     UNKNOWN_AUDIO_DEVICE                -1:设备类型未知
        ///     AUDIO_PLAYOUT_DEVICE                0:音频播放设备
        ///     AUDIO_RECORDING_DEVICE              1:音频采集设备
        ///     VIDEO_RENDER_DEVICE                 2:视频渲染设备
        ///     VIDEO_CAPTURE_DEVICE                3:视频采集设备
        ///     AUDIO_APPLICATION_PLAYOUT_DEVICE    4:音频应用播放设备</param>
        /// <param name="deviceState">设备状态
        ///     MEDIA_DEVICE_STATE_IDLE         0:设备处于空闲状态
        ///     MEDIA_DEVICE_STATE_ACTIVE       1:设备正在使用
        ///     MEDIA_DEVICE_STATE_DISABLED     2:设备被禁用
        ///     MEDIA_DEVICE_STATE_NOT_PRESENT  4:没有此设备
        ///     MEDIA_DEVICE_STATE_UNPLUGGED    8:设备被拔出</param>
        public override void OnAudioDeviceStateChanged(string deviceId, MEDIA_DEVICE_TYPE deviceType, MEDIA_DEVICE_STATE_TYPE deviceState)
        {
    
    
            Main.Log.Info("【Agora RTC】音频设备变化,DeviceId:{0}  DeviceType:{1}  DeviceState:{2}", deviceId, deviceType, deviceState);
            //抛出事件
            Main.Events.Publish(RtcEngineAudioDeviceStateChangedEventArgs.Allocate(deviceId, deviceType, deviceState));
        }

        /// <summary>
        /// 音频设备或App的音量发生改变回调
        /// 
        /// 当音频播放、采集设备或App的音量发生改变时,会触发该回调。
        /// 注:该回调仅适用于Windows和MacOS。
        /// </summary>
        /// <param name="deviceType">设备类型
        ///     UNKNOWN_AUDIO_DEVICE                -1:设备类型未知
        ///     AUDIO_PLAYOUT_DEVICE                0:音频播放设备
        ///     AUDIO_RECORDING_DEVICE              1:音频采集设备
        ///     VIDEO_RENDER_DEVICE                 2:视频渲染设备
        ///     VIDEO_CAPTURE_DEVICE                3:视频采集设备
        ///     AUDIO_APPLICATION_PLAYOUT_DEVICE    4:音频应用播放设备</param>
        /// <param name="volume">音量 [0,255]</param>
        /// <param name="muted">音频设备是否为静音状态</param>
        public override void OnAudioDeviceVolumeChanged(MEDIA_DEVICE_TYPE deviceType, int volume, bool muted)
        {
    
    
            Main.Log.Info("【Agora RTC】音频设备或App的音量变化,DeviceType:{0}  Volume:{1}  IsMute:{2}", deviceType, volume, muted);
            //抛出事件
            Main.Events.Publish(RtcEngineAudioDeviceVolumeChangedEventArgs.Allocate(deviceType, volume, muted));
        }
        #endregion

        #region >> 视频设备管理回调
        /// <summary>
        /// 相机对焦区域已改变回调
        /// 
        /// 该回调是由本地用户调用SetCameraFocusPositionInPreview方法改变对焦位置触发的。
        /// 注:该回调仅适用于Android和IOS。
        /// </summary>
        /// <param name="x">发生改变的对焦区域的x坐标</param>
        /// <param name="y">发生改变的对焦区域的y坐标</param>
        /// <param name="width">发生改变的对焦区域的宽度</param>
        /// <param name="height">发生改变的对焦区域的高度</param>
        public override void OnCameraFocusAreaChanged(int x, int y, int width, int height)
        {
    
    

        }

        /// <summary>
        /// 摄像头曝光区域已改变回调
        /// </summary>
        /// <param name="x">发生改变的曝光区域的x坐标</param>
        /// <param name="y">发生改变的曝光区域的y坐标</param>
        /// <param name="width">发生改变的曝光区域的宽度</param>
        /// <param name="height">发生改变的曝光区域的高度</param>
        public override void OnCameraExposureAreaChanged(int x, int y, int width, int height)
        {
    
    

        }
        #endregion

        #region >> 网络及其他回调
        /// <summary>
        /// 网络连接中断,且SDK无法在10秒内连接服务器回调。
        /// 
        /// SDK在调用JoinChannel[2/2]后,无论是否加入成功,只要10秒和服务器无法连接就会触发该回调。
        /// 如果SDK在断开连接后,20分钟内还是没能重新加入频道,SDK会停止尝试重连。
        /// </summary>
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        public override void OnConnectionLost(RtcConnection connection)
        {
    
    
            Main.Log.Info("【Agora RTC】网络连接中断");
            //抛出事件
            Main.Events.Publish(RtcEngineConnectionLostEventArgs.Allocate(connection)); 
        }

        /// <summary>
        /// 网络连接状态已改变回调
        /// 该回调在网络连接状态发生改变的时候触发,并告知用户当前的网络连接状态和引起网络状态改变的原因。
        /// </summary>
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        /// <param name="state">当前网络连接状态
        ///     CONNECTION_STATE_DISCONNECTED   1: 网络连接断开。该状态表示SDK处于:
        ///                                         调用JoinChannel[2/2]加入频道前的初始化阶段。
        ///                                         或调用LeaveChannel[1/2]后的离开频道阶段。
        ///     CONNECTION_STATE_CONNECTING     2: 建立网络连接中。该状态表示SDK在调用JoinChannel[2/2]后正在与指定的频道建立连接。
        ///                                         如果成功加入频道,App会收到该回调,通知当前网络状态变成CONNECTION_STATE_CONNECTED。
        ///                                         建立连接后,SDK还会初始化媒体,一切就绪后会回调OnJoinChannelSuccess。
        ///     CONNECTION_STATE_CONNECTED      3: 网络已连接。该状态表示用户已经加入频道,可以在频道内发布或订阅媒体流。
        ///                                         如果因网络断开或切换而导致SDK与频道的连接中断,SDK会自动重连,此时App会收到该回调,通知当前网络状态变成CONNECTION_STATE_RECONNECTING。
        ///     CONNECTION_STATE_RECONNECTING   4: 重新建立网络连接中。该状态表示SDK之前曾加入过频道,但因网络等原因连接中断了,此时SDK会自动尝试重新接入频道。
        ///                                         如果SDK无法在10秒内重新加入频道,则OnConnectionLost会被触发,SDK会一直保持在CONNECTION_STATE_RECONNECTING的状态,并不断尝试重新加入频道。
        ///                                         如果SDK在断开连接后,20分钟内还是没能重新加入频道,则会收到该回调,通知App的网络状态进入CONNECTION_STATE_FAILED,SDK停止尝试重连。
        ///     CONNECTION_STATE_FAILED         5: 网络连接失败。该状态表示SDK已不再尝试重新加入频道,需要调用LeaveChannel[1/2]离开频道。
        ///                                         如果用户还想重新加入频道,则需要再次调用JoinChannel[2/2]。
        ///                                         如果SDK因服务器端使用RESTful API禁止加入频道,则App会收到该回调。
        /// </param>
        /// <param name="reason">引起当前网络连接状态改变的原因
        ///     CONNECTION_CHANGED_CONNECTING                   0: 建立网络连接中。
        ///     CONNECTION_CHANGED_JOIN_SUCCESS                 1: 成功加入频道。
        ///     CONNECTION_CHANGED_INTERRUPTED                  2: 网络连接中断。
        ///     CONNECTION_CHANGED_BANNED_BY_SERVER             3: 网络连接被服务器禁止。服务端踢人场景时会报这个错。
        ///     CONNECTION_CHANGED_JOIN_FAILED                  4: 加入频道失败。SDK在尝试加入频道20分钟后还是没能加入频道,会返回该状态,并停止尝试重连。
        ///     CONNECTION_CHANGED_LEAVE_CHANNEL                5: 离开频道。
        ///     CONNECTION_CHANGED_INVALID_APP_ID               6: 不是有效的APP ID。请更换有效的APP ID重新加入频道。
        ///     CONNECTION_CHANGED_INVALID_CHANNEL_NAME         7: 不是有效的频道名。请更换有效的频道名重新加入频道。
        ///     CONNECTION_CHANGED_INVALID_TOKEN                8: 生成的Token无效。一般有以下原因:
        ///                                                             在控制台上启用了App Certificate,但加入频道未使用Token。当启用了App Certificate,必须使用Token。
        ///                                                             在调用JoinChannel[2/2]加入频道时指定的用户ID与生成Token时传入的用户 ID 不一致。
        ///     CONNECTION_CHANGED_TOKEN_EXPIRED                9: 当前使用的Token过期,不再有效,需要重新在服务端申请生成Token。
        ///     CONNECTION_CHANGED_REJECTED_BY_SERVER           10: 此用户被服务器禁止。一般有以下原因:
        ///                                                             用户已进入频道,再次调用加入频道的API,例如JoinChannel[2/2],会返回此状态。停止调用该方法即可。
        ///                                                             用户在进行通话测试时尝试加入频道。等待通话测试结束后再加入频道即可。
        ///     CONNECTION_CHANGED_SETTING_PROXY_SERVER         11: 由于设置了代理服务器,SDK尝试重连。
        ///     CONNECTION_CHANGED_RENEW_TOKEN                  12: 更新Token引起网络连接状态改变。
        ///     CONNECTION_CHANGED_CLIENT_IP_ADDRESS_CHANGED    13: 客户端IP地址变更,可能是由于网络类型,或网络运营商的IP或端口发生改变引起。
        ///     CONNECTION_CHANGED_KEEP_ALIVE_TIMEOUT           14: SDK和服务器连接保活超时,进入自动重连状态。
        ///     CONNECTION_CHANGED_REJOIN_SUCCESS               15: 重新加入频道成功。
        ///     CONNECTION_CHANGED_LOST                         16: SDK和服务器失去连接。
        ///     CONNECTION_CHANGED_ECHO_TEST                    17: 连接状态变化由回声测试引起。
        /// </param>
        public override void OnConnectionStateChanged(RtcConnection connection, CONNECTION_STATE_TYPE state, CONNECTION_CHANGED_REASON_TYPE reason)
        {
    
    
            Main.Log.Info("【Agora RTC】网络连接状态变更,State:{0}  Reason:{1}", state, reason);
            //抛出事件
            Main.Events.Publish(RtcEngineConnectionStateChangedEventArgs.Allocate(connection, state, reason));
        }

        /// <summary>
        /// 内置加密出错回调
        /// 调用EnableEncryption开启加密后,如果发流端、收流端出现加解密出错,SDK会触发该回调。
        /// </summary>
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        /// <param name="errorType">错误类型
        ///     ENCRYPTION_ERROR_INTERNAL_FAILURE       0: 内部原因。
        ///     ENCRYPTION_ERROR_DECRYPTION_FAILURE     1: 解密错误。请确保接收端和发送端使用的加密模式或密钥一致。
        ///     ENCRYPTION_ERROR_ENCRYPTION_FAILURE     2: 加密错误。
        /// </param>
        public override void OnEncryptionError(RtcConnection connection, ENCRYPTION_ERROR_TYPE errorType)
        {
    
    
            Main.Log.Info("【Agora RTC】内置加密出错,ErrorType:{0}", errorType);
        }

        /// <summary>
        /// 插件出错回调
        /// 当调用EnableExtension(true)启用插件失败或者插件运行出错时,插件会触发该回调并上报错误码和错误原因。
        /// </summary>
        /// <param name="provider">提供插件的服务商名称</param>
        /// <param name="extension">插件的名称</param>
        /// <param name="error">错误码</param>
        /// <param name="message">错误原因</param>
        public override void OnExtensionError(string provider, string extension, int error, string message)
        {
    
    
            Main.Log.Info("【Agora RTC】插件出错,Provider:{0}  Extension:{1}  Error:{2}  Message:{3}", provider, extension, error, message);
        }

        /// <summary>
        /// 插件事件回调
        /// 当调用EnableExtension(true)启用插件成功时,插件会触发该回调。
        /// </summary>
        /// <param name="provider">提供插件的服务商名称</param>
        /// <param name="extension">插件名称</param>
        /// <param name="key">插件属性的Key</param>
        /// <param name="value">插件属性Key对应的值</param>
        public override void OnExtensionEvent(string provider, string extension, string key, string value)
        {
    
    
            Main.Log.Info("【Agora RTC】插件事件,Provider:{0}  Extension:{1}  Key:{2}  Value:{3}", provider, extension, key, value);
        }

        /// <summary>
        /// 插件启用回调
        /// </summary>
        /// <param name="provider">提供插件的服务商名称</param>
        /// <param name="extension">插件名称</param>
        public override void OnExtensionStarted(string provider, string extension)
        {
    
    
            Main.Log.Info("【Agora RTC】插件启用,Provider:{0}  Extension:{1}", provider, extension);
        }

        /// <summary>
        /// 插件禁用回调
        /// </summary>
        /// <param name="provider">提供插件的服务商名称</param>
        /// <param name="extension">插件名称</param>
        public override void OnExtensionStopped(string provider, string extension)
        {
    
    
            Main.Log.Info("【Agora RTC】插件禁用,Provider:{0}  Extension:{1}", provider, extension);
        }

        /// <summary>
        /// 通话前网络质量探测报告回调
        /// 
        /// 通话前网络上下行Last mile质量探测报告回调。
        /// 在调用StartLastmileProbeTest之后,SDK会在约30秒内返回该回调。
        /// </summary>
        /// <param name="result">上下行Last mile质量探测结果
        ///     state:          Last mile 质量探测结果的状态。详见:LASTMILE_PROBE_RESULT_STATE:
        ///                         LASTMILE_PROBE_RESULT_COMPLETE              1: 表示本次last mile质量探测的结果是完整的。
        ///                         LASTMILE_PROBE_RESULT_INCOMPLETE_NO_BWE     2: 表示本次last mile质量探测未进行带宽预测,因此结果不完整。一个可能的原因是测试资源暂时受限。
        ///                         LASTMILE_PROBE_RESULT_UNAVAILABLE           3: 未进行last mile质量探测。一个可能的原因是网络连接中断。
        ///     uplinkReport:   上行网络质量报告。packetLossRate:丢包率  jitter:网络抖动 (ms)  availableBandwidth:可用网络带宽预估 (bps)。
        ///     downlinkReport: 下行网络质量报告。packetLossRate:丢包率  jitter:网络抖动 (ms)  availableBandwidth:可用网络带宽预估 (bps)。
        ///     rtt:            往返时延 (ms)。
        /// </param>
        public override void OnLastmileProbeResult(LastmileProbeResult result)
        {
    
    

        }

        /// <summary>
        /// 网络上下行last mile质量报告回调
        /// 
        /// 该回调描述本地用户在加入频道前的last mile网络探测的结果,其中last mile是指设备到声网边缘服务器的网络状态。
        /// 加入频道前,调用StartLastmileProbeTest之后,SDK触发该回调报告本地用户在加入频道前的last mile网络探测的结果。
        /// </summary>
        /// <param name="quality">Last mile 网络质量
        ///     QUALITY_UNKNOWN     0:网络质量未知
        ///     QUALITY_EXCELLENT   1:网络质量极好
        ///     QUALITY_GOOD        2:用户主观感觉和QUALITY_EXCELLENT差不多,但码率可能略低于QUALITY_EXCELLENT
        ///     QUALITY_POOR        3:用户主观感受有瑕疵但不影响沟通
        ///     QUALITY_BAD         4:勉强能够沟通但不顺畅
        ///     QUALITY_VBAD        5:网络质量非常差,基本不能沟通
        ///     QUALITY_DOWN        6:完全无法沟通
        ///     QUALITY_UNSUPPORTED 7:暂时无法检测网络质量(未使用)
        ///     QUALITY_DETECTING   8:网络质量检测已开始还没完成 </param>
        public override void OnLastmileQuality(int quality)
        {
    
    
            Main.Log.Info("【Agora RTC】网络上下行last mile质量报告,Quality:{0}", quality);
        }

        /// <summary>
        /// 本地用户成功注册UserAccount回调
        /// 
        /// 本地用户成功调用RegisterLocalUserAccount方法注册用户User Account,
        /// 或调用JoinChannelWithUserAccount[2/2]加入频道后,SDK会触发该回调,并告知本地用户的UID和User Account。
        /// </summary>
        /// <param name="uid">本地用户的ID</param>
        /// <param name="userAccount">本地用户的User Account</param>
        public override void OnLocalUserRegistered(uint uid, string userAccount)
        {
    
    
            Main.Log.Info("【Agora RTC】成功注册User Account,Uid:{0}  UserAccount:{1}", uid, userAccount);
            //抛出事件
            Main.Events.Publish(RtcEngineLocalUserRegisteredEventArgs.Allocate(uid, userAccount));
        }

        /// <summary>
        /// 通话中每个用户的网络上下行last mile质量报告回调
        /// 
        /// 该回调描述每个用户在通话中的last mile网络状态,其中last mile是指设备到声网边缘服务器的网络状态。
        /// 该回调每2秒触发一次。如果远端有多个用户,该回调每2秒会被触发多次。
        /// 
        /// QUALITY_TYPE:
        ///     QUALITY_UNKNOWN     0:网络质量未知
        ///     QUALITY_EXCELLENT   1:网络质量极好
        ///     QUALITY_GOOD        2:用户主观感觉和QUALITY_EXCELLENT差不多,但码率可能略低于QUALITY_EXCELLENT
        ///     QUALITY_POOR        3:用户主观感受有瑕疵但不影响沟通
        ///     QUALITY_BAD         4:勉强能够沟通但不顺畅
        ///     QUALITY_VBAD        5:网络质量非常差,基本不能沟通
        ///     QUALITY_DOWN        6:完全无法沟通
        ///     QUALITY_UNSUPPORTED 7:暂时无法检测网络质量(未使用)
        ///     QUALITY_DETECTING   8:网络质量检测已开始还没完成   
        /// </summary>
        /// 
        /// <param name="connection">RTC连接信息 包含频道ID、用户ID</param>
        /// <param name="remoteUid">远端用户ID</param>
        /// <param name="txQuality">该用户的上行网络质量,基于发送码率、上行丢包率、平均往返时延和网络抖动计算。
        ///     该值代表当前的上行网络质量,帮助判断是否可以支持当前设置的视频编码属性。
        ///     假设上行码率是1000 Kbps,那么支持直播场景下640×480的分辨率、15fps的帧率没有问题,但是支持1280×720的分辨率就会有困难。</param>
        /// <param name="rxQuality">该用户的下行网络质量,基于下行网络的丢包率、平均往返延时和网络抖动计算。</param>
        public override void OnNetworkQuality(RtcConnection connection, uint remoteUid, int txQuality, int rxQuality)
        {
    
    

        }

        /// <summary>
        /// 本地网络类型发生改变回调
        /// 
        /// 本地网络连接类型发生改变时,SDK会触发该回调,并在回调中明确当前的网络连接类型。
        /// 可以通过该回调获取正在使用的网络类型;当连接中断时,该回调能辨别引起中断的原因是网络切换还是网络条件不好。
        /// 
        /// </summary>
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        /// <param name="type">本地网络连接类型
        ///     NETWORK_TYPE_UNKNOWN        -1: 网络连接类型未知。
        ///     NETWORK_TYPE_DISCONNECTED   0: 网络连接已断开。
        ///     NETWORK_TYPE_LAN            1: 网络类型为LAN。
        ///     NETWORK_TYPE_WIFI           2: 网络类型为Wi-Fi (包含热点)。
        ///     NETWORK_TYPE_MOBILE_2G      3: 网络类型为 2G 移动网络。
        ///     NETWORK_TYPE_MOBILE_3G      4: 网络类型为 3G 移动网络。
        ///     NETWORK_TYPE_MOBILE_4G      5: 网络类型为 4G 移动网络。
        /// </param>
        public override void OnNetworkTypeChanged(RtcConnection connection, NETWORK_TYPE type)
        {
    
    
            Main.Log.Info("【Agora RTC】本地网络类型变更,Type:{0}", type);
            //抛出事件
            Main.Events.Publish(RtcEngineNetworkTypeChangedEventArgs.Allocate(connection, type));
        }

        /// <summary>
        /// 获取设备权限出错回调
        /// 无法获取设备权限时,SDK会触发该回调,报告哪个设备的权限无法获取。
        /// </summary>
        /// <param name="permissionType">设备权限类型
        ///     RECORD_AUDIO    0: 音频采集设备的权限。
        ///     CAMERA          1: 摄像头权限。
        ///     SCREEN_CAPTURE  2: 屏幕共享权限。(仅适用于Android) 
        /// </param>
        public override void OnPermissionError(PERMISSION_TYPE permissionType)
        {
    
    
            Main.Log.Info("【Agora RTC】获取设备权限出错,PermissionType:{0}", permissionType);
            //抛出事件
            Main.Events.Publish(RtcEnginePermissionErrorEventArgs.Allocate(permissionType));
        }

        /// <summary>
        /// 代理连接状态回调
        /// 通过该回调可以监听SDK连接代理的状态
        /// 
        /// 例如当用户调用SetCloudProxy或setLocalAccessPoint设置代理或本地代理并成功加入频道后, 
        ///     SDK会触发该回调报告用户ID、连接的代理类型和从调用JoinChannel到触发该回调经过的时间等。
        /// </summary>
        /// <param name="channel">频道名称</param>
        /// <param name="uid">用户ID</param>
        /// <param name="proxyType">代理类型
        ///     NONE_PROXY_TYPE                 0: 预留参数,暂不支持。
        ///     UDP_PROXY_TYPE                  1: UDP协议的云代理,即Force UDP云代理模式。在该模式下,SDK始终通过UDP协议传输数据。
        ///     TCP_PROXY_TYPE                  2: TCP(加密)协议的云代理,即Force TCP云代理模式。在该模式下,SDK始终通过TLS 443传输数据。
        ///     LOCAL_PROXY_TYPE                3: 预留参数,暂不支持。
        ///     TCP_PROXY_AUTO_FALLBACK_TYPE    4: 自动模式。在该模式下,SDK优先连接SD-RTN™,如果连接失败,自动切换为TLS 443。
        /// </param>
        /// <param name="localProxyIp">本地代理的IP地址 当代理类型不为3时改参数值为空</param>
        /// <param name="elapsed">从调用JoinChannel到SDK触发该回调的经过的时间 单位毫秒</param>
        public override void OnProxyConnected(string channel, uint uid, PROXY_TYPE proxyType, string localProxyIp, int elapsed)
        {
    
    
            Main.Log.Info("【Agora RTC】代理连接状态,Channel:{0}  Uid:{1}  ProxyType:{2}  LocalProxyIP:{3}", channel, uid, proxyType, localProxyIp);
        }

        /// <summary>
        /// 接收到对方数据流消息的回调
        /// 该回调表示本地用户收到了远端用户调用SendStreamMessage方法发送的流消息。
        /// </summary>
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        /// <param name="remoteUid">远端用户ID</param>
        /// <param name="streamId">接收到的消息的StreamID</param>
        /// <param name="data">接收到的数据</param>
        /// <param name="length">数据长度 单位字节</param>
        /// <param name="sentTs">数据流发出的时间</param>
        public override void OnStreamMessage(RtcConnection connection, uint remoteUid, int streamId, byte[] data, uint length, ulong sentTs)
        {
    
    
            Main.Log.Info("【Agora RTC】接收到对方数据流消息,Uid:{0}  StreamId:{1}  Length:{2}  SentTs:{3}", remoteUid, streamId, length, sentTs);
        }

        /// <summary>
        /// 接收对方数据流消息发生错误的回调
        /// 该回调表示本地用户未收到远端用户调用SendStreamMessage方法发送的流消息。
        /// </summary>
        /// <param name="connection">RTC连接信息 包含频道ID、本地用户ID</param>
        /// <param name="remoteUid">发送消息的用户ID</param>
        /// <param name="streamId">接收到的消息的StreamID</param>
        /// <param name="code">发生错误的错误码</param>
        /// <param name="missed">丢失的消息数量</param>
        /// <param name="cached">数据流中断时,后面缓存的消息数量</param>
        public override void OnStreamMessageError(RtcConnection connection, uint remoteUid, int streamId, int code, int missed, int cached)
        {
    
    
            Main.Log.Info("【Agora RTC】接收对方数据流消息发生错误,Uid:{0}  StreamId:{1}  Code:{2}  Missed:{3}  Cached:{4}", remoteUid, streamId, code, missed, cached);
        }

        /// <summary>
        /// 上行网络信息发生变化回调
        /// </summary>
        /// <param name="info">上行网络信息
        ///     video_encoder_target_bitrate_bps:目标视频编码器的码率 (bps)。
        /// </param>
        public override void OnUplinkNetworkInfoUpdated(UplinkNetworkInfo info)
        {
    
    

        }
        #endregion
    }
}

部分核心API调用封装:

/* =======================================================
 *  Unity版本:2020.3.16f1c1
 *  作 者:CoderZ
 *  邮 箱:[email protected]
 *  创建时间:2023-05-09 14:47:49
 *  当前版本:1.0.0
 *  主要功能:音视频功能
 *  详细描述:基于Agora声网SDK v4.1.0  接口调用封装
 *  修改记录:
 * =======================================================*/

using Agora.Rtc;
using SK.Framework;
using SK.Framework.Events;
using System.Collections.Generic;

namespace Metaverse
{
    
    
    public class RtcEngine
    {
    
    
        private const string appId = "";
        private const string token = "";

        private IRtcEngine engine;

        //RTC的用户ID与环信IM的用户ID的映射表
        private Dictionary<uint, string> uidMap = new Dictionary<uint, string>();

        /// <summary>
        /// 当前RTC连接信息
        /// </summary>
        public RtcConnection connection;

        /// <summary>
        /// 音频流发布状态
        /// </summary>
        public STREAM_PUBLISH_STATE AudioPublishState {
    
     get; private set; }

        /// <summary>
        /// 视频流发布状态
        /// </summary>
        public STREAM_PUBLISH_STATE VideoPublishState {
    
     get; private set; }

        public IRtcEngine Get()
        {
    
    
            return engine;
        }

        /// <summary>
        /// 初始化
        /// </summary>
        /// <returns>0:初始化成功  小于0:初始化失败
        ///     -1:     一般性的错误(未明确归类)。
        ///     -2:     设置了无效的参数。
        ///     -7:     SDK 初始化失败。
        ///     -22:    资源申请失败。当 app 占用资源过多,或系统资源耗尽时,SDK 分配资源失败,会返回该错误。
        ///     -101:   App ID 无效。
        /// </returns>
        public int Initialize()
        {
    
    
            Main.Log.Info("【RTC Engine】开始初始化...");
            //创建IRtcEngine对象
            engine = Agora.Rtc.RtcEngine.CreateAgoraRtcEngine();
            //IRtcEngine实例的配置
            RtcEngineContext context = new RtcEngineContext(appId, 0,
                CHANNEL_PROFILE_TYPE.CHANNEL_PROFILE_LIVE_BROADCASTING,
                AUDIO_SCENARIO_TYPE.AUDIO_SCENARIO_DEFAULT,
                AREA_CODE.AREA_CODE_GLOB);
            //初始化IRtcEngine
            int code = engine.Initialize(context);
            if (code == 0)
            {
    
    
                Main.Log.Info("【RTC Engine】初始化成功");
                //添加回调事件处理器
                if (engine.InitEventHandler(new RtcEngineEventHandler()) == 0)
                    Main.Log.Info("【RTC Engine】添加回调事件处理器成功");
                else
                    Main.Log.Info("【RTC Engine】添加回调事件处理器失败");
            }
            else
                Main.Log.Info("【RTC Engine】初始化失败:{0}", code);

            //默认开启音频和视频模块
            engine.EnableAudio();
            engine.EnableVideo();

            engine.SetVideoEncoderConfiguration(new VideoEncoderConfiguration()
            {
    
    
                dimensions = new VideoDimensions(160, 100),
                frameRate = 15,
                bitrate = 0,
            });
            engine.SetChannelProfile(CHANNEL_PROFILE_TYPE.CHANNEL_PROFILE_COMMUNICATION);
            engine.SetClientRole(CLIENT_ROLE_TYPE.CLIENT_ROLE_BROADCASTER);
            return code;
        }

        /// <summary>
        /// 释放
        /// </summary>
        public void Dispose()
        {
    
    
            if (engine != null)
            {
    
    
                Main.Log.Info("【RTC Engine】释放");
                engine.InitEventHandler(null);
                engine.Dispose();
                engine = null;
            }
        }

        /// <summary>
        /// 加入频道(JoinChannel[1/2])
        /// 
        /// 该方法让用户加入通话频道,在同一个频道内的用户可以互相通话,多个用户加入同一个频道,可以群聊。
        /// 
        /// 成功调用该方法加入频道后会触发以下回调:
        ///     本地会触发 OnJoinChannelSuccess 和 OnConnectionStateChanged 回调。
        ///     通信场景下的用户和直播场景下的主播加入频道后,远端会触发 OnUserJoined 回调。
        ///     
        /// 注:用户成功加入频道后,默认订阅频道内所有其他用户的音频流和视频流,因此产生用量并影响计费。
        ///     如果想取消订阅,可以通过调用相应的 mute 方法实现。
        /// 
        /// </summary>
        /// <param name="channelId">频道名。该参数标识用户进行实时音视频互动的频道。
        ///     填入相同频道名的用户会进入同一个频道进行音视频互动。
        ///     该参数为长度在 64 字节以内的字符串。
        /// </param>
        /// <param name="info">(非必选项) 预留参数</param>
        /// <param name="uid">用户ID。该参数用于标识在实时音视频互动频道中的用户。
        ///     需要自行设置和管理用户ID,并确保同一频道内的每个用户ID是唯一的。
        ///     如果不指定(即设为 0),SDK会自动分配一个,并在 OnJoinChannelSuccess 回调中返回, 
        ///     应用层必须记住该返回值并维护,SDK不对该返回值进行维护。
        /// </param>
        /// <param name="token">在服务端生成的用于鉴权的动态密钥</param>
        public void JoinChannel(string channelId, string info = null, uint uid = 0, string token = token)
        {
    
    
            if (engine != null)
            {
    
    
                int code = engine.JoinChannel(token, channelId, info, uid);
                if (code != 0)
                {
    
    
                    Main.Log.Info("【RTC Engine】加入频道失败,ErrorCode:{0}  Channel:{1}  Info:{2}  Uid:{3}  Token:{4}", code, channelId, info, uid, token);
                }
            }
            else
            {
    
    
                Main.Log.Info("【RTC Engine】未初始化,请先调用Initialize进行初始化。");
            }
        }

        /// <summary>
        /// 设置媒体选项并加入频道(JoinChannel[2/2])
        /// 
        /// 相比 JoinChannel [1/2],该方法增加了 options 参数,用于配置用户加入频道时是否自动订阅频道内所有远端音视频流。
        /// 默认情况下,用户订阅频道内所有其他用户的音频流和视频流,因此会产生用量并影响计费。
        /// 如果想取消订阅,可以通过设置 options 参数或相应的 mute 方法实现。
        /// </summary>
        /// <param name="channelId">频道名。该参数标识用户进行实时音视频互动的频道。
        ///     填入相同频道名的用户会进入同一个频道进行音视频互动。
        ///     该参数为长度在 64 字节以内的字符串。
        /// </param>
        /// <param name="options">频道媒体设置选项</param>
        /// <param name="uid">用户ID。该参数用于标识在实时音视频互动频道中的用户。
        ///     需要自行设置和管理用户ID,并确保同一频道内的每个用户ID是唯一的。
        ///     如果不指定(即设为 0),SDK会自动分配一个,并在 OnJoinChannelSuccess 回调中返回, 
        ///     应用层必须记住该返回值并维护,SDK不对该返回值进行维护。
        /// </param>
        /// <param name="token">在服务端生成的用于鉴权的动态密钥</param>
        public void JoinChannel(string channelId, ChannelMediaOptions options, uint uid = 0, string token = token)
        {
    
    
            if (engine != null)
            {
    
    
                int code = engine.JoinChannel(token, channelId, uid, options);
                if (code != 0)
                {
    
    
                    Main.Log.Info("【RTC Engine】加入频道失败,ErrorCode:{0}  Channel:{1}  Uid:{2}  Token:{3}", code, channelId, uid, token);
                }
            }
            else
            {
    
    
                Main.Log.Info("【RTC Engine】未初始化,请先调用Initialize进行初始化。");
            }
        }

        /// <summary>
        /// 使用UserAccount加入频道。(JoinChannelWithUserAccount [1/2])
        /// 
        /// 该方法允许本地用户使用User Account和Token加入频道。成功加入频道后,会触发以下回调:
        /// 本地:OnLocalUserRegistered、OnJoinChannelSuccess 和 OnConnectionStateChanged 回调。
        /// 通信场景下的用户和直播场景下的主播加入频道后,远端会依次触发 OnUserJoined 和 OnUserInfoUpdated 回调。
        /// </summary>
        /// <param name="userAccount">该参数用于标识实时音视频互动频道中的用户。
        ///     需要自行设置和管理用户的User Account,并确保同一频道中每个用户的User Account 是唯一的。 
        ///     该参数为必填,最大不超过 255 字节,不可填 NULL。
        /// </param>
        /// <param name="channelId">频道名。该参数标识用户进行实时音视频互动的频道。
        ///     填入相同频道名的用户会进入同一个频道进行音视频互动。
        ///     该参数为长度在 64 字节以内的字符串。
        /// </param>
        /// <param name="token">在服务端生成的用于鉴权的动态密钥</param>
        public void JoinChannelWithUserAccount(string userAccount, string channelId, string token = token)
        {
    
    
            if (engine != null)
            {
    
    
                int code = engine.JoinChannelWithUserAccount(token, channelId, userAccount);
                if (code != 0)
                {
    
    
                    Main.Log.Info("【RTC Engine】加入频道失败,ErrorCode:{0}  Channel:{1}  UserAccount:{2}  Token:{3}", code, channelId, userAccount, token);
                }
            }
            else
            {
    
    
                Main.Log.Info("【RTC Engine】未初始化,请先调用Initialize进行初始化。");
            }
        }

        /// <summary>
        /// 使用User Account加入频道,并设置是否自动订阅音频或视频流。(JoinChannelWithUserAccount [2/2])
        /// 
        /// 相比 JoinChannelWithUserAccount [1/2],该方法增加了 options 参数,用于配置用户加入频道时是否自动订阅频道内所有远端音视频流。
        /// 默认情况下,用户订阅频道内所有其他用户的音频流和视频流,因此会产生用量并影响计费。
        /// 如果想取消订阅,可以通过设置 options 参数或相应的 mute 方法实现。
        /// </summary>
        /// <param name="userAccount">该参数用于标识实时音视频互动频道中的用户。
        ///     需要自行设置和管理用户的User Account,并确保同一频道中每个用户的User Account 是唯一的。 
        ///     该参数为必填,最大不超过 255 字节,不可填 NULL。
        /// </param>
        /// <param name="channelId">频道名。该参数标识用户进行实时音视频互动的频道。
        ///     填入相同频道名的用户会进入同一个频道进行音视频互动。
        ///     该参数为长度在 64 字节以内的字符串。
        /// </param>
        /// <param name="options">频道媒体设置选项</param>
        /// <param name="token">在服务端生成的用于鉴权的动态密钥</param>
        public void JoinChannelWithUserAccount(string userAccount, string channelId, ChannelMediaOptions options, string token = token)
        {
    
    
            if (engine != null)
            {
    
    
                int code = engine.JoinChannelWithUserAccount(token, channelId, userAccount, options);
                if (code != 0)
                {
    
    
                    Main.Log.Info("【RTC Engine】加入频道失败,ErrorCode:{0}  Channel:{1}  UserAccount:{2}  Token:{3}", code, channelId, userAccount, token);
                }
            }
            else
            {
    
    
                Main.Log.Info("【RTC Engine】未初始化,请先调用Initialize进行初始化。");
            }
        }

        /// <summary>
        /// 退出频道(LeaveChannel [1/2])
        /// 
        /// 该方法会把会话相关的所有资源释放掉。
        /// 该方法是异步操作,调用返回时并没有真正退出频道。
        /// 成功加入频道后,必须调用本方法或者LeaveChannel [2/2]结束通话,否则无法开始下一次通话。
        /// 成功调用该方法、并且离开频道后会触发以下回调:
        ///     本地:OnLeaveChannel 回调。
        ///     远端:通信场景下的用户和直播场景下的主播离开频道后,远端会触发 OnUserOffline 回调。
        /// </summary>
        public void LevelChannel()
        {
    
    
            if (engine != null)
            {
    
    
                int code = engine.LeaveChannel();
                if (code != 0)
                {
    
    
                    Main.Log.Info("【RTC Engine】离开频道失败,ErrorCode:{0}", code);
                }
            }
            else
            {
    
    
                Main.Log.Info("【RTC Engine】未初始化,请先调用Initialize进行初始化。");
            }
        }

        /// <summary>
        /// 设置频道选项并离开频道(LeaveChannel [2/2])
        /// 
        /// 该方法会把会话相关的所有资源释放掉,离开频道,即挂断或退出通话。不管当前是否在通话中均可以调用该方法。
        /// 加入频道后,必须调用本方法结束通话,才能开始下一次通话。
        /// 该方法是异步操作,调用返回时并没有真正退出频道。在真正退出频道后,本地会触发 OnLeaveChannel 回调;
        /// 通信场景下的用户和直播场景下的主播离开频道后,远端会触发 OnUserOffline 回调。
        /// </summary>
        /// <param name="options"></param>
        public void LevelChannel(LeaveChannelOptions options)
        {
    
    
            if (engine != null)
            {
    
    
                int code = engine.LeaveChannel(options);
                if (code != 0)
                {
    
    
                    Main.Log.Info("【RTC Engine】离开频道失败,ErrorCode:{0}", code);
                }
            }
            else
            {
    
    
                Main.Log.Info("【RTC Engine】未初始化,请先调用Initialize进行初始化。");
            }
        }

        /// <summary>
        /// 发布、停止发布音频流
        /// </summary>
        /// <param name="publish">true:发布  false:停止发布</param>
        public void PublishMicrophoneTrack(bool publish)
        {
    
    
            if (engine != null)
            {
    
    
                var options = new ChannelMediaOptions();
                options.publishMicrophoneTrack.SetValue(publish);
                int code = engine.UpdateChannelMediaOptions(options);
                if (code == 0)
                {
    
    
                    Main.Log.Info("【RTC Engine】{0}音频流", publish ? "发布" : "停止发布");
                }
                else
                {
    
    
                    Main.Log.Info("【RTC Engine】{0}音频流失败,ErrorCode:{1}", publish ? "发布" : "停止发布", code);
                }
            }
            else
            {
    
    
                Main.Log.Info("【RTC Engine】未初始化,请先调用Initialize进行初始化。");
            }
        }

        /// <summary>
        /// 获取音频播放设备列表信息
        /// </summary>
        /// <returns>音频播放设备列表</returns>
        public DeviceInfo[] GetAudioPlaybackDeviceInfo()
        {
    
    
            if (engine != null)
            {
    
    
                var deviceManager = engine.GetAudioDeviceManager();
                var deviceInfos = deviceManager.EnumeratePlaybackDevices();
                Main.Log.Info("【RTC Engine】音频播放设备数量:{0}", deviceInfos.Length);
                return deviceInfos;
            }
            else
            {
    
    
                Main.Log.Info("【RTC Engine】未初始化,请先调用Initialize进行初始化。");
                return null;
            }
        }

        /// <summary>
        /// 设置音频播放设备
        /// </summary>
        /// <param name="deviceInfo">设备信息</param>
        public void SetAudioPlaybackDevice(DeviceInfo deviceInfo)
        {
    
    
            if (engine != null)
            {
    
    
                var deviceManager = engine.GetAudioDeviceManager();
                int code = deviceManager.SetPlaybackDevice(deviceInfo.deviceId);
                if (code == 0)
                {
    
    
                    Main.Log.Info("【RTC Engine】设置音频播放设备,DeviceID:{0}  DeviceName:{1}", deviceInfo.deviceId, deviceInfo.deviceName);
                }
                else
                {
    
    
                    Main.Log.Info("【RTC Engine】设置音频播放设备失败,ErrorCode:{0}", code);
                }
            }
            else
            {
    
    
                Main.Log.Info("【RTC Engine】未初始化,请先调用Initialize进行初始化。");
            }
        }

        /// <summary>
        /// 发布、停止发布视频流
        /// </summary>
        /// <param name="publish">true:发布  false:停止发布</param>
        public void PublishCameraTrack(bool publish)
        {
    
    
            if (engine != null)
            {
    
    
                var options = new ChannelMediaOptions();
                options.publishCameraTrack.SetValue(publish);
                int code = engine.UpdateChannelMediaOptions(options);
                if (code == 0)
                {
    
    
                    Main.Log.Info("【RTC Engine】{0}视频流", publish ? "发布" : "停止发布");
                }
                else
                {
    
    
                    Main.Log.Info("【RTC Engine】{0}视频流失败,ErrorCode:{1}", publish ? "发布" : "停止发布", code);
                }
            }
            else
            {
    
    
                Main.Log.Info("【RTC Engine】未初始化,请先调用Initialize进行初始化。");
            }
        }

        /// <summary>
        /// 获取视频设备列表信息
        /// </summary>
        /// <returns>视频设备列表</returns>
        public DeviceInfo[] GetVideoDeviceInfo()
        {
    
    
            if (engine != null)
            {
    
    
                var deviceManager = engine.GetVideoDeviceManager();
                var deviceInfos = deviceManager.EnumerateVideoDevices();
                Main.Log.Info("【RTC Engine】视频设备数量:{0}", deviceInfos.Length);
                return deviceInfos;
            }
            else
            {
    
    
                Main.Log.Info("【RTC Engine】未初始化,请先调用Initialize进行初始化。");
                return null;
            }
        }

        /// <summary>
        /// 设置视频采集设备
        /// </summary>
        /// <param name="deviceInfo">设备信息</param>
        public void SetVideoCaptureDevice(DeviceInfo deviceInfo)
        {
    
    
            if (engine != null)
            {
    
    
                var deviceManager = engine.GetVideoDeviceManager();
                int code = deviceManager.SetDevice(deviceInfo.deviceId);
                if (code == 0)
                {
    
    
                    Main.Log.Info("【RTC Engine】设置视频采集设备,DeviceID:{0}  DeviceName:{1}", deviceInfo.deviceId, deviceInfo.deviceName);
                }
                else
                {
    
    
                    Main.Log.Info("【RTC Engine】设置视频采集设备失败,ErrorCode:{0}", code);
                }
            }
            else
            {
    
    
                Main.Log.Info("【RTC Engine】未初始化,请先调用Initialize进行初始化。");
            }
        }
    }
}

测试

通过OnGUI添加如下测试按钮:

private void OnGUI()
{
    
    
    if (GUILayout.Button("退出房间", GUILayout.Width(200f), GUILayout.Height(50f)))
    {
    
    
        Main.Custom.RoomManager.LeaveRoom();
    }
    if (GUILayout.Button("开启麦克风", GUILayout.Width(200f), GUILayout.Height(50f)))
    {
    
    
        Main.Custom.RtcEngine.PublishMicrophoneTrack(true);
    }
    if (GUILayout.Button("关闭麦克风", GUILayout.Width(200f), GUILayout.Height(50f)))
    {
    
    
        Main.Custom.RtcEngine.PublishMicrophoneTrack(false);
    }
    if (GUILayout.Button("开启摄像头", GUILayout.Width(200f), GUILayout.Height(50f)))
    {
    
    
        Main.Custom.RtcEngine.PublishCameraTrack(true);
    }
    if (GUILayout.Button("关闭摄像头", GUILayout.Width(200f), GUILayout.Height(50f)))
    {
    
    
        Main.Custom.RtcEngine.PublishCameraTrack(false);
    }
}

订阅事件:

//视频发布状态变更事件
private void OnVideoPublishStateChangedEvent(EventArgs e)
{
    
    
    if (e is RtcEngineVideoPublishStateChangedEventArgs vpsce)
    {
    
    
        VideoPublishState = vpsce.newState;
        switch (vpsce.newState)
        {
    
    
            //视频流发布成功
            case STREAM_PUBLISH_STATE.PUB_STATE_PUBLISHED:
                if (Main.Custom.AvatarManager.LocalAvatarInstance != null)
                    Main.Custom.AvatarManager.LocalAvatarInstance.OnVideoPublishSuccessed(0, vpsce.channel);
                else
                    Main.Log.Error("Avatar人物实例为空,未能显示视频");
                break;
            //停止发布视频流
            case STREAM_PUBLISH_STATE.PUB_STATE_NO_PUBLISHED:
                if (Main.Custom.AvatarManager.LocalAvatarInstance != null)
                    Main.Custom.AvatarManager.LocalAvatarInstance.OnVideoPublishStopped();
                else
                    Main.Log.Info("Avatar人物实例为空");
                break;
            default: break;
        }
    }
}

视频流发布

猜你喜欢

转载自blog.csdn.net/qq_42139931/article/details/131259195