• 直播相关——声网rtc SDK


    声网 SDK项目集成与api使用整理

    遥想约4年前,也自行调研过,虽然最终没有在实际项目中落地。 声网Android端集成与一对一音视频功能实现
    现在,终于要开始在项目中正式落地了,而声网也从原来的v3.x升级到了v4.x版本了。根据官网介绍,两大版本间改动还是比较大的。本次集成落地,会直接用v4.x版本。
    迁移指南

    demo

    示例项目 API-Examples
    跑通 API 示例项目

    生成token

    进来这个地址
    点击总览这里:点击:临时Token生成器生成。(24小时有效期)

    水晶球

    点击旁边的水晶球,进入水晶球页面,可以查看频道列表
    点击对应频道,可以进入其通话详情。可以看到对应用户的uid等信息

    集成

    发版说明
    /app/build.gradle

    //声网
        dependencies {
           ...
           // x.y.z 替换为具体的 SDK 版本号,如:4.0.0 或 4.1.0-1
           implementation 'io.agora.rtc:full-sdk:x.y.z'
        }
       //implementation 'io.agora.rtc:agora-special-full:4.1.1.26'
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这里的集成,可能会存在一些问题。
    比如:不同版本可能会有些api用不了;比如说旧版本有些功能有些bug,所以还是使用最新的推荐版本稳妥。

    混淆

    /app/proguard-rules.pro 文件

    #声网
    -keep class io.agora.**{*;}
    
    • 1
    • 2

    权限

    /app/src/main/AndroidManifest.xml

    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    tip:这里有个需要特别注意的点。如果当前项目中目标版本不是Android 12.0且不是集成 v4.1.0 以下 SDK 的设备。不能加入最后的三个权限。否则部分机型会有闪退问题。目前验证是有一部华为手机鸿蒙4.0的会闪退。
    参考链接:快速开始-实现音视频互动

    基本流程

    创建 RtcEngineConfig 对象,并进行配置。

    // 先初始化相关
    try {
        //创建 RtcEngineConfig 对象,并进行配置
        initRtcEngineConfig()
    } catch (e: Exception) {
        throw RuntimeException("Check the error.")
    }
    
    
    private fun initRtcEngineConfig() {
            // 创建 RtcEngineConfig 对象,并进行配置
            val config = RtcEngineConfig()
            config.mContext = baseContext
            config.mAppId = ""
            config.mEventHandler = mRtcEventHandler
            // 创建并初始化 RtcEngine
            mRtcEngine = RtcEngine.create(config)
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    启用视频模块

    private fun startVideo() {
       mRtcEngine?.apply {
                // 启用视频模块
                this.enableVideo()
                // 开启本地预览
                this.startPreview()
                // 创建一个 SurfaceView 对象,并将其作为 FrameLayout 的子对象
                val container: FrameLayout = findViewById(R.id.local_video_view_container)
                container.removeAllViews()
                container.addView(surfaceView)
                // 将 SurfaceView 对象传入声网实时互动 SDK,设置本地视图
                this.setupLocalVideo(VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_HIDDEN, 0))
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    获取token,并且处理token过期问题

     private val mRtcEventHandler: IRtcEngineEventHandler = object : IRtcEngineEventHandler() {
            //token监听
            override fun onTokenPrivilegeWillExpire(token: String?) {
                super.onTokenPrivilegeWillExpire(token)
                needUpdateToken = true
                mPresenter.getLiveToken()
            }
    
            override fun onRequestToken() {
                super.onRequestToken()
                needUpdateToken = true
                mPresenter.getLiveToken()
            }
    }
    //处理token过期问题
    if (needUpdateToken) {
        mRtcEngine?.renewToken(mPresenter.roomToken?.token.orEmpty())
        return
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    获取直播间配置参数

    其中,不同分辨率和帧率下适配的码率可以看这个文档:

    //获取直播间配置参数
    mRtcEngine?.queryDeviceScore()?.let {
        mPresenter.getLiveConfig(it)
    }
    override fun getLiveConfig() {
        mPresenter.liveRoomConfig?.definition_high?.let {
            setVideoEncoderConfiguration(it)
        }
    }
    private fun setVideoEncoderConfiguration(configuration: LiveRoomDefinitionMedium) {
        if (renderMode != configuration.render_mode) {
            mRtcEngine?.setupLocalVideo(VideoCanvas(surfaceView, configuration.render_mode, 0))
            renderMode = configuration.render_mode
        }
    
        videoEncoderConfiguration.bitrate = configuration.bitrate
        videoEncoderConfiguration.frameRate = configuration.frame_rate //帧率
        videoEncoderConfiguration.mirrorMode = configuration.getMirrorMode()
        videoEncoderConfiguration.dimensions = configuration.getDimensions() //分辨率
        videoEncoderConfiguration.orientationMode = configuration.getOrientationMode()//自适应模式
        val res = mRtcEngine?.setVideoEncoderConfiguration(videoEncoderConfiguration)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    加入频道并发布音视频

    如果是要为极速直播,则需要多设置:将 options 参数设置为 AUDIENCE_LATENCY_LEVEL_LOW_LATENCY(低延时)。

    //加入频道并发布音视频流
    private fun initChannelMediaOptions() {
        mPresenter.roomToken?.let {
            // 创建 ChannelMediaOptions 对象,并进行配置
            val options = ChannelMediaOptions()
            options.clientRoleType =  if (isFromControl) Constants.CLIENT_ROLE_AUDIENCE else Constants.CLIENT_ROLE_BROADCASTER
            options.channelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING
            val res = mRtcEngine?.joinChannel(
                it.token,
                liveRoomBean.room_id,
                if (isFromControl) it.audience_uid.toIntDefault else it.live_uid.toIntDefault,
                options
            )
    
            if(res == 0){
                isConnectionLost = false
                if(isFromControl.not()){
                    mPresenter.startPushStream()
                }
    
            }
        } ?: run {
            toast("直播地址为空!!")
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    观看端

    如果是设置观看端的话,则可以使用setupRemoteVideo。

    离开页面的时候,需要留意关闭预览

     mRtcEngine?.stopPreview()
    mRtcEngine?.leaveChannel()
    
    • 1
    • 2

    一些功能使用和踩坑记录

    处理截图问题

    需要留意的是,不能直接在takeSnapshot这里拿到路径就开始操作。而应该到回调onSnapshotTaken中处理。
    处理的过程需要留意耗时的操作要放到子线程去执行。

     private fun takeSnapshot(fileName: String = "${System.currentTimeMillis()}.jpg") {
        val uid = if(isFromControl) mPresenter.roomToken?.live_uid.toIntDefault else 0
        val filePath: String = SaveUtils.mkdir("live") +  File.separator + fileName
        val ret: Int? = mRtcEngine?.takeSnapshot(uid, filePath)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    tip:这里遇到一个声网的bug,在使用远端用户进行截图的时候,能够正常返回图片资源。但是如果使用主播端,则会发现有延迟问题。
    通过排查定位,发现这种延迟不是时间上的。而是会返回上一次截图的资源过来。目前已同步给声网,声网表示已跟进这个bug。
    但是,我们还需要解决。所以首先,这边是考虑自己实现截图。简易代码如下:

     fun takeScreen(
            view: View,
            path: String?,
        ){
            val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
            val canvas = Canvas(bitmap)
            view.draw(canvas)
            saveBitmap(bitmap, path)
        }
    
        private fun saveBitmap(
            bitmap: Bitmap,
            path: String? = null,
        ){
            try {
                val imageFile = File(path)
                val fos = FileOutputStream(imageFile)
                bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos)
                fos.flush()
                fos.close()
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    通过验证,SurfaceView这样子截图是会失败的,截取到数据是0size。于是改成传入其包裹的容器之后,验证发现SurfaceView部分是黑屏。
    当然也不能直接截全屏,因为不符合业务需求。声网官方示例中是通过SurfaceView来作为视图组件的。(后面通过验证,使用TextureView替代SurfaceView也是可以。)
    而SurfaceView 是用于绘制图形的视图组件,它通常不会保存绘制的内容,而是直接将内容显示在屏幕上。具体来说,SurfaceView 的绘制是由 SurfaceFlinger 系统服务管理的,
    它将 SurfaceView 的内容绘制到屏幕上的一个独立的 Surface 上。这个过程是在底层硬件加速的情况下进行的,绘制的内容并不会保存在普通的 Bitmap 中,
    因此无法直接通过传统的方法获取 SurfaceView 的截图。这边不过多对SurfaceView做详解。

    SurfaceView采用双缓存机制,SurfaceView在更新视图时用到了两张 Canvas,一张 frontCanvas 和一张 backCanvas ,每次实际显示的是 frontCanvas ,backCanvas 存储的是上一次更改前的视图。当你在播放这一帧的时候,它已经提前帮你加载好后面一帧了,所以播放起视频很流畅。当使用lockCanvas() 获取画布时,得到的实际上是backCanvas 而不是正在显示的 frontCanvas ,之后你在获取到的 backCanvas 上绘制新视图,
    再 unlockCanvasAndPost(canvas)此视图,那么上传的这张 canvas 将替换原来的 frontCanvas 作为新的frontCanvas ,原来的 frontCanvas 将切换到后台作为 backCanvas 。例如,如果你已经先后两次绘制了视图A和B,那么你再调用 lockCanvas() 获取视图,获得的将是A而不是正在显示的B,之后你将重绘的 A 视图上传,那么 A 将取代 B 作为新的 frontCanvas 显示在SurfaceView 上,原来的B则转换为backCanvas。相当与多个线程,交替解析和渲染每一帧视频数据
    引用 https://www.jianshu.com/p/a2a235bee59e
    普通View onDraw 内容是静态的,不调invalidate() 它是不会发生变化,你可以拿到里面的Bitmap;但是SurfaceView不同,无法拿到它back buffer里面的Bitmap。

    回到解决问题本身,既然没办法直接通过对SurfaceView截图。还是从声网的api入手。最后验证通过截图两次取第二次的方式可以解决该问题。
    但是还需要注意的是,不能简单粗暴直接调用两次api。否则api还是返回异常。而是在调用第一次之后,在onSnapshotTaken再判断处理调起第二次。

    设置清晰度

    在设置清晰度的时候,需要先得知当前设备的设备评分等级。使用的api是:queryDeviceScore。然后再根据分数,得到适合配置进行设置。
    在高清或超高清视频场景下,可以先调用该方法查询设备的等级评分。如果返回的评分较低(比如低于 60),则需要适当调低视频分辨率,以避免影响视频体验。

     private fun setVideoEncoderConfiguration(configuration: LiveRoomDefinitionMedium) {
        videoEncoderConfiguration.bitrate = configuration.bitrate
        videoEncoderConfiguration.frameRate = configuration.frame_rate //帧率
        videoEncoderConfiguration.mirrorMode = configuration.getMirrorMode()
        videoEncoderConfiguration.dimensions = configuration.getDimensions() //分辨率
        videoEncoderConfiguration.orientationMode = configuration.getOrientationMode()//自适应模式
        val res = mRtcEngine?.setVideoEncoderConfiguration(videoEncoderConfiguration)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    token失效问题

    不仅在加入频道的时候需要token,直播过程也会一直监测token的时效。因此还需要监听。声网提供了两个api:

    • onTokenPrivilegeWillExpire
    • onRequestToken。
      需要在监听到token过期或即将过期的时候,重新拿到新的token,并通过renewToken重新赋值。

    对焦问题

    声网提供了相关对焦的功能,包括人脸自动对焦和手动对焦功能。

    • isCameraFocusSupported 检测设备是否支持手动对焦功能
    • isCameraAutoFocusFaceModeSupported 检测设备是否支持人脸对焦功能
    • setCameraFocusPositionInPreview 设置手动对焦位置,并触发对焦
    • setCameraAutoFocusFaceModeEnabled 设置是否开启人脸对焦功能
      经过验证,发现人脸对焦在前置摄像的时候会检测不支持。因此联系声网,得到反馈是就算开启了人脸对焦,也会比较损耗性能。
      再加上经过多次验证发现,再切换镜头的时候立即调用也偶现失败。结合业务大多是开启前置,因此只接入手动对焦功能。
      而手动对焦功能,发现相比腾讯的手动对焦,声网的手动对焦感官体验上只会晃动一下,因此最好还是像腾讯的一样加多个动效。
      目前初步实现如下,感兴趣可以看看,也可以直接跳过这趴:
    surfaceView.setOnTouchListener { view, event ->
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                val x = event.x
                val y = event.y
                // 在这里处理点击事件,可以使用 x 和 y 坐标执行相应操作
                // 检测当前设备是否支持手动对焦并设置。
                if (isCameraFocusSupported) {
                    // 假设在屏幕(50,100)的位置对焦。
                   val res1 =  setCameraFocusPositionInPreview(x, y)
                   if(res1 == 0){
                       val borderAnimationView = BorderAnimationView(this@LivePlayerActivityV2)
                       local_video_view_container.addView(borderAnimationView)
    
                       borderAnimationView.showAnimation(x, y)
    
                       Handler(Looper.getMainLooper()).postDelayed({
                           local_video_view_container.removeView(borderAnimationView)
                       }, 500) //850毫秒后执行
                   }
                }
    
                true
            }
            else -> false
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    BorderAnimationView则是一个直播对焦边框动画View,这边处理的方式:通过绘制的方式画出对应区域内的一个白色边框。然后开启缩放动画效果,最后消失。
    不多说,初步代码如下:

    class BorderAnimationView(context: Context) : View(context) {
        private var x = 0f
        private var y = 0f
        private var scale = 1f
        private var paint = Paint().apply {
            color = Color.WHITE
            style = Paint.Style.STROKE
            strokeWidth = 3f
        }
    
        fun showAnimation(x: Float, y: Float) {
            this.x = x
            this.y = y
            scale = 1f
            invalidate() // 请求重绘
            startScaleAnimation()
        }
    
        private fun startScaleAnimation() {
            val scaleTo = 1.25f
            val scaleBack = 1f
    
            val scaleAnimation = ValueAnimator.ofFloat(scale, scaleTo)
            scaleAnimation.addUpdateListener { valueAnimator ->
                scale = valueAnimator.animatedValue as Float
                invalidate()
            }
            scaleAnimation.duration = 250 // 0.25秒
            scaleAnimation.interpolator = AccelerateDecelerateInterpolator()
    
            val scaleBackAnimation = ValueAnimator.ofFloat(scaleTo, scaleBack)
            scaleBackAnimation.addUpdateListener { valueAnimator ->
                scale = valueAnimator.animatedValue as Float
                invalidate()
            }
            scaleBackAnimation.duration = 350 // 0.35秒
            scaleBackAnimation.interpolator = AccelerateDecelerateInterpolator()
    
            scaleAnimation.addListener(object : AnimatorListenerAdapter() {
                override fun onAnimationEnd(animation: Animator?) {
                    scaleBackAnimation.start()
                }
            })
    
            scaleAnimation.start()
        }
    
        override fun onDraw(canvas: Canvas) {
            super.onDraw(canvas)
            val scaledSize = 80.dp * scale
            val halfScaledSize = scaledSize / 2
            canvas.drawRect(x - halfScaledSize, y - halfScaledSize, x + halfScaledSize, y + halfScaledSize, paint) // 绘制带有白色边框的矩形
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    网络监听

    直播过程避免网络波动,因此需要监听网络做出必要的交互。声网提供的api是onNetworkQuality。
    需要注意的是,要判断是主播端还是观看端。
    如果是主播端,则应该拿上行网络质量txQuality进行判断;如果是观看端,则应该用下行网络质量rxQuality。
    不足的地方是,只能拿到网络质量状态的枚举,而没能拿到具体网络速率等数值。

    其他

    • 美颜美白:这个主要是业务逻辑比较多,实际核心代码就是调用setBeautyEffectOptions进行设置。声网还支持了更多的美颜面板设置。
    mRtcEngine?.setBeautyEffectOptions(true, options)
    
    • 1
    • 告警通知服务:比如视频卡顿率在一定周期内连续大于某个阈值。告警通知
  • 相关阅读:
    【Tricks】关于如何防止edge浏览器偷取chrome浏览器的账号
    OSN 1800 I 增强型华为多业务光传送设备
    【CSS】CSS选择器汇总
    Day20_的笔记补充
    虹科分享 | 读不懂CANopen报文?看这篇文章就够了
    linux 输出重定向
    vscode 资源管理器移动到右边
    微服务框架 SpringCloud微服务架构 5 Nacos 5.5 服务实例的权重设置
    MySQL多表查询——子查询(临时表,all、any操作符)
    2022 华为 Java 高级面试题及答案
  • 原文地址:https://blog.csdn.net/wzj_what_why_how/article/details/136563881