SellyCloudSDK_Android_demo/README.md

644 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
```
在 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
<string name="signaling_app_id">your-app-id</string>
<string name="signaling_secret">your-secret</string>
<string name="signaling_token"></string> <!-- 可选:直接写死 token -->
```
> 生产环境建议:
> 不要在 App 里写 secret而是在你们自己的业务服务器上生成 TokenApp 只向服务器请求 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<String, SurfaceViewRenderer>()
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)
```