直播相关——声网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'

这里的集成,可能会存在一些问题。

比如:不同版本可能会有些api用不了;比如说旧版本有些功能有些bug,所以还是使用最新的推荐版本稳妥。

混淆

/app/proguard-rules.pro 文件

复制代码
#声网
-keep class io.agora.**{*;}

权限

/app/src/main/AndroidManifest.xml

复制代码
<!--必要权限-->
<uses-permission android:name="android.permission.INTERNET"/>

<!--可选权限-->
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
<!-- 对于 Android 12.0 及以上且集成 v4.1.0 以下 SDK 的设备,还需要添加以下权限 -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<!-- 对于 Android 12.0 及以上设备,还需要添加以下权限 -->
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>

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)
    }

启用视频模块

复制代码
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))
   }
}

获取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
}

获取直播间配置参数

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

复制代码
//获取直播间配置参数
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)
}

加入频道并发布音视频流

如果是要为极速直播,则需要多设置:将 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("直播地址为空!!")
    }
}

观看端

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

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

复制代码
 mRtcEngine?.stopPreview()
mRtcEngine?.leaveChannel()

一些功能使用和踩坑记录

处理截图问题

需要留意的是,不能直接在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)
}

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()
        }
    }

通过验证,SurfaceView这样子截图是会失败的,截取到数据是0size。于是改成传入其包裹的容器之后,验证发现SurfaceView部分是黑屏。

当然也不能直接截全屏,因为不符合业务需求;也不能使用其他方式如TextureView替代SurfaceView。因为声网是通过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)
}

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
      }

    }

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) // 绘制带有白色边框的矩形
    }
}

网络监听

直播过程避免网络波动,因此需要监听网络做出必要的交互。声网提供的api是onNetworkQuality。

需要注意的是,要判断是主播端还是观看端。

如果是主播端,则应该拿上行网络质量txQuality进行判断;如果是观看端,则应该用下行网络质量rxQuality。

不足的地方是,只能拿到网络质量状态的枚举,而没能拿到具体网络速率等数值。

其他

  • 美颜美白:这个主要是业务逻辑比较多,实际核心代码就是调用setBeautyEffectOptions进行设置。声网还支持了更多的美颜面板设置。

    mRtcEngine?.setBeautyEffectOptions(true, options)

  • 告警通知服务:比如视频卡顿率在一定周期内连续大于某个阈值。告警通知

  • ...

相关推荐
雨白2 小时前
Android 快捷方式实战指南:静态、动态与固定快捷方式详解
android
hqk2 小时前
鸿蒙项目实战:手把手带你实现 WanAndroid 布局与交互
android·前端·harmonyos
LING3 小时前
RN容器启动优化实践
android·react native
恋猫de小郭5 小时前
Flutter 发布官方 Skills ,Flutter 在 AI 领域再添一助力
android·前端·flutter
Kapaseker10 小时前
一杯美式搞懂 Any、Unit、Nothing
android·kotlin
黄林晴10 小时前
你的 Android App 还没接 AI?Gemini API 接入全攻略
android
恋猫de小郭20 小时前
2026 Flutter VS React Native ,同时在 AI 时代 VS Native 开发,你没见过的版本
android·前端·flutter
冬奇Lab1 天前
PowerManagerService(上):电源状态与WakeLock管理
android·源码阅读
BoomHe1 天前
Now in Android 架构模式全面分析
android·android jetpack
二流小码农1 天前
鸿蒙开发:上传一张参考图片便可实现页面功能
android·ios·harmonyos