From 55b6ac9fbe166abeb2438b749abf80bf33af5496 Mon Sep 17 00:00:00 2001 From: shou Date: Sun, 1 Mar 2026 07:53:46 +0000 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20README.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 644 +----------------------------------------------------- 1 file changed, 1 insertion(+), 643 deletions(-) diff --git a/README.md b/README.md index 791f007..df9fe0a 100644 --- a/README.md +++ b/README.md @@ -1,643 +1 @@ -SellyCloudSDK - -SellyRTC Android SDK 接入文档 - -本文档介绍如何在 Android 中使用 SellyRTC 快速集成一对一或多人音视频通话功能,包括: -- 基本接入 -- 音视频控制 -- 数据处理(如美颜) -- 事件回调 -- 通话统计 -- Token 生成与更新机制 - ---- - -## 目录 -1. 准备工作 -2. 快速开始 - - 创建引擎 - - 设置本地/远端画面 - - 配置视频参数 - - 加入频道 - - 结束通话 -3. 常用功能 - - 开关本地音视频 - - 切换摄像头 - - 静音远端音视频 - - 音频输出控制(扬声器 / 听筒) - - 发送自定义消息 - - 美颜开关 -4. 视频帧处理(美颜等) -5. 事件回调 (InteractiveRtcEngineEventHandler) -6. 通话统计 -7. Token 过期机制 -8. 常见问题 - ---- - -# 1. 准备工作 - -## 1.1 集成 SellyCloudSDK - -或如果目前是通过本地 AAR 集成(demo 方式): - -```gradle -dependencies { - implementation files("libs/sellycloudsdk-release.aar") -} -``` - -> 注意:如果你的业务侧还依赖 WebRTC、ijkplayer、美颜等第三方库,请保持与 SDK Demo 中的依赖版本一致。 - -## 1.2 必要权限 - -在 `AndroidManifest.xml` 中声明音视频必需权限: - -```xml - - -``` - -在 Android 6.0+ 设备上,运行时还需要动态申请权限,示例见后文(Demo 中的 `requiredPermissions` + `ActivityResultContracts.RequestMultiplePermissions` 已经实现)。 - -## 1.3 获取 AppId / Secret / Token - -从 SellyCloud 控制台获取: - -- `signaling_app_id` -- `signaling_secret`(用于服务端生成 Token) -- 或直接配置一个测试用的 `signaling_token` - -在 Demo 中,这些值通常配置在 `res/values/strings.xml`: - -```xml -your-app-id -your-secret - -``` - -> 生产环境建议: -> 不要在 App 里写 secret,而是在你们自己的业务服务器上生成 Token,App 只向服务器请求 Token。 - ---- - -# 2. 快速开始 - -以下示例基于 Demo 中的 `InteractiveLiveActivity`,展示最小接入流程。 - -## 2.1 创建引擎 InteractiveRtcEngine - -在 `Activity` 中创建并配置 RTC 引擎: - -```kotlin -private var rtcEngine: InteractiveRtcEngine? = null -private var beautyRenderer: FURenderer? = null -private var fuFrameInterceptor: FuVideoFrameInterceptor? = null -@Volatile private var isFrontCamera = true -@Volatile private var beautyEnabled: Boolean = true - -private fun initRtcEngine() { - val appId = getString(R.string.signaling_app_id) - val token = getString(R.string.signaling_token).takeIf { it.isNotBlank() } - - // 可选:初始化美颜 - beautyRenderer = FURenderer(this).also { it.setup() } - fuFrameInterceptor = beautyRenderer?.let { - FuVideoFrameInterceptor(it).apply { - setFrontCamera(isFrontCamera) - setEnabled(beautyEnabled) - } - } - - rtcEngine = InteractiveRtcEngine.create( - InteractiveRtcEngineConfig( - context = applicationContext, - appId = appId, - defaultToken = token - ) - ).apply { - // 设置回调 - setEventHandler(rtcEventHandler) - - // 角色:主播/观众,Demo 里默认主播(BROADCASTER) - setClientRole(InteractiveRtcEngine.ClientRole.BROADCASTER) - - // 配置视频参数(可选,见下一节) - setVideoEncoderConfiguration( - InteractiveVideoEncoderConfig( - width = 640, - height = 480, - fps = 20, - minBitrateKbps = 150, - maxBitrateKbps = 350 - ) - ) - - // 默认走扬声器 - setDefaultAudioRoutetoSpeakerphone(true) - - // 视频采集前拦截(用于美颜等) - setCaptureVideoFrameInterceptor { frame -> - if (!beautyEnabled) return@setCaptureVideoFrameInterceptor frame - fuFrameInterceptor?.process(frame) ?: frame - } - } -} -``` - -生命周期注意: -在 `onDestroy` 中记得 `leaveChannel()` 并销毁引擎,避免内存泄漏: - -```kotlin -override fun onDestroy() { - super.onDestroy() - rtcEngine?.setCaptureVideoFrameInterceptor(null) - leaveChannel() - InteractiveRtcEngine.destroy(rtcEngine) - rtcEngine = null - // 释放 renderer / 美颜资源... -} -``` - -## 2.2 设置本地 & 远端画面 - -SellyRTC 使用 `InteractiveVideoCanvas + SurfaceViewRenderer` 来承载视频画面。 - -### 初始化本地与远端渲染 View - -```kotlin -private var localRenderer: SurfaceViewRenderer? = null -private val remoteRendererMap = mutableMapOf() - -private fun createRenderer(): SurfaceViewRenderer = - SurfaceViewRenderer(this).apply { - setZOrderMediaOverlay(false) - } - -private fun setupVideoSlots() { - // 本地 slot - if (localRenderer == null) { - localRenderer = createRenderer() - } - localRenderer?.let { renderer -> - // Demo 中使用自定义的 VideoReportLayout 来承载 - binding.flLocal.attachRenderer(renderer) - } - - // 远端 slot 见 Demo 中的 remoteSlots / ensureRemoteRenderer -} -``` - -### 绑定本地视频 - -在加入频道前/时,设置本地视频 canvas: - -```kotlin -val renderer = localRenderer ?: createRenderer().also { localRenderer = it } -rtcEngine?.setupLocalVideo(InteractiveVideoCanvas(renderer, localUserId)) -``` - -### 绑定远端视频 - -在 `onUserJoined` 或业务逻辑中,为某个 `userId` 分配一个远端窗口: - -```kotlin -private fun ensureRemoteRenderer(userId: String): SurfaceViewRenderer { - return remoteRendererMap[userId] ?: createRenderer().also { renderer -> - remoteRendererMap[userId] = renderer - rtcEngine?.setupRemoteVideo(InteractiveVideoCanvas(renderer, userId)) - } -} -``` - -> 多人会议:为不同的 `userId` 分配不同的 View / slot,即可实现多路画面显示。 - -## 2.3 配置视频参数(可选) - -视频编码参数需要在加入频道前配置: - -```kotlin -rtcEngine?.setVideoEncoderConfiguration( - InteractiveVideoEncoderConfig( - width = 640, - height = 480, - fps = 20, - minBitrateKbps = 150, - maxBitrateKbps = 350 - ) -) -// 不设置则使用 SDK 默认配置 -``` - -## 2.4 加入频道 / 发起通话 - -### 1)准备 CallType 等入会参数 - -```kotlin -val options = InteractiveChannelMediaOptions( - callType = if (isP2P) CallType.ONE_TO_ONE else CallType.GROUP -) -``` - -其中: - -- `CallType.ONE_TO_ONE`:一对一视频通话 -- `CallType.GROUP`:多人会议 / 互动直播 - -### 2)生成 Token - -Demo 中的策略(简化): - -```kotlin -private val defaultTokenTtlSeconds = InteractiveCallConfig.DEFAULT_TOKEN_TTL_SECONDS - -private fun buildToken(appId: String, callId: String, userId: String): TokenBundle? { - val manualToken = getString(R.string.signaling_token).takeIf { it.isNotBlank() } - if (manualToken != null) { - return TokenBundle( - token = manualToken, - expiresAtSec = parseExprTime(manualToken), - secret = null - ) - } - - val secret = getString(R.string.signaling_secret) - if (secret.isBlank()) { - Toast.makeText( - this, - "请在 strings.xml 配置 signaling_secret 用于生成 token,或直接填写 signaling_token", - Toast.LENGTH_LONG - ).show() - return null - } - - return try { - val generated = TokenGenerator.generate( - appId = appId, - userId = userId, - callId = callId, - secret = secret, - ttlSeconds = defaultTokenTtlSeconds - ) - TokenBundle( - token = generated.token, - expiresAtSec = generated.expiresAtSec, - secret = secret - ) - } catch (t: Throwable) { - Toast.makeText(this, "生成 token 失败: ${t.message}", Toast.LENGTH_LONG).show() - null - } -} -``` - -> 生产环境建议: -> 将 `TokenGenerator` 放在你的业务服务器,客户端只请求业务服务器获取 Token。 - -### 3)调用 joinChannel - -```kotlin -rtcEngine?.joinChannel( - token = request.token, - callId = request.callId, - userId = request.userId, - options = request.options, // CallType 等 - tokenSecret = request.tokenSecret, // 可为空 - tokenExpiresAtSec = request.tokenExpiresAtSec, - tokenTtlSeconds = request.tokenTtlSeconds -) -``` - -成功后,会回调: - -```kotlin -override fun onJoinChannelSuccess(channel: String, userId: String, code: Int) { - // 已成功加入频道,可更新 UI 状态 -} -``` - -## 2.5 结束通话 - -业务结束通话时调用: - -```kotlin -private fun leaveChannel() { - rtcEngine?.leaveChannel() - resetUiAfterLeave() // 清 UI、清理 renderer 等 -} -``` - -SDK 会通过: - -```kotlin -override fun onLeaveChannel(durationSeconds: Int) { - // 通话结束时长(秒) -} -``` - -通知已经离开频道。 - ---- - -# 3. 常用功能 - -以下示例同样来自 Demo,可直接复用。 - -## 3.1 开/关本地视频 - -```kotlin -private var isLocalVideoEnabled = true -private var isLocalPreviewEnabled = true - -binding.btnToggleCamera.setOnClickListener { - isLocalVideoEnabled = !isLocalVideoEnabled - rtcEngine?.enableLocalVideo(isLocalVideoEnabled) - isLocalPreviewEnabled = isLocalVideoEnabled - updateControlButtons() -} -``` - -## 3.2 开/关本地音频采集 - -```kotlin -private var isLocalAudioEnabled = true - -binding.btnToggleMic.setOnClickListener { - isLocalAudioEnabled = !isLocalAudioEnabled - rtcEngine?.enableLocalAudio(isLocalAudioEnabled) - updateControlButtons() -} -``` - -## 3.3 切换前后摄像头 - -```kotlin -binding.btnSwitchCamera.setOnClickListener { - isFrontCamera = !isFrontCamera - fuFrameInterceptor?.setFrontCamera(isFrontCamera) - rtcEngine?.switchCamera() -} -``` - -## 3.4 静音远端音视频 - -按用户静音远端音频 / 视频: - -```kotlin -private fun muteRemoteUserAudio(targetUserId: String, muted: Boolean) { - rtcEngine?.muteRemoteAudioStream(targetUserId, muted) -} - -private fun muteRemoteUserVideo(targetUserId: String, muted: Boolean) { - rtcEngine?.muteRemoteVideoStream(targetUserId, muted) -} -``` - -## 3.5 控制音频输出(扬声器 / 听筒) - -```kotlin -private var isSpeakerOn = true - -binding.btnToggleAudioRoute.setOnClickListener { - isSpeakerOn = !isSpeakerOn - rtcEngine?.setDefaultAudioRoutetoSpeakerphone(isSpeakerOn) - updateControlButtons() -} -``` - -## 3.6 发送自定义消息 - -```kotlin -binding.btnSendMessage.setOnClickListener { - val text = binding.etMessage.text?.toString()?.trim().orEmpty() - if (text.isEmpty()) { - Toast.makeText(this, "请输入消息内容", Toast.LENGTH_SHORT).show() - } else if (currentCallId == null) { - Toast.makeText(this, "请先加入频道", Toast.LENGTH_SHORT).show() - } else { - rtcEngine?.sendMessage(text) { error -> - runOnUiThread { - if (error != null) { - Toast.makeText(this, "发送失败: ${error.message}", Toast.LENGTH_SHORT).show() - } else { - Toast.makeText(this, "已发送", Toast.LENGTH_SHORT).show() - binding.etMessage.text?.clear() - binding.tvMessageLog.text = "我: $text" - } - } - } - } -} -``` - -收到消息的回调见后文 `onMessageReceived`。 - -## 3.7 美颜开关 - -```kotlin -binding.btnToggleBeauty.setOnClickListener { - beautyEnabled = !beautyEnabled - fuFrameInterceptor?.setEnabled(beautyEnabled) - updateControlButtons() -} -``` - ---- - -# 4. 视频帧处理(美颜等) - -SellyRTC 提供视频采集前拦截接口,可以在推流前做美颜、滤镜等处理。 - -在创建引擎后设置: - -```kotlin -rtcEngine?.setCaptureVideoFrameInterceptor { frame -> - if (!beautyEnabled) return@setCaptureVideoFrameInterceptor frame - fuFrameInterceptor?.process(frame) ?: frame -} -``` - -其中 `FuVideoFrameInterceptor` 内部使用 `FURenderer` 做实际美颜处理。 - -> 你也可以替换为自己的处理逻辑: -> - 对 `frame` 做 GPU 或 CPU 处理 -> - 返回处理后的帧给 SDK 继续编码和发送 - ---- - -# 5. 事件回调 (InteractiveRtcEngineEventHandler) - -实现 `InteractiveRtcEngineEventHandler`,监听通话过程中发生的事件: - -```kotlin -private val rtcEventHandler = object : InteractiveRtcEngineEventHandler { - - override fun onJoinChannelSuccess(channel: String, userId: String, code: Int) { ... } - - override fun onLeaveChannel(durationSeconds: Int) { ... } - - override fun onUserJoined(userId: String, code: Int) { ... } - - override fun onUserLeave(userId: String, code: Int) { ... } - - override fun onConnectionStateChanged( - state: InteractiveConnectionState, - reason: Int, - userId: String? - ) { ... } - - override fun onError(code: String, message: String) { ... } - - override fun onLocalVideoStats(stats: InteractiveStreamStats) { ... } - - override fun onRemoteVideoStats(stats: InteractiveStreamStats) { ... } - - override fun onMessageReceived(message: String, userId: String?) { ... } - - override fun onTokenWillExpire(token: String?, expiresAt: Long) { ... } - - override fun onTokenExpired(token: String?, expiresAt: Long) { ... } - - override fun onDuration(durationSeconds: Long) { ... } - - override fun onRemoteVideoEnabled(enabled: Boolean, userId: String?) { ... } - - override fun onRemoteAudioEnabled(enabled: Boolean, userId: String?) { ... } - - override fun onStreamStateChanged( - peerId: String, - state: RemoteState, - code: Int, - message: String? - ) { ... } -} -``` - -**常见事件说明:** - -- `onConnectionStateChanged`:连接状态变化(Disconnected / Connecting / Connected / Reconnecting / Failed) -- `onUserJoined` / `onUserLeave`:远端用户加入/离开频道 -- `onRemoteVideoEnabled` / `onRemoteAudioEnabled`:远端用户开关音视频 -- `onMessageReceived`:收到自定义消息 -- `onDuration`:通话时长更新(秒) -- `onError`:错误回调(建议弹窗 + 打日志) - ---- - -# 6. 通话统计信息 - -## 6.1 单路流统计:InteractiveStreamStats - -在本地/远端视频统计回调中获取: - -```kotlin -override fun onLocalVideoStats(stats: InteractiveStreamStats) { - // stats.width / height / fps / videoBitrateKbps / audioBitrateKbps / rttMs 等 -} - -override fun onRemoteVideoStats(stats: InteractiveStreamStats) { - // 针对某个 userId 的码率、分辨率、丢包、RTT 等 -} -``` - -你可以将这些信息显示在 UI 上,Demo 中的 `buildStatsLabel` 已经示范了如何构造: - -```kotlin -private fun buildStatsLabel(header: String, stats: InteractiveStreamStats?): String { - // Res: WxH, FPS, Codec, Video/Audio Kbps, RTT 等 -} -``` - -## 6.2 通话结束时长:onLeaveChannel - -在 `onLeaveChannel` 中可以拿到本次通话时长(秒),无论是主动离开还是断网/失败结束,只要曾加入成功都会回调: - -```kotlin -override fun onLeaveChannel(durationSeconds: Int) { - Log.d(TAG, "onLeaveChannel duration=${durationSeconds}s") -} -``` - ---- - -# 7. Token 过期机制 - -SDK 在 Token 生命周期内会通过事件提醒你续期: - -## 7.1 Token 即将过期 - -```kotlin -override fun onTokenWillExpire(token: String?, expiresAt: Long) { - Toast.makeText( - this@InteractiveLiveActivity, - "Token 即将过期,请及时续期", - Toast.LENGTH_LONG - ).show() - // 1. 通知业务服务器刷新 Token - // 2. 拿到新 Token 后调用 rtcEngine?.renewToken(newToken)(具体接口以实际 SDK 为准) -} -``` - -## 7.2 Token 已过期 - -```kotlin -override fun onTokenExpired(token: String?, expiresAt: Long) { - Toast.makeText( - this@InteractiveLiveActivity, - "Token 已过期,断线后将无法重连", - Toast.LENGTH_LONG - ).show() -} -``` - -> 说明: -> - Token 过期后,**当前通话不会立刻中断**,但网络异常时自动重连会失败。 -> - 请务必在 `onTokenWillExpire` 阶段就完成续期。 - ---- - -# 8. 常见问题 (FAQ) - -## Q1:多人远端画面如何渲染? - -为每一个远端用户(`userId`)分配一个 `SurfaceViewRenderer`,并调用: - -```kotlin -val canvas = InteractiveVideoCanvas(renderer, userId) -rtcEngine?.setupRemoteVideo(canvas) -``` - -在布局层面,你可以将多个 `renderer` 放到不同的容器中(网格布局 / 自定义九宫格等),参考 Demo 中的 `remoteSlots`。 - ---- - -## Q2:远端画面不显示怎么办? - -排查方向: - -1. 是否收到了 `onUserJoined` 回调? -2. 有没有为该 `userId` 调用 `setupRemoteVideo` 并绑定到一个可见的 View? -3. View 是否被其他控件覆盖? -4. 远端用户是否已开启视频(可监听 `onRemoteVideoEnabled` 回调)? - ---- - -## Q3:如何实现画中画 / 小窗布局? - -这是布局层面的工作,与 SDK 解耦: - -- 将远端大画面放在父容器(如 `FrameLayout`)中 -- 再将本地小窗 View 作为子 View 添加在右下角,并设置合适的 `layoutParams` -- SDK 会把视频渲染到对应的 View 上,你只需要控制 View 的大小和位置即可 - ---- - -## Q4:如何在后台保持通话? - -Demo 中使用了一个前台 Service: - -```kotlin -InteractiveForegroundService.start(this) -// 离开频道后记得 stop -InteractiveForegroundService.stop(this) -``` +文档请参考doc目录下 \ No newline at end of file