845 lines
29 KiB
Markdown
845 lines
29 KiB
Markdown
# 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)
|
||
|
||
```gradle
|
||
dependencies {
|
||
implementation files("libs/sellycloudsdk-1.0.1.aar")
|
||
}
|
||
```
|
||
|
||
> 若接入美颜等能力,请按业务侧 SDK 要求额外引入第三方美颜库(Demo 使用 FaceUnity AAR)。
|
||
|
||
### 3.3 权限配置(AndroidManifest.xml)
|
||
|
||
```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:
|
||
|
||
```kotlin
|
||
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 外部获取,然后通过以下接口注入:
|
||
|
||
```kotlin
|
||
// 设置代理地址
|
||
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 属性**
|
||
|
||
推荐流程:
|
||
|
||
1. 业务侧向服务端获取新 Token
|
||
2. 调用 `pusher.token = newToken` / `player.token = newToken`
|
||
3. 停止并重新开始推流 / 拉流流程
|
||
|
||
### 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 创建推流实例
|
||
|
||
```kotlin
|
||
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 视频参数配置与预览
|
||
|
||
```kotlin
|
||
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)`:手动传入预览 View
|
||
- `setPreviewView(view, mode)`:当传入 `TextureView` 时,建议使用这个显式协议版本
|
||
|
||
示例:
|
||
|
||
```kotlin
|
||
// 默认旧路径
|
||
pusher.attachPreview(previewContainer, useTextureView = false)
|
||
|
||
// TextureView 路径
|
||
pusher.attachPreview(previewContainer, useTextureView = true)
|
||
```
|
||
|
||
```kotlin
|
||
// 手动指定 TextureView 时,建议显式传入 liveMode
|
||
val textureView = com.sellycloud.sellycloudsdk.widget.AspectRatioTextureView(this)
|
||
pusher.setPreviewView(textureView, SellyLiveMode.RTMP)
|
||
```
|
||
|
||
说明:
|
||
|
||
- `RTMP` 模式下,SDK 内部会根据预览 View 类型自动选择 `OpenGlView` 或 `TextureView`
|
||
- `RTC/WHIP` 预览也支持 `TextureView`
|
||
- 当前版本建议在 **开始采集/推流前** 选定预览后端;不保证运行中热切换预览后端
|
||
|
||
### 6.3 设置推流 Token(使用 streamId 时)
|
||
|
||
```kotlin
|
||
pusher.token = pushToken
|
||
```
|
||
|
||
#### 推流 XOR(RTMP / RTC-WHIP,可选)
|
||
|
||
```kotlin
|
||
val xorKeyHex = "A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6"
|
||
|
||
// 建议在 startLiveWith... 之前设置
|
||
pusher.setXorKey(xorKeyHex)
|
||
```
|
||
|
||
> `setXorKey(...)` 同时作用于 RTMP 推流与 RTC/WHIP 推流。若在推流中修改 key,需停止并重新开始推流后才会使用新 key。
|
||
|
||
### 6.4 开始/停止推流
|
||
|
||
```kotlin
|
||
pusher.startLiveWithStreamId(streamId)
|
||
```
|
||
|
||
```kotlin
|
||
pusher.stopLive()
|
||
```
|
||
|
||
#### 直接使用 RTMP 地址推流(Demo 未覆盖)
|
||
|
||
```kotlin
|
||
val rtmpUrl = "rtmp://your.host/live/streamKey"
|
||
pusher.startLiveWithUrl(rtmpUrl)
|
||
```
|
||
|
||
> 直接使用 RTMP 地址推流不需要 Token,可不设置。
|
||
|
||
#### 停止推流(带回调)
|
||
|
||
```kotlin
|
||
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`
|
||
|
||
接入示例:
|
||
|
||
```kotlin
|
||
val beautyEngine = FaceUnityBeautyEngine()
|
||
|
||
pusher.setBeautyEngine(beautyEngine)
|
||
pusher.setBeautyEnabled(true)
|
||
pusher.setBeautyLevel(3.0f)
|
||
```
|
||
|
||
说明:
|
||
|
||
- `BeautyEngine.createProcessor()` 返回的是 SDK V2 `VideoProcessor`
|
||
- 当前 Demo 的美颜实现走 `TEXTURE_2D + READ_WRITE`
|
||
- 美颜属于“完整重写输出”的场景,建议在 `VideoProcessorConfig` 中设置 `fullRewrite = true`
|
||
- `RTC/WHIP` 路径优先推荐 `TEXTURE_2D`,避免对 texture-backed 帧做额外的 texture-to-CPU 转换
|
||
|
||
### 6.5.2 推流前帧处理与观察
|
||
|
||
直播推流支持:
|
||
|
||
- 一个可写 `VideoProcessor`
|
||
- 多个只读 `VideoFrameObserver`
|
||
|
||
只读观测示例:
|
||
|
||
```kotlin
|
||
val disposable = pusher.addVideoFrameObserver(object : VideoFrameObserver {
|
||
override val config = VideoFrameObserverConfig(
|
||
preferredFormat = VideoProcessFormat.TEXTURE_2D
|
||
)
|
||
|
||
override fun onTextureFrame(frame: VideoTextureFrame) {
|
||
// 只读观测,不修改输出
|
||
}
|
||
})
|
||
```
|
||
|
||
可写处理示例:
|
||
|
||
```kotlin
|
||
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_2D`
|
||
- `RTMP` 在确实需要 CPU 像素时,可使用 `I420` / `RGBA`
|
||
- `READ_WRITE` 模式下,SDK 会准备输出缓冲;只有“完整覆盖输出”的场景才建议 `fullRewrite = true`
|
||
- `outputTextureId` 由 SDK 管理,处理器不应转移所有权,也不应在回调里主动删除纹理
|
||
- `VideoFrameObserverConfig` 的默认值仍为 `I420` 以兼容旧接入;新接入建议显式声明 `preferredFormat`
|
||
|
||
Demo 中当前可直接验证的模式:
|
||
|
||
- `帧回调纹理`:`TEXTURE_2D` observer
|
||
- `帧回调空CPU`:声明 `I420`,不处理像素
|
||
- `帧回调单CPU`:单个 `I420` observer
|
||
- `帧回调双CPU`:两个 `I420` observer,共享同一次 CPU 转换
|
||
- `改帧`:`RTC` 下走 `TEXTURE_2D`,`RTMP` 示例走 `RGBA`
|
||
|
||
### 6.5.3 自动取景(Auto Framing)
|
||
|
||
当前高层 API 已暴露:
|
||
|
||
- `setAutoFramingEnabled(enabled)`:开启 / 关闭自动取景
|
||
- `getAutoFramingCapability()`:查询当前是否支持及原因
|
||
- `getAutoFramingState()`:读取当前状态
|
||
- `delegate.onAutoFramingStateChanged(state)`:接收状态变化回调
|
||
|
||
状态枚举:
|
||
|
||
- `OFF`
|
||
- `INACTIVE`
|
||
- `FRAMING`
|
||
- `CONVERGED`
|
||
- `UNSUPPORTED`
|
||
|
||
当前约束:
|
||
|
||
- 当前自动取景只在 **RTMP 推流** 路径可用
|
||
- `RTC / WHIP` 推流当前会返回 `UNSUPPORTED`
|
||
- 需要摄像头已启动后再查询 capability;相机关闭、背景图推流等场景也会返回不支持
|
||
|
||
示例:
|
||
|
||
```kotlin
|
||
val capability = pusher.getAutoFramingCapability()
|
||
if (capability.supported) {
|
||
pusher.setAutoFramingEnabled(true)
|
||
}
|
||
```
|
||
|
||
### 6.6 生命周期建议
|
||
|
||
在宿主 Activity 中对齐生命周期:
|
||
|
||
- `onResume()` → `pusher.onResume()`
|
||
- `onPause()` → `pusher.onPause()`
|
||
- `onDestroy()` → `pusher.release()`
|
||
|
||
### 6.7 状态与统计回调
|
||
|
||
**状态枚举:**
|
||
|
||
- `Idle`
|
||
- `Connecting`
|
||
- `Publishing`
|
||
- `Reconnecting`
|
||
- `Stopped`
|
||
- `Failed`
|
||
|
||
**统计字段:**
|
||
|
||
- 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 创建播放器
|
||
|
||
```kotlin
|
||
val player = SellyLiveVideoPlayer.initWithStreamId(
|
||
context = this,
|
||
streamId = streamId,
|
||
liveMode = SellyLiveMode.RTC,
|
||
xorKeyHex = "" // 加密流传入同一 key,明文流可留空
|
||
)
|
||
// 或直接使用完整 URL
|
||
// val player = SellyLiveVideoPlayer.initWithUrl(this, playUrl, xorKeyHex = "A1B2...")
|
||
```
|
||
|
||
若需要指定 `vhost` / `appName`:
|
||
|
||
```kotlin
|
||
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 时)
|
||
|
||
```kotlin
|
||
player.token = playToken
|
||
```
|
||
> 直接使用 RTMP 地址拉流不需要 Token,可不设置。
|
||
|
||
### 7.3 播放流程
|
||
|
||
```kotlin
|
||
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)`:使用 `TextureView`
|
||
- `setRenderView(view)`:手动传入 `SurfaceView`、`SurfaceViewRenderer` 或 `TextureView`
|
||
- `setRenderSurfaceTexture(surfaceTexture, width, height)`:高级场景下直接绑定 `SurfaceTexture`(调用方负责 SurfaceTexture 生命周期)
|
||
|
||
示例:
|
||
|
||
```kotlin
|
||
val backend = com.sellycloud.sellycloudsdk.render.RenderBackend.TEXTURE_VIEW
|
||
player.attachRenderView(renderContainer, backend)
|
||
player.prepareToPlay()
|
||
player.play()
|
||
```
|
||
|
||
说明:
|
||
|
||
- `RTMP` 播放支持 `SurfaceView`、`TextureView`、`SurfaceTexture`
|
||
- `RTC/WHEP` 播放支持 `SurfaceViewRenderer`、`TextureView`,以及高级场景下的 `SurfaceTexture`
|
||
- `RTMP/VOD` 的 `TextureView / SurfaceTexture` 默认走 **direct output**,优先保证首帧和低延迟
|
||
- 当前版本建议在 **开始播放前** 选定渲染后端;运行中如需变更目标,请走 `clearRenderTarget()` + `setRenderView(...)` / `setRenderSurfaceTexture(...)` 的显式重绑流程
|
||
- Flutter 场景优先使用 `setRenderSurfaceTexture(...)`,配合 Flutter `Texture` widget 使用;如 UI 层级正确性优先,不建议继续依赖 `Hybrid Composition + SurfaceView`
|
||
|
||
控制接口:
|
||
|
||
- `pause()`
|
||
- `stop()`
|
||
- `play()`
|
||
- `setMuted(true/false)`
|
||
- `release()`
|
||
|
||
补充接口(Demo 未覆盖):
|
||
|
||
- `setRenderView(view)`:手动指定渲染 View
|
||
- `setRenderSurfaceTexture(surfaceTexture, width, height)`:直接绑定 `SurfaceTexture`(调用方负责 SurfaceTexture 生命周期)
|
||
- `clearRenderTarget()`:解绑当前渲染面,播放会话可继续存活
|
||
- `seekBy(deltaMs)`:播放进度跳转(仅在流支持快进/回放时有效)
|
||
|
||
### 7.3.2 Flutter / SurfaceTexture 接入建议
|
||
|
||
如果业务侧需要把视频放到 Flutter UI 层下面,并正常叠加按钮、封面、弹层、动画,推荐使用:
|
||
|
||
- Flutter 侧创建 `TextureRegistry.SurfaceTextureEntry`
|
||
- Android 插件层取出 `SurfaceTexture`
|
||
- 调用 `setRenderSurfaceTexture(surfaceTexture, width, height)`
|
||
- Flutter 页面使用 `Texture(textureId)` 显示视频
|
||
|
||
示意:
|
||
|
||
```kotlin
|
||
player.setRenderSurfaceTexture(surfaceTexture, width, height)
|
||
player.prepareToPlay()
|
||
player.play()
|
||
```
|
||
|
||
说明:
|
||
|
||
- `SurfaceTexture` 生命周期由调用方负责
|
||
- 销毁前建议先调用 `clearRenderTarget()` 或直接 `release()`
|
||
- 如果页面重建、Texture 重新申请或 Flutter 侧切换 textureId,需要重新绑定新的 `SurfaceTexture`
|
||
|
||
### 7.4 播放回调
|
||
|
||
```kotlin
|
||
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) {}
|
||
}
|
||
```
|
||
|
||
状态枚举:
|
||
|
||
- `Idle`
|
||
- `Connecting`
|
||
- `Playing`
|
||
- `Paused`
|
||
- `Stopped`
|
||
- `Reconnecting`
|
||
- `Failed`
|
||
|
||
首帧语义说明:
|
||
|
||
- 默认 `DIRECT` 模式下,`onFirstVideoFrameRendered()` 对应 decoder 首帧可用时机
|
||
- 对 `TextureView / SurfaceTexture` 且启用了 playback processing 的场景,SDK 会等待目标渲染面确认首帧已真正呈现后,再触发 `onFirstVideoFrameRendered()`
|
||
- `onFirstAudioFrameRendered()` 仍表示音频首帧可播放时机;在 texture-backed processing 场景中,音频与视频首帧不一定完全同一时刻
|
||
|
||
### 7.4.1 播放侧帧回调与二次处理
|
||
|
||
播放器支持一组独立于采集/推流链路的播放侧高级能力:
|
||
|
||
- `setPlaybackFrameObserver(observer)`:播放侧只读帧回调
|
||
- `setPlaybackVideoProcessor(processor)`:播放侧可写纹理处理
|
||
|
||
当前能力边界:
|
||
|
||
- 当前仅支持 **texture-backed** 播放目标:`TextureView` / `SurfaceTexture`
|
||
- 当前仅支持 `preferredFormat = TEXTURE_2D`
|
||
- 当前仅支持 `stage = RENDER_PRE_DISPLAY`
|
||
- 当前默认渲染模式为 `DIRECT`
|
||
- 只有设置了有效的 observer / processor,才会切到 `PROCESSING`
|
||
- 如果当前 render target 已经绑定,新增或移除 observer / processor 后,需要 **重绑一次 texture render target** 才会生效
|
||
- `RTC/WHEP` 播放当前不支持这套 playback processing;当前主要用于 `RTMP/VOD` 播放链
|
||
|
||
只读 observer 示例:
|
||
|
||
```kotlin
|
||
player.setPlaybackFrameObserver(object : PlaybackFrameObserver {
|
||
override val config = PlaybackFrameObserverConfig(
|
||
preferredFormat = VideoProcessFormat.TEXTURE_2D,
|
||
stage = VideoStage.RENDER_PRE_DISPLAY
|
||
)
|
||
|
||
override fun onTextureFrame(frame: VideoTextureFrame) {
|
||
// 读取播放侧纹理帧信息
|
||
}
|
||
})
|
||
```
|
||
|
||
可写 processor 示例:
|
||
|
||
```kotlin
|
||
player.setPlaybackVideoProcessor(object : PlaybackVideoProcessor {
|
||
override val config = PlaybackVideoProcessorConfig(
|
||
preferredFormat = VideoProcessFormat.TEXTURE_2D,
|
||
mode = VideoProcessMode.READ_WRITE,
|
||
stage = VideoStage.RENDER_PRE_DISPLAY
|
||
)
|
||
|
||
override fun processTexture(input: VideoTextureFrame, outputTextureId: Int) {
|
||
// 将后处理结果写入 outputTextureId
|
||
}
|
||
})
|
||
```
|
||
|
||
### 7.5 播放 API 速览(含 Demo 未覆盖)
|
||
|
||
创建与渲染:
|
||
|
||
- `initWithStreamId(context, streamId, liveMode, vhost, appName, xorKeyHex)`:使用 streamId 创建播放器
|
||
- `initWithUrl(context, url, xorKeyHex)`:使用完整 URL 创建播放器
|
||
- `attachRenderView(container)`:创建默认 `SurfaceView` 渲染 View
|
||
- `attachRenderView(container, backend)`:创建指定 backend 的渲染 View
|
||
- `setRenderView(view)`:手动设置渲染 View
|
||
- `setRenderSurfaceTexture(surfaceTexture, width, height)`:绑定 `SurfaceTexture`(调用方负责 SurfaceTexture 生命周期)
|
||
- `clearRenderTarget()`:解绑当前渲染面
|
||
- `getRenderView()`:获取当前渲染 View
|
||
- `setPlaybackFrameObserver(observer)`:设置播放侧只读 observer(texture 路径)
|
||
- `setPlaybackVideoProcessor(processor)`:设置播放侧 processor(texture 路径)
|
||
|
||
播放控制:
|
||
|
||
- `prepareToPlay()` / `play()` / `pause()` / `stop()`:播放流程控制
|
||
- `seekBy(deltaMs)`:播放进度跳转(流支持回放时有效)
|
||
- `isPlaying()`:查询播放状态
|
||
- `setMuted(true/false)`:静音播放
|
||
|
||
统计与释放:
|
||
|
||
- `setStatsListener { snapshot -> }`:播放统计回调
|
||
- `release()`:释放播放器资源
|
||
|
||
### 7.6 点播播放器渲染说明
|
||
|
||
`SellyVodPlayer` 与直播播放器在渲染后端模型上保持一致:
|
||
|
||
- `attachRenderView(container, backend)`:支持 `SURFACE_VIEW` / `TEXTURE_VIEW`
|
||
- `setRenderView(surfaceView)` / `setRenderView(textureView)`:手动绑定现有 View
|
||
- `setRenderSurfaceTexture(surfaceTexture, width, height)`:高级场景使用 `SurfaceTexture`(调用方负责 SurfaceTexture 生命周期)
|
||
- `clearRenderTarget()`:解绑当前渲染面但不一定立即销毁播放实例
|
||
- `setPlaybackFrameObserver(observer)` / `setPlaybackVideoProcessor(processor)`:点播同样支持 texture-backed playback processing
|
||
|
||
补充说明:
|
||
|
||
- 点播在重绑 `TextureView / SurfaceTexture` 后,会自动复用最近一次视频宽高信息,保持正确显示比例
|
||
- 如在已有 texture 目标上新增或移除 observer / processor,也需要重绑一次 texture render target 才会应用新的渲染模式
|
||
|
||
因此 Demo 中点播页的 `SurfaceView / TextureView` 选择,也与直播播放页保持一致,均在首页设置中统一生效。
|
||
|
||
---
|
||
|
||
## 8. 错误处理与重试建议
|
||
|
||
### Token 错误
|
||
|
||
1. 停止当前推 / 拉
|
||
2. 获取新 Token
|
||
3. 重新设置 Token
|
||
4. 重新开始推流 / 拉流流程
|
||
|
||
### 网络错误
|
||
|
||
- 监听 `onStatisticsUpdate` 或播放器状态
|
||
- 弱网时适当降低分辨率 / 码率
|
||
- 必要时重启连接
|
||
|
||
---
|
||
|
||
## 9. 最佳实践
|
||
|
||
- 推流前先完成采集预览
|
||
- `SurfaceView / TextureView` backend 建议在开始推流或播放前选定
|
||
- Flutter 场景优先使用 `setRenderSurfaceTexture(...)`,不要把 `Hybrid Composition + SurfaceView` 当成默认方案
|
||
- 普通播放默认保持 `DIRECT`;只有确实需要播放侧帧观察或纹理后处理时,再启用 playback processing
|
||
- playback processing 当前仅建议用于 `TextureView / SurfaceTexture + TEXTURE_2D + RENDER_PRE_DISPLAY`
|
||
- 变更 texture 路径的 observer / processor 后,显式重绑一次 render target
|
||
- `RTC/WHIP` 的美颜、滤镜、水印、观测优先使用 `TEXTURE_2D`
|
||
- `I420 / 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()` 接入,配合 Flutter `Texture` widget 使用
|
||
- 当前版本建议在开始推流/播放前选定 backend;当前 Demo 在首页设置中统一选择,进入页面后不支持切换
|
||
|
||
### Q5.1:`TextureView` 模式下,VOD/RTMP 播放的 `BufferQueueProducer timeout` 日志是什么?
|
||
**A:**
|
||
|
||
当前 `RTMP/VOD` 的 `TextureView / SurfaceTexture` 默认走 direct output,以缩短首帧和减少黑屏。极端机型或系统版本下仍可能偶现 `BufferQueueProducer timeout` / `BufferQueue has been abandoned` 之类系统日志;如果不伴随黑屏、花屏、卡死,通常可视为 Android BufferQueue 机制噪声。开启 playback processing 时,texture 路径内部会启用额外的处理中转链,日志形态也可能与 direct 模式不同。
|
||
|
||
### 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。
|
||
|
||
示例流程:
|
||
1. 在 Application 或 Activity 中初始化代理 SDK
|
||
2. 获取本地代理地址(如 `http://127.0.0.1:12345`)
|
||
3. 调用 `SellyCloudManager.setProxyAddress("http://127.0.0.1:12345")`
|
||
4. 正常进行推流 / 拉流
|
||
|
||
> Demo 中的 `KiwiHelper` 展示了洋葱盾的完整接入流程,可作为参考。
|