24 KiB
Selly Live SDK 推拉流接入文档(Android)
统一 SDK 名称:SellyCloudSDK 本文档适用于 Android 客户端,面向对外集成方与内部使用。
1. 概述
Selly Live SDK 提供完整的音视频直播能力,支持 推流(直播发布) 与 拉流(直播播放) 两大核心场景,适用于泛直播、互动直播、实时音视频等业务。
主要能力
- 支持 RTMP / RTC 推流与播放模式
- 支持 SurfaceView / TextureView 两套渲染后端
- 直播播放器与点播播放器支持 SurfaceTexture 高级渲染接入
- 高性能音视频采集与编码
- 灵活的视频参数配置(分辨率 / 帧率 / 码率)
- 推流状态与统计回调
- 拉流播放状态与错误回调
- 支持视频帧处理(美颜 / 滤镜 / 水印)
- 基于 Token 的安全鉴权机制
- 支持 RTMP Payload XOR 保护(可选)
- 支持 RTC(WHEP/WHIP)WebRTC Frame XOR 加解密(可选)
- 支持 外部代理地址注入(如洋葱盾等第三方安全代理)
2. 系统要求
- Android 8.0+(Demo
minSdk为 26) - 需真机运行(音视频采集限制)
- 需要摄像头 / 麦克风权限
3. SDK 集成
3.1 项目结构参考
example/:Android Demo 工程- 推流示例:
example/src/main/java/com/demo/SellyCloudSDK/live/LivePushActivity.kt - 拉流示例:
example/src/main/java/com/demo/SellyCloudSDK/live/LivePlayActivity.kt
- 推流示例:
example/libs/:本地 AAR 依赖存放目录
3.2 Gradle 依赖方式(仅 AAR)
dependencies {
implementation files("libs/sellycloudsdk-1.0.1.aar")
}
若接入美颜等能力,请按业务侧 SDK 要求额外引入第三方美颜库(Demo 使用 FaceUnity AAR)。
3.3 权限配置(AndroidManifest.xml)
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
如需截图保存且兼容 Android 9 及以下,可额外申请
WRITE_EXTERNAL_STORAGE。
4. SDK 初始化与代理配置
4.1 SDK 初始化
在使用任何推流 / 拉流功能前,必须先初始化 SDK:
SellyCloudManager.initialize(
context = applicationContext,
appId = "your-app-id",
config = SellyCloudConfig(
vhost = "your-vhost",
vhostKey = "your-vhost-key",
defaultStreamId = "default-stream",
defaultLiveMode = SellyLiveMode.RTMP
)
)
initialize 参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
context |
Context | 应用上下文 |
appId |
String | 应用 ID(权威值,会覆盖 config 中的 appId) |
config |
SellyCloudConfig? | 可选配置,不传则使用默认值 |
SellyCloudConfig 字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
vhost |
String | 虚拟主机 |
vhostKey |
String | vhost 密钥(用于鉴权签名) |
defaultStreamId |
String | 默认流 ID |
logEnabled |
Boolean | 是否启用日志,默认 true |
defaultLiveMode |
SellyLiveMode | 默认推拉流模式(RTMP / RTC) |
appName |
String | 应用名称,为空时自动使用 appId,一般无需设置 |
config.appId无需设置,SDK 内部会用initialize(appId=)参数覆盖。
4.2 代理地址配置(可选)
SDK 支持通过外部代理(如洋葱盾等安全加速服务)进行流媒体连接。代理地址由业务方在 SDK 外部获取,然后通过以下接口注入:
// 设置代理地址
SellyCloudManager.setProxyAddress("http://127.0.0.1:12345")
// 清除代理(恢复直连)
SellyCloudManager.setProxyAddress(null)
// 查询当前代理地址
val proxy = SellyCloudManager.getProxyAddress() // null 表示未设置
格式要求:
- 必须以
http://或https://开头 - 传
null或空字符串表示清除代理 - 格式不合法时抛出
IllegalArgumentException
生效范围:
- 设置后对 RTMP 推拉流、RTC(WHEP/WHIP)播放推流、Signaling 信令连接均生效
- SDK 内部通过代理地址解析真实服务器 IP,对上层透明
时机要求:
- 必须在推流 / 拉流 开始之前 设置
- 推流 / 拉流过程中修改代理地址,需停止后重新开始才能生效
Demo 中使用
KiwiHelper封装了洋葱盾 SDK 的初始化与代理地址获取流程,通过SellyCloudManager.setProxyAddress()将结果传给 SDK。详见example/src/main/java/com/demo/SellyCloudSDK/KiwiHelper.kt。
5. Token 鉴权机制(重点)
5.1 Token 注入方式
| 场景 | 设置位置 |
|---|---|
| 推流 | SellyLiveVideoPusher.token |
| 拉流 | SellyLiveVideoPlayer.token |
说明:
- Token 不拼接到 URL
- Token 不绑定 streamId
- SDK 内部在建立连接时自动携带当前 Token
- 直接使用 RTMP 地址推/拉流不需要 Token,可不设置
5.2 Token 设置时机(强约束)
推流
必须在以下接口调用 之前 设置:
startLiveWithStreamId(...)
拉流
必须在以下接口调用 之前 设置:
prepareToPlay()play()
在连接建立后修改 Token,不会影响当前连接。
5.3 Token 刷新机制说明
- SDK 不提供自动刷新
- 业务层可在任意时刻 重新设置 token 属性
推荐流程:
- 业务侧向服务端获取新 Token
- 调用
pusher.token = newToken/player.token = newToken - 停止并重新开始推流 / 拉流流程
5.4 RTMP / WebRTC XOR 保护(可选)
用途:
- 提高流地址泄露后被直接播放、转码或抓流的门槛
生效范围与约束:
- RTMP 推拉流:支持 payload XOR,当前仅支持 H264 + AAC
- RTC(WHEP/WHIP) 推拉流:支持 WebRTC frame XOR 加解密
- 当前这里的 WebRTC 指直播 RTC 推拉流,不包含互动通话高层 API
- RTMP 只处理 payload,配置帧(SPS/PPS、AAC Sequence Header)保持不变
- 推流端与播放端必须使用同一个 key
Key 格式:
hex字符串,建议 16 或 32 字节(即 32/64 个 hex 字符)- 支持
0x前缀 - 长度必须为偶数
- 非法 key 会直接抛出
IllegalArgumentException,不会静默降级
时机要求:
- 推流:请在
startLiveWithStreamId(...)/startLiveWithUrl(...)之前调用setXorKey(...) - 拉流:请在
initWithStreamId(...)/initWithUrl(...)创建播放器时传入xorKeyHex - 运行中修改 key 不会影响当前连接,需重启推流或重建播放器实例
6. 推流接入详解
6.1 创建推流实例
val pusher = SellyLiveVideoPusher.initWithLiveMode(
context = this,
liveMode = SellyLiveMode.RTMP
)
pusher.delegate = object : SellyLiveVideoPusherDelegate {
override fun liveStatusDidChanged(status: SellyLiveStatus) {}
override fun onStatisticsUpdate(stats: SellyLivePusherStats) {}
override fun onError(error: SellyLiveError) {}
}
6.2 视频参数配置与预览
val config = SellyLiveVideoConfiguration.defaultConfiguration().apply {
videoSize = SellyLiveVideoResolution.RES_1280x720
videoFrameRate = 25
videoBitRate = 1000 * 1000
videoMinBitRate = 500 * 1000
outputImageOrientation = SellyLiveOrientation.PORTRAIT
}
pusher.attachPreview(previewContainer, useTextureView = false)
pusher.startRunning(
cameraPosition = SellyLiveCameraPosition.FRONT,
videoConfig = config,
audioConfig = null
)
6.2.1 预览后端选择
推流预览支持两种接入方式:
attachPreview(container, useTextureView = false):SDK 创建预览 View,默认走旧的Surface/OpenGL预览链路attachPreview(container, useTextureView = true):SDK 创建TextureView预览,适合需要普通 View 层级混排的场景setPreviewView(view):手动传入预览 ViewsetPreviewView(view, mode):当传入TextureView时,建议使用这个显式协议版本
示例:
// 默认旧路径
pusher.attachPreview(previewContainer, useTextureView = false)
// TextureView 路径
pusher.attachPreview(previewContainer, useTextureView = true)
// 手动指定 TextureView 时,建议显式传入 liveMode
val textureView = com.sellycloud.sellycloudsdk.widget.AspectRatioTextureView(this)
pusher.setPreviewView(textureView, SellyLiveMode.RTMP)
说明:
RTMP模式下,SDK 内部会根据预览 View 类型自动选择OpenGlView或TextureViewRTC/WHIP预览也支持TextureView- 当前版本建议在 开始采集/推流前 选定预览后端;不保证运行中热切换预览后端
6.3 设置推流 Token(使用 streamId 时)
pusher.token = pushToken
推流 XOR(RTMP / RTC-WHIP,可选)
val xorKeyHex = "A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6"
// 建议在 startLiveWith... 之前设置
pusher.setXorKey(xorKeyHex)
setXorKey(...)同时作用于 RTMP 推流与 RTC/WHIP 推流。若在推流中修改 key,需停止并重新开始推流后才会使用新 key。
6.4 开始/停止推流
pusher.startLiveWithStreamId(streamId)
pusher.stopLive()
直接使用 RTMP 地址推流(Demo 未覆盖)
val rtmpUrl = "rtmp://your.host/live/streamKey"
pusher.startLiveWithUrl(rtmpUrl)
直接使用 RTMP 地址推流不需要 Token,可不设置。
停止推流(带回调)
pusher.stopLive { error ->
if (error != null) {
// 处理停止失败
}
}
6.5 常用控制接口
setMuted(true/false):静音switchCameraPosition(...):切换摄像头switchCamera():前后摄像头切换(自动切换)startCamera()/stopCamera():控制摄像头startMicrophone()/stopMicrophone():控制麦克风setCameraEnabled(true/false):关闭/开启摄像头setStreamOrientation(...):切换推流方向setVideoConfiguration(...)+changeResolution(...):动态调整分辨率setAutoFramingEnabled(...)/getAutoFramingCapability()/getAutoFramingState():自动取景setBeautyEngine(...)+setBeautyEnabled(...):接入美颜setBeautyLevel(level):设置美颜强度setBitmapAsVideoSource(...)/restoreCameraVideoSource():背景图推流
6.5.1 美颜引擎接入
当前版本推荐通过 BeautyEngine + VideoProcessor 接入美颜。Demo 使用 FaceUnityBeautyEngine,位于:
example/src/main/java/com/demo/SellyCloudSDK/beauty/FaceUnityBeautyEngine.kt
接入示例:
val beautyEngine = FaceUnityBeautyEngine()
pusher.setBeautyEngine(beautyEngine)
pusher.setBeautyEnabled(true)
pusher.setBeautyLevel(3.0f)
说明:
BeautyEngine.createProcessor()返回的是 SDK V2VideoProcessor- 当前 Demo 的美颜实现走
TEXTURE_2D + READ_WRITE - 美颜属于“完整重写输出”的场景,建议在
VideoProcessorConfig中设置fullRewrite = true RTC/WHIP路径优先推荐TEXTURE_2D,避免对 texture-backed 帧做额外的 texture-to-CPU 转换
6.5.2 推流前帧处理与观察
直播推流支持:
- 一个可写
VideoProcessor - 多个只读
VideoFrameObserver
只读观测示例:
val disposable = pusher.addVideoFrameObserver(object : VideoFrameObserver {
override val config = VideoFrameObserverConfig(
preferredFormat = VideoProcessFormat.TEXTURE_2D
)
override fun onTextureFrame(frame: VideoTextureFrame) {
// 只读观测,不修改输出
}
})
可写处理示例:
pusher.setVideoProcessor(object : VideoProcessor {
override val config = VideoProcessorConfig(
preferredFormat = VideoProcessFormat.TEXTURE_2D,
mode = VideoProcessMode.READ_WRITE
)
override fun processTexture(input: VideoTextureFrame, outputTextureId: Int) {
// 将滤镜/水印直接写入 SDK 提供的 outputTextureId
}
})
当前 SDK / Demo 的处理建议:
RTC/WHIP路径优先使用TEXTURE_2DRTMP在确实需要 CPU 像素时,可使用I420/RGBAREAD_WRITE模式下,SDK 会准备输出缓冲;只有“完整覆盖输出”的场景才建议fullRewrite = trueoutputTextureId由 SDK 管理,处理器不应转移所有权,也不应在回调里主动删除纹理VideoFrameObserverConfig的默认值仍为I420以兼容旧接入;新接入建议显式声明preferredFormat
Demo 中当前可直接验证的模式:
帧回调纹理:TEXTURE_2Dobserver帧回调空CPU:声明I420,不处理像素帧回调单CPU:单个I420observer帧回调双CPU:两个I420observer,共享同一次 CPU 转换改帧:RTC下走TEXTURE_2D,RTMP示例走RGBA
6.5.3 自动取景(Auto Framing)
当前高层 API 已暴露:
setAutoFramingEnabled(enabled):开启 / 关闭自动取景getAutoFramingCapability():查询当前是否支持及原因getAutoFramingState():读取当前状态delegate.onAutoFramingStateChanged(state):接收状态变化回调
状态枚举:
OFFINACTIVEFRAMINGCONVERGEDUNSUPPORTED
当前约束:
- 当前自动取景只在 RTMP 推流 路径可用
RTC / WHIP推流当前会返回UNSUPPORTED- 需要摄像头已启动后再查询 capability;相机关闭、背景图推流等场景也会返回不支持
示例:
val capability = pusher.getAutoFramingCapability()
if (capability.supported) {
pusher.setAutoFramingEnabled(true)
}
6.6 生命周期建议
在宿主 Activity 中对齐生命周期:
onResume()→pusher.onResume()onPause()→pusher.onPause()onDestroy()→pusher.release()
6.7 状态与统计回调
状态枚举:
IdleConnectingPublishingReconnectingStoppedFailed
统计字段:
- fps
- videoBitrateKbps / audioBitrateKbps
- rttMs
- cpu 使用率(Demo 通过
CpuUsage读取) - auto framing state(通过
onAutoFramingStateChanged/getAutoFramingState()获取)
6.8 推流 API 速览(含 Demo 未覆盖)
初始化与预览:
initWithLiveMode(context, liveMode):创建推流实例setPreviewView(view):设置预览 View;TextureView会按当前liveMode选择协议setPreviewView(view, mode):显式设置预览 View 与协议,TextureView推荐使用attachPreview(container):将默认预览 View 添加到容器attachPreview(container, useTextureView):创建并绑定Surface/OpenGL或TextureView预览getPreviewView():获取当前预览 View
采集与推流:
startRunning(cameraPosition, videoConfig, audioConfig):开始采集预览setVideoConfiguration(config):更新视频参数setXorKey(hexKey):设置推流 XOR key(RTMP payload / RTC-WHIP frame,可选)setAutoFramingEnabled(enabled)/getAutoFramingCapability()/getAutoFramingState():自动取景控制与状态查询startLiveWithStreamId(streamId):使用 streamId 推流startLiveWithUrl(url):使用完整 URL 推流stopLive()/stopLive(callback):停止推流
设备与音视频控制:
switchCameraPosition(position)/switchCamera():切换摄像头startCamera()/stopCamera():启动 / 停止摄像头startMicrophone()/stopMicrophone():启动 / 停止麦克风setMuted(true/false):静音推流音频setCameraEnabled(true/false):开启 / 关闭摄像头
美颜与画面:
setBeautyEngine(engine):设置美颜引擎setBeautyEnabled(true/false):启用 / 关闭美颜setBeautyLevel(level):设置美颜强度onAutoFramingStateChanged(state):自动取景状态回调setStreamOrientation(orientation):设置推流方向changeResolution(width, height):动态调整分辨率setBitmapAsVideoSource(bitmap)/restoreCameraVideoSource():背景图推流
生命周期:
onPause()/onResume()/release():与 Activity 生命周期对齐
7. 拉流接入详解
7.1 创建播放器
val player = SellyLiveVideoPlayer.initWithStreamId(
context = this,
streamId = streamId,
liveMode = SellyLiveMode.RTC,
xorKeyHex = "" // 加密流传入同一 key,明文流可留空
)
// 或直接使用完整 URL
// val player = SellyLiveVideoPlayer.initWithUrl(this, playUrl, xorKeyHex = "A1B2...")
若需要指定 vhost / appName:
val player = SellyLiveVideoPlayer.initWithStreamId(
context = this,
streamId = streamId,
liveMode = SellyLiveMode.RTMP,
vhost = "your-vhost",
appName = "live",
xorKeyHex = "A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6"
)
使用 RTMP 或 RTC/WHEP 加密流时,请在创建播放器时传入
xorKeyHex;后续如需换 key,请重建播放器实例。
7.2 设置拉流 Token(使用 streamId 时)
player.token = playToken
直接使用 RTMP 地址拉流不需要 Token,可不设置。
7.3 播放流程
player.attachRenderView(renderContainer, com.sellycloud.sellycloudsdk.render.RenderBackend.SURFACE_VIEW)
player.prepareToPlay()
player.play()
7.3.1 播放渲染后端选择
直播播放器支持以下渲染接入方式:
attachRenderView(container, RenderBackend.SURFACE_VIEW):默认旧路径attachRenderView(container, RenderBackend.TEXTURE_VIEW):使用TextureViewsetRenderView(view):手动传入SurfaceView、SurfaceViewRenderer或TextureViewsetRenderSurfaceTexture(surfaceTexture, width, height):高级场景下直接绑定SurfaceTexture(调用方负责 SurfaceTexture 生命周期)
示例:
val backend = com.sellycloud.sellycloudsdk.render.RenderBackend.TEXTURE_VIEW
player.attachRenderView(renderContainer, backend)
player.prepareToPlay()
player.play()
说明:
RTMP播放支持SurfaceView、TextureView、SurfaceTextureRTC/WHEP播放支持SurfaceViewRenderer、TextureView,以及高级场景下的SurfaceTexture- 当前版本建议在 开始播放前 选定渲染后端;当前 Demo 在首页设置中统一选择,进入页面后不再暴露热切换
控制接口:
pause()stop()play()setMuted(true/false)release()
补充接口(Demo 未覆盖):
setRenderView(view):手动指定渲染 ViewsetRenderSurfaceTexture(surfaceTexture, width, height):直接绑定SurfaceTexture(调用方负责 SurfaceTexture 生命周期)clearRenderTarget():解绑当前渲染面,播放会话可继续存活seekBy(deltaMs):播放进度跳转(仅在流支持快进/回放时有效)
7.4 播放回调
player.delegate = object : SellyLiveVideoPlayerDelegate {
override fun playbackStateChanged(state: SellyPlayerState) {}
override fun onFirstVideoFrameRendered() {}
override fun onFirstAudioFrameRendered() {}
override fun onLatencyChasingUpdate(update: SellyLatencyChasingUpdate) {}
override fun onLatencyChasingReloadRequired(latencyMs: Long) {}
override fun onError(error: SellyLiveError) {}
}
状态枚举:
IdleConnectingPlayingPausedStoppedReconnectingFailed
7.5 播放 API 速览(含 Demo 未覆盖)
创建与渲染:
initWithStreamId(context, streamId, liveMode, vhost, appName, xorKeyHex):使用 streamId 创建播放器initWithUrl(context, url, xorKeyHex):使用完整 URL 创建播放器attachRenderView(container):创建默认SurfaceView渲染 ViewattachRenderView(container, backend):创建指定 backend 的渲染 ViewsetRenderView(view):手动设置渲染 ViewsetRenderSurfaceTexture(surfaceTexture, width, height):绑定SurfaceTexture(调用方负责 SurfaceTexture 生命周期)clearRenderTarget():解绑当前渲染面getRenderView():获取当前渲染 View
播放控制:
prepareToPlay()/play()/pause()/stop():播放流程控制seekBy(deltaMs):播放进度跳转(流支持回放时有效)isPlaying():查询播放状态setMuted(true/false):静音播放
统计与释放:
setStatsListener { snapshot -> }:播放统计回调release():释放播放器资源
7.6 点播播放器渲染说明
SellyVodPlayer 与直播播放器在渲染后端模型上保持一致:
attachRenderView(container, backend):支持SURFACE_VIEW/TEXTURE_VIEWsetRenderView(surfaceView)/setRenderView(textureView):手动绑定现有 ViewsetRenderSurfaceTexture(surfaceTexture, width, height):高级场景使用SurfaceTexture(调用方负责 SurfaceTexture 生命周期)clearRenderTarget():解绑当前渲染面但不一定立即销毁播放实例
因此 Demo 中点播页的 SurfaceView / TextureView 选择,也与直播播放页保持一致,均在首页设置中统一生效。
8. 错误处理与重试建议
Token 错误
- 停止当前推 / 拉
- 获取新 Token
- 重新设置 Token
- 重新开始推流 / 拉流流程
网络错误
- 监听
onStatisticsUpdate或播放器状态 - 弱网时适当降低分辨率 / 码率
- 必要时重启连接
9. 最佳实践
- 推流前先完成采集预览
SurfaceView / TextureViewbackend 建议在开始推流或播放前选定RTC/WHIP的美颜、滤镜、水印、观测优先使用TEXTURE_2DI420 / RGBA仅在算法必须访问 CPU 像素时再使用- 完整重写输出的 GPU 处理器设置
fullRewrite = true;叠加类处理保留默认值 - Token 即将过期前提前刷新
- 使用统计回调做质量监控
- 拉流失败避免无限重试
- 使用代理时,确保在推拉流开始前代理地址已设置完毕
10. 常见问题(FAQ)
Q1:Token 可以拼接到 URL 吗?
A: 不可以。
SDK 不解析 URL 中的鉴权信息,所有鉴权均通过 token 属性完成。
Q2:运行中修改 Token 是否生效?
A: 运行中修改 Token 不会影响当前已建立的连接。 下次重连或重新启动推流 / 拉流时会使用新的 Token。
Q3:播放器出现黑屏怎么办?
A: 可按以下步骤排查:
- 检查播放地址是否正确
- 确认当前网络连接正常
- 查看播放器回调中的错误信息
- 确认视频流格式是否被 SDK 支持
Q4:加密流播放花屏/噪音怎么办?
A: 重点检查以下项:
- 推流端与播放端
xorKeyHex是否完全一致 - key 格式是否为合法 hex(偶数长度,支持
0x前缀) - 当前是
RTMP还是RTC/WHEP,两端是否都走了对应的加密流配置 - 变更 key 后是否已重启推流 / 重建播放器
Q5:什么时候选择 SurfaceView,什么时候选择 TextureView?
A:
- 普通原生 Android 页面,优先使用默认
SurfaceView,性能最优 - 需要与按钮、封面、弹层等普通 View 正常混排时,优先使用
TextureView - Flutter 场景通过
setRenderSurfaceTexture()接入,走TextureView同一套渲染管线 - 当前版本建议在开始推流/播放前选定 backend;当前 Demo 在首页设置中统一选择,进入页面后不支持切换
Q5.1:TextureView 模式下,VOD/RTMP 播放的 BufferQueueProducer timeout 日志是什么?
A:
SDK 内部使用 GL Bridge 将 MediaCodec 硬解输出通过 OpenGL 中转渲染到 TextureView,大幅减少此类日志。如在极端场景下仍偶现,属于 Android 系统 BufferQueue 机制限制,不影响播放功能。SurfaceView 路径不存在此问题。
Q5.2:attach 和 set 两套 API 的区别?
A:
| API | 谁创建 View | 谁释放 |
|---|---|---|
attachRenderView() / attachPreview() |
SDK 创建 | SDK 在 release() 时自动释放 |
setRenderView() / setPreviewView() |
调用方创建并传入 | 调用方负责释放,SDK 只做绑定/解绑 |
setRenderSurfaceTexture() |
调用方传入 SurfaceTexture | 调用方负责 SurfaceTexture 生命周期 |
Q6:如何接入代理/加速服务(如洋葱盾)?
A:
SDK 本身不集成任何第三方代理 SDK。业务方需在 SDK 外部完成代理初始化与地址获取,然后通过 SellyCloudManager.setProxyAddress(proxyUrl) 注入。SDK 内部会自动通过代理地址解析真实服务器 IP。
示例流程:
- 在 Application 或 Activity 中初始化代理 SDK
- 获取本地代理地址(如
http://127.0.0.1:12345) - 调用
SellyCloudManager.setProxyAddress("http://127.0.0.1:12345") - 正常进行推流 / 拉流
Demo 中的
KiwiHelper展示了洋葱盾的完整接入流程,可作为参考。