17 KiB
SellyCloudSDK
SellyRTC Android SDK 接入文档
本文档介绍如何在 Android 中使用 SellyRTC 快速集成一对一或多人音视频通话功能,包括:
- 基本接入
- 音视频控制
- 数据处理(如美颜)
- 事件回调
- 通话统计
- Token 生成与更新机制
目录
- 准备工作
- 快速开始
- 创建引擎
- 设置本地/远端画面
- 配置视频参数
- 加入频道
- 结束通话
- 常用功能
- 开关本地音视频
- 切换摄像头
- 静音远端音视频
- 音频输出控制(扬声器 / 听筒)
- 发送自定义消息
- 美颜开关
- 视频帧处理(美颜等)
- 事件回调 (InteractiveRtcEngineEventHandler)
- 通话统计
- Token 过期机制
- 常见问题
1. 准备工作
1.1 集成 SellyCloudSDK
或如果目前是通过本地 AAR 集成(demo 方式):
dependencies {
implementation files("libs/sellycloudsdk-release.aar")
}
注意:如果你的业务侧还依赖 WebRTC、ijkplayer、美颜等第三方库,请保持与 SDK Demo 中的依赖版本一致。
1.2 必要权限
在 AndroidManifest.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_idsignaling_secret(用于服务端生成 Token)- 或直接配置一个测试用的
signaling_token
在 Demo 中,这些值通常配置在 res/values/strings.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,而是在你们自己的业务服务器上生成 Token,App 只向服务器请求 Token。
2. 快速开始
以下示例基于 Demo 中的 InteractiveLiveActivity,展示最小接入流程。
2.1 创建引擎 InteractiveRtcEngine
在 Activity 中创建并配置 RTC 引擎:
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() 并销毁引擎,避免内存泄漏:
override fun onDestroy() {
super.onDestroy()
rtcEngine?.setCaptureVideoFrameInterceptor(null)
leaveChannel()
InteractiveRtcEngine.destroy(rtcEngine)
rtcEngine = null
// 释放 renderer / 美颜资源...
}
2.2 设置本地 & 远端画面
SellyRTC 使用 InteractiveVideoCanvas + SurfaceViewRenderer 来承载视频画面。
初始化本地与远端渲染 View
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:
val renderer = localRenderer ?: createRenderer().also { localRenderer = it }
rtcEngine?.setupLocalVideo(InteractiveVideoCanvas(renderer, localUserId))
绑定远端视频
在 onUserJoined 或业务逻辑中,为某个 userId 分配一个远端窗口:
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 配置视频参数(可选)
视频编码参数需要在加入频道前配置:
rtcEngine?.setVideoEncoderConfiguration(
InteractiveVideoEncoderConfig(
width = 640,
height = 480,
fps = 20,
minBitrateKbps = 150,
maxBitrateKbps = 350
)
)
// 不设置则使用 SDK 默认配置
2.4 加入频道 / 发起通话
1)准备 CallType 等入会参数
val options = InteractiveChannelMediaOptions(
callType = if (isP2P) CallType.ONE_TO_ONE else CallType.GROUP
)
其中:
CallType.ONE_TO_ONE:一对一视频通话CallType.GROUP:多人会议 / 互动直播
2)生成 Token
Demo 中的策略(简化):
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
rtcEngine?.joinChannel(
token = request.token,
callId = request.callId,
userId = request.userId,
options = request.options, // CallType 等
tokenSecret = request.tokenSecret, // 可为空
tokenExpiresAtSec = request.tokenExpiresAtSec,
tokenTtlSeconds = request.tokenTtlSeconds
)
成功后,会回调:
override fun onJoinChannelSuccess(channel: String, userId: String, code: Int) {
// 已成功加入频道,可更新 UI 状态
}
2.5 结束通话
业务结束通话时调用:
private fun leaveChannel() {
rtcEngine?.leaveChannel()
resetUiAfterLeave() // 清 UI、清理 renderer 等
}
SDK 会通过:
override fun onLeaveChannel(durationSeconds: Int) {
// 通话结束时长(秒)
}
通知已经离开频道。
3. 常用功能
以下示例同样来自 Demo,可直接复用。
3.1 开/关本地视频
private var isLocalVideoEnabled = true
private var isLocalPreviewEnabled = true
binding.btnToggleCamera.setOnClickListener {
isLocalVideoEnabled = !isLocalVideoEnabled
rtcEngine?.enableLocalVideo(isLocalVideoEnabled)
isLocalPreviewEnabled = isLocalVideoEnabled
updateControlButtons()
}
3.2 开/关本地音频采集
private var isLocalAudioEnabled = true
binding.btnToggleMic.setOnClickListener {
isLocalAudioEnabled = !isLocalAudioEnabled
rtcEngine?.enableLocalAudio(isLocalAudioEnabled)
updateControlButtons()
}
3.3 切换前后摄像头
binding.btnSwitchCamera.setOnClickListener {
isFrontCamera = !isFrontCamera
fuFrameInterceptor?.setFrontCamera(isFrontCamera)
rtcEngine?.switchCamera()
}
3.4 静音远端音视频
按用户静音远端音频 / 视频:
private fun muteRemoteUserAudio(targetUserId: String, muted: Boolean) {
rtcEngine?.muteRemoteAudioStream(targetUserId, muted)
}
private fun muteRemoteUserVideo(targetUserId: String, muted: Boolean) {
rtcEngine?.muteRemoteVideoStream(targetUserId, muted)
}
3.5 控制音频输出(扬声器 / 听筒)
private var isSpeakerOn = true
binding.btnToggleAudioRoute.setOnClickListener {
isSpeakerOn = !isSpeakerOn
rtcEngine?.setDefaultAudioRoutetoSpeakerphone(isSpeakerOn)
updateControlButtons()
}
3.6 发送自定义消息
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 美颜开关
binding.btnToggleBeauty.setOnClickListener {
beautyEnabled = !beautyEnabled
fuFrameInterceptor?.setEnabled(beautyEnabled)
updateControlButtons()
}
4. 视频帧处理(美颜等)
SellyRTC 提供视频采集前拦截接口,可以在推流前做美颜、滤镜等处理。
在创建引擎后设置:
rtcEngine?.setCaptureVideoFrameInterceptor { frame ->
if (!beautyEnabled) return@setCaptureVideoFrameInterceptor frame
fuFrameInterceptor?.process(frame) ?: frame
}
其中 FuVideoFrameInterceptor 内部使用 FURenderer 做实际美颜处理。
你也可以替换为自己的处理逻辑:
- 对
frame做 GPU 或 CPU 处理- 返回处理后的帧给 SDK 继续编码和发送
5. 事件回调 (InteractiveRtcEngineEventHandler)
实现 InteractiveRtcEngineEventHandler,监听通话过程中发生的事件:
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
在本地/远端视频统计回调中获取:
override fun onLocalVideoStats(stats: InteractiveStreamStats) {
// stats.width / height / fps / videoBitrateKbps / audioBitrateKbps / rttMs 等
}
override fun onRemoteVideoStats(stats: InteractiveStreamStats) {
// 针对某个 userId 的码率、分辨率、丢包、RTT 等
}
你可以将这些信息显示在 UI 上,Demo 中的 buildStatsLabel 已经示范了如何构造:
private fun buildStatsLabel(header: String, stats: InteractiveStreamStats?): String {
// Res: WxH, FPS, Codec, Video/Audio Kbps, RTT 等
}
6.2 通话结束时长:onLeaveChannel
在 onLeaveChannel 中可以拿到本次通话时长(秒),无论是主动离开还是断网/失败结束,只要曾加入成功都会回调:
override fun onLeaveChannel(durationSeconds: Int) {
Log.d(TAG, "onLeaveChannel duration=${durationSeconds}s")
}
7. Token 过期机制
SDK 在 Token 生命周期内会通过事件提醒你续期:
7.1 Token 即将过期
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 已过期
override fun onTokenExpired(token: String?, expiresAt: Long) {
Toast.makeText(
this@InteractiveLiveActivity,
"Token 已过期,断线后将无法重连",
Toast.LENGTH_LONG
).show()
}
说明:
- Token 过期后,当前通话不会立刻中断,但网络异常时自动重连会失败。
- 请务必在
onTokenWillExpire阶段就完成续期。
8. 常见问题 (FAQ)
Q1:多人远端画面如何渲染?
为每一个远端用户(userId)分配一个 SurfaceViewRenderer,并调用:
val canvas = InteractiveVideoCanvas(renderer, userId)
rtcEngine?.setupRemoteVideo(canvas)
在布局层面,你可以将多个 renderer 放到不同的容器中(网格布局 / 自定义九宫格等),参考 Demo 中的 remoteSlots。
Q2:远端画面不显示怎么办?
排查方向:
- 是否收到了
onUserJoined回调? - 有没有为该
userId调用setupRemoteVideo并绑定到一个可见的 View? - View 是否被其他控件覆盖?
- 远端用户是否已开启视频(可监听
onRemoteVideoEnabled回调)?
Q3:如何实现画中画 / 小窗布局?
这是布局层面的工作,与 SDK 解耦:
- 将远端大画面放在父容器(如
FrameLayout)中 - 再将本地小窗 View 作为子 View 添加在右下角,并设置合适的
layoutParams - SDK 会把视频渲染到对应的 View 上,你只需要控制 View 的大小和位置即可
Q4:如何在后台保持通话?
Demo 中使用了一个前台 Service:
InteractiveForegroundService.start(this)
// 离开频道后记得 stop
InteractiveForegroundService.stop(this)