Android使用声网SDK实现音视频互动(RTC)功能
一、前期准备
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文件中声明如下权限
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 { val listStr = sharedPreferences.getString(\"list\", \"\") if (listStr == \"\") { return listOf() } else { val typeToken = object : TypeToken<List>() {}.type return Gson().fromJson(listStr, typeToken) } } //保存绑定的摄像头列表 fun saveCameraListData(list: List) { 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() {}.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.Manifestimport android.content.Contextimport android.content.pm.PackageManagerimport android.os.Buildimport android.util.Logimport android.view.SurfaceViewimport android.widget.FrameLayoutimport androidx.core.content.ContextCompatimport com.kwad.sdk.utils.bt.runOnUiThreadimport fczs.colorscol.rrjj.base.Appimport fczs.colorscol.rrjj.beans.Cameraimport io.agora.rtc2.ChannelMediaOptionsimport io.agora.rtc2.Constantsimport io.agora.rtc2.IRtcEngineEventHandlerimport io.agora.rtc2.RtcEngineimport io.agora.rtc2.RtcEngineConfigimport io.agora.rtc2.video.VideoCanvasimport kotlinx.coroutines.suspendCancellableCoroutineimport okhttp3.Callimport okhttp3.Callbackimport okhttp3.FormBodyimport okhttp3.OkHttpClientimport okhttp3.Requestimport okhttp3.Responseimport org.json.JSONObjectimport java.io.IOExceptionimport kotlin.coroutines.resumeclass 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 { // 判断 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)发送音视频界面
界面:
确报有一个下面的布局就行
代码:
在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)接收音视频界面
界面:
代码:
和上面发送的大差不差,只是进入acticity时传递了要发送端的Camera对象,里面有渠道号,token等信息,这些发送端在拉token的时候已经通过SpUtil存到本地了,自己读取一下需要的
private lateinit var camera: Camerapublic 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, 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方法不一样,其余的都差不多,希望上面的经验能帮到你