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