Android使用声网SDK实现音视频互动(RTC)功能

发布于:2025-04-08 ⋅ 阅读:(33) ⋅ 点赞:(0)

一、前期准备

1、注册声网账号

声网官网

2、创建项目

拿到AppID,主要证书

二、代码部分

先上一下官方提供的demo地址:

Agora-RTC-QuickStart: 此仓库包含 Agora RTC Native SDK 的QuickStart示例项目。 - Gitee.comhttps://gitee.com/agoraio-community/Agora-RTC-QuickStart/tree/main/Android/Agora-RTC-QuickStart-Android可以在声网的帮助文档中看下图的教程很详细,或者无脑跑上面的demo,只需要填入声网控制台上获取到的appid,证书,和生成的临时token,以及生成临时token时填入的渠道号,但是控制台生成的临时token只有一天的有效期,下面会给出服务端生成临时token的代码,自己部署到服务器上,用客户端去调用接口

服务端:

提供一个获取token的接口

//还没要到代码,后续会补充上来,或者自行去帮助文档中查看,注意是rtc_token

客户端:

1、配置仓库

在settings.gradle中配置,主要是配置镜像

pluginManagement {
    repositories {

        maven { url "https://maven.aliyun.com/repository/public" }
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        maven { url "https://maven.aliyun.com/repository/public" }
        google()
        mavenCentral()
    }
}

rootProject.name = "你的项目名称"
include ':app'

2、导入声网的sdk

在app模块下的build.gradle的dependencies中加入下面这行,注意下面这个是轻量级的库,详细的库在声网自行搜索

 implementation 'io.agora.rtc:lite-sdk:4.5.1' //替换为最新的

 3、添加防混淆规则

在app模块下的proguard-rules.pro文件中加入下面代码

-keep class io.agora.**{*;}
-dontwarn io.agora.**

4、 静态声明权限

在AndroidManifest.XML文件中声明如下权限

  <uses-feature
        android:name="android.hardware.camera"
        android:required="true" />

    <!--必要权限-->
    <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"/>

5、具体代码部分

注:如果需要两个客户端互相传输音视频的话,直接用上面给的官方demo的代码就行,下面介绍的是对官方代码的一些封装,可以满足单西向传输,动态申请权限,调用临时token的接口,token本地存储及校验和不同页面的使用方法

(1)创建bean对象:
import java.io.Serializable

/*
* name :相机的名称
* channelName :频道名称 必须唯一
* uid :用户唯一id
* token : 临时token
* lastPostTime :上次成功获取到数据的时间
*/
data class Camera(
    val name: String,
    val channelName: String = "",
    var uid:Int = 0,
    var token: String? = null,
    var lastPostTime: Long = 0
) : Serializable
(2)token的本地存储工具类
object SpUtil {
    private val context = App.app!!

    val sharedPreferences: SharedPreferences =
        context.getSharedPreferences("camera", Context.MODE_PRIVATE)

    //获取绑定的摄像头列表
    fun getCameraData(): List<Camera> {
        val listStr = sharedPreferences.getString("list", "")
        if (listStr == "") {
            return listOf<Camera>()
        } else {
            val typeToken = object : TypeToken<List<Camera>>() {}.type
            return Gson().fromJson(listStr, typeToken)
        }
    }
    //保存绑定的摄像头列表
    fun saveCameraListData(list: List<Camera>) {
        sharedPreferences.edit().apply {
            putString("list", Gson().toJson(list))
            apply()
        }
    }
    //更新绑定的摄像头列表
    fun updateCameraData(channelName: String, token: String, uid: Int) {
        val list = getCameraData().toMutableList()
        val localCameraList = list.filter { it.channelName == channelName }

        if (localCameraList.isNotEmpty()) {
            val localCamera = localCameraList.first()
            localCameraList.forEach {
                list.remove(it)
            }
            localCamera.token = token
            localCamera.lastPostTime = System.currentTimeMillis()
            localCamera.uid = uid
            list.add(localCamera)
        }
        saveCameraListData(list)
    }

    //检查绑定摄像头的token是否过期
    fun checkToken(camera: Camera): Boolean {
        val list = getCameraData()
        val localCameraList = list.filter { it.channelName == camera.channelName }
        if (localCameraList.isNotEmpty()) {
            val localCamera = localCameraList.first()
            // 判断token是否过期
            val checkTime = System.currentTimeMillis() - localCamera.lastPostTime <  43200000
            if (localCamera.token != null && checkTime) {
                return true
            }
        }
        return false
    }

    //获取本地摄像头的数据
    fun getLocalCameraData(): Camera {
        val str = sharedPreferences.getString("localCamera", "")
        if (str == "") {
            val localCamera = Camera("本机", PlatformApp.getInstance().oaid)
            saveLocalCameraData(localCamera)
            return localCamera
        } else {
            val typeToken = object : TypeToken<Camera>() {}.type
            return Gson().fromJson(str, typeToken)
        }
    }

    //保存本地摄像头的数据
    fun saveLocalCameraData(camera: Camera) {
        sharedPreferences.edit().apply {
            putString("localCamera", Gson().toJson(camera))
            apply()
        }
    }

    //检查本地摄像头的token是否过期
    fun checkLocalCameraData(): Boolean {
        val localCamera = getLocalCameraData()
        // 判断token是否过期
        val checkTime = System.currentTimeMillis() - localCamera.lastPostTime < 43200000
        return localCamera.token != null && checkTime
    }
}
(3)RTC的管理类

import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import android.view.SurfaceView
import android.widget.FrameLayout
import androidx.core.content.ContextCompat
import com.kwad.sdk.utils.bt.runOnUiThread
import fczs.colorscol.rrjj.base.App
import fczs.colorscol.rrjj.beans.Camera
import io.agora.rtc2.ChannelMediaOptions
import io.agora.rtc2.Constants
import io.agora.rtc2.IRtcEngineEventHandler
import io.agora.rtc2.RtcEngine
import io.agora.rtc2.RtcEngineConfig
import io.agora.rtc2.video.VideoCanvas
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.Call
import okhttp3.Callback
import okhttp3.FormBody
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.json.JSONObject
import java.io.IOException
import kotlin.coroutines.resume

class RtcManger {

    private val baseContext = App.app!!

    // 填写项目的 App ID,可在声网控制台中生成
    private val appId = "你的AppId"
    
    //临时token
    var token = ""
    
    //uid每个渠道应保持唯一性,为0的话,sdk会自动分配一个,但如果临时token是自己服务器生成的,那就应该保持和服务给的一致,否则token鉴权不过,无法加入渠道
    var uid = 0

    //要加入的渠道
    var channelName = "";

    private var mRtcEngine: RtcEngine? = null

    //权限回调码
    val PERMISSION_REQ_ID: Int = 22

    //远程视频视图容器
    var remoteVideoViewContainer: FrameLayout? = null

    //本地视图容器
    var localVideoViewContainer: FrameLayout? = null

    private val mRtcEventHandler: IRtcEngineEventHandler = object : IRtcEngineEventHandler() {
        // 监听频道内的远端用户,获取用户的 uid 信息
        override fun onUserJoined(uid: Int, elapsed: Int) {
            runOnUiThread { // 获取 uid 后,设置远端视频视图
                setupRemoteVideo(uid)
            }
        }

        override fun onUserOffline(uid: Int, reason: Int) {
            super.onUserOffline(uid, reason)
            runOnUiThread {
                remoteVideoViewContainer?.removeAllViews()
            }
        }
    }

    fun init(camera: Camera) {
        this.channelName = camera.channelName
        this.token = camera.token ?: ""
        this.uid = camera.uid
    }

    /*
    * clientRoleType: 用户角色类型,默认为 Constants.BROADCASTER (主播) 发送方 ,还可以是Constants.CLIENT_ROLE_AUDIENCE(观众) 接收方
    * localVideoViewContainer: 本地视频视图容器
    * remoteVideoViewContainer: 远端视频视图容器
    */
    fun initializeAndJoinChannel(
        clientRoleType: Int = Constants.CLIENT_ROLE_BROADCASTER,
        localVideoViewContainer: FrameLayout? = null,
        remoteVideoViewContainer: FrameLayout? = null
    ) {
        this.remoteVideoViewContainer = remoteVideoViewContainer
        this.localVideoViewContainer = localVideoViewContainer
        try {
            // 创建 RtcEngineConfig 对象,并进行配置
            val config = RtcEngineConfig()
            config.mContext = baseContext
            config.mAppId = appId

            //添加远端视频视图handler
            if (remoteVideoViewContainer != null) {
                config.mEventHandler = mRtcEventHandler
            }
            // 创建并初始化 RtcEngine
            mRtcEngine = RtcEngine.create(config)

        } catch (e: Exception) {
            throw RuntimeException("Check the error.")
        }
        // 启用视频模块
        mRtcEngine!!.enableVideo()

        //本地视图显示
        if (localVideoViewContainer != null) {
            // 开启本地预览
            mRtcEngine!!.startPreview()
            // 创建一个 SurfaceView 对象,并将其作为 FrameLayout 的子对象
            val container = localVideoViewContainer
            val surfaceView = SurfaceView(baseContext)
            container.addView(surfaceView)
            // 将 SurfaceView 对象传入声网实时互动 SDK,设置本地视图
            mRtcEngine!!.setupLocalVideo(VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_FIT, 0))
        }

        // 创建 ChannelMediaOptions 对象,并进行配置
        val options = ChannelMediaOptions()
        // 根据场景将用户角色设置为 BROADCASTER (主播) 或 AUDIENCE (观众)
        options.clientRoleType = clientRoleType
        // 直播场景下,设置频道场景为 BROADCASTING (直播场景)
        options.channelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING

        // 使用临时 Token 加入频道,自行指定用户 ID 并确保其在频道内的唯一性
        mRtcEngine!!.joinChannel(token, channelName, uid, options)
    }


    // 获取体验实时音视频互动所需的录音、摄像头等权限
    fun getRequiredPermissions(): Array<String> {
        // 判断 targetSDKVersion 31 及以上时所需的权限
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            arrayOf(
                Manifest.permission.RECORD_AUDIO,  // 录音权限
                Manifest.permission.CAMERA,  // 摄像头权限
                Manifest.permission.READ_PHONE_STATE,  // 读取电话状态权限
                Manifest.permission.BLUETOOTH_CONNECT, // 蓝牙连接权限
            )
        } else {
            arrayOf(
                Manifest.permission.RECORD_AUDIO,
                Manifest.permission.CAMERA
            )
        }
    }

    private fun setupRemoteVideo(uid: Int) {
        if (remoteVideoViewContainer != null) {
            val container = remoteVideoViewContainer!!
            val surfaceView = SurfaceView(baseContext)
            surfaceView.setZOrderMediaOverlay(true)
            container.addView(surfaceView)
            // 将 SurfaceView 对象传入声网实时互动 SDK,设置远端视图
            mRtcEngine!!.setupRemoteVideo(
                VideoCanvas(
                    surfaceView,
                    VideoCanvas.RENDER_MODE_FIT,
                    uid
                )
            )
        }
    }

    fun checkPermissions(context: Context): Boolean {
        for (permission in getRequiredPermissions()) {
            val permissionCheck = ContextCompat.checkSelfPermission(context, permission)
            if (permissionCheck != PackageManager.PERMISSION_GRANTED) {
                return false
            }
        }
        return true
    }

    fun close() {
        // 停止本地视频预览
        mRtcEngine?.stopPreview()
        // 离开频道
        mRtcEngine?.leaveChannel()

        localVideoViewContainer?.removeAllViews()
    }

    //获取后台数据  channelName:渠道名称  expire: 获取到的临时token的有效时间  localCamera: 是否是本地摄像头(摄像)
    suspend fun requestData(channelName: String, localCamera: Boolean = false): String {
        return suspendCancellableCoroutine { continuation ->

            val fromBody = FormBody.Builder()
                .add("channelName",channelName)
                .add("uid","能保持唯一性的字符串,如设备的oaid")
                .build()
            val request = Request.Builder()
                .url("自己服务器接口的url")
                .post(fromBody)
                .build()

            // 发起异步请求
            OkHttpClient().newCall(request).enqueue(object : Callback {
                override fun onResponse(call: Call, response: Response) {
                    if (response.isSuccessful) {
                        try {
                            val responseBody = response.body?.string() ?: ""
                            val jsonObject = JSONObject(responseBody)
                            val code = jsonObject.getInt("code")
                            if (code == 200) {

                                var data = jsonObject.getJSONObject("data")
                                val token = data.getString("token")
                                val uid = data.getInt("uid")
                                this@RtcManger.channelName = channelName
                                this@RtcManger.token = token
                                this@RtcManger.uid = uid
                                if (localCamera) {
                                    //更新本机摄像头发送视图时的数据
                                    SpUtil.saveLocalCameraData(Camera("本机", channelName, uid, token, System.currentTimeMillis()))
                                } else {
                                    //更新本地存储的绑定摄像头的数据
                                    SpUtil.updateCameraData(channelName, token, uid)
                                }
                                continuation.resume("true")
                            } else {
                                continuation.resume("false")
                            }
                        } catch (e: Exception) {
                            // JSON 解析失败
                            continuation.resume("false")
                            Log.e("Request failed", "Json解析失败:" + e.message.toString())
                        }
                    }
                }

                override fun onFailure(call: Call, e: IOException) {
                    continuation.resume("false")
                    Log.e("Request failed", "请求失败:" + e.message.toString())
                }
            })
        }
    }
}
(4)发送音视频界面
界面:

确报有一个下面的布局就行

  <FrameLayout
        android:id="@+id/local_video_view_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
   />
代码:

在activity/fragment中声明如下变量和方法,示例中发送端是fragment,后面接收端是activity的示例代码

private val rtcManger = RtcManger()
private val localCamera by lazy { SpUtil.getLocalCameraData() }

//正常动态申请权限应该在activity的结果回调方法中写,但如果在onResume中执行判断就可以不用写结果回调方法
override fun onResume() {
        super.onResume()

        // 如果已经授权,则初始化 RtcEngine 并加入频道
        if (rtcManger.checkPermissions(requireContext())) {
            binding.permissionLL.container.visibility = View.GONE
            checkToken()
        } else {
            //显示无权限布局
            binding.permissionLL.apply {
                container.visibility = View.VISIBLE
                permissionBt.setOnClickListener {
                    ActivityCompat.requestPermissions(
                        requireActivity(),
                        rtcManger.getRequiredPermissions(),
                        rtcManger.PERMISSION_REQ_ID
                    )

                }
            }
        }
    }

private fun checkToken() {
        binding.permissionLL.container.visibility = View.GONE
        if (SpUtil.checkLocalCameraData()) {
            rtcManger.init(localCamera)
            start()
        } else {
             
            CoroutineScope(Dispatchers.Main).launch {
                connectDialog.show()
                val result = withContext(Dispatchers.IO) {
                    rtcManger.requestData(localCamera.channelName, true)
                }.toBoolean()
                connectDialog.dismiss()
                if (result) {
                    start()
                } else {
                    Toast.makeText(requireContext(), "服务器异常,请稍后重试", Toast.LENGTH_SHORT)
                        .show()
                }
            }
        }
    }

 private fun start() {
        rtcManger.initializeAndJoinChannel(
            Constants.CLIENT_ROLE_BROADCASTER,
            binding.localVideoViewContainer
        )
    }

    private fun stop() {
        rtcManger.close()
    }

    override fun onPause() {
        super.onPause()
        stop()
    }
 (5)接收音视频界面
界面:
    <FrameLayout
        android:id="@+id/remote_video_view_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
代码: 

和上面发送的大差不差,只是进入acticity时传递了要发送端的Camera对象,里面有渠道号,token等信息,这些发送端在拉token的时候已经通过SpUtil存到本地了,自己读取一下需要的

private lateinit var camera: Camera

public override fun onCreate(savedInstanceState: Bundle?) {

 camera = intent.getSerializableExtra("camera") as Camera

}

 override fun onResume() {
        super.onResume()
        // 如果已经授权,则初始化 RtcEngine 并加入频道
        if (rtcManger.checkPermissions(this)) {
            binding.permissionLL.container.visibility = View.GONE
            checkToken()
        } else {
            binding.permissionLL.apply {
                container.visibility = View.VISIBLE
                permissionBt.setOnClickListener {
                    ActivityCompat.requestPermissions(
                        this@CameraActivity,
                        rtcManger.getRequiredPermissions(),
                        rtcManger.PERMISSION_REQ_ID
                    )
                }
            }
        }
}

    @Deprecated("Deprecated in Java")
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<String?>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        // 系统权限申请回调
        if (rtcManger.checkPermissions(this)) {
            checkToken()
        }
    }

 private fun checkToken() {
        binding.permissionLL.container.visibility = View.GONE

        if (SpUtil.checkToken(camera)) {
            rtcManger.init(camera)
            start()
        } else {
           
            CoroutineScope(Dispatchers.Main).launch {
                connectDialog.show()
                val result = withContext(Dispatchers.IO) {
                    rtcManger.requestData(camera.channelName)
                }.toBoolean()
                if (result) {
                    connectDialog.dismiss()
                    start()
                } else {
                    connectDialog.showFailView {
                        finish()
                    }
                }
            }
        }
    }

  fun start() {
        rtcManger.initializeAndJoinChannel(
            Constants.CLIENT_ROLE_AUDIENCE,
            null,
            binding.remoteVideoViewContainer
        )
    }

    override fun onPause() {
        super.onPause()
        binding.remoteVideoViewContainer.removeAllViews() 
        rtcManger.close()
    }

ok,就是这样,总体来说是很简单的,发送端和接收端只是start方法不一样,其余的都差不多,希望上面的经验能帮到你