添加游戏盾代理支持
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Selly Live SDK 推拉流接入文档(Android)
|
# Selly Live SDK 推拉流接入文档(Android)
|
||||||
|
|
||||||
> 统一 SDK 名称:**SellyCloudSDK**
|
> 统一 SDK 名称:**SellyCloudSDK**
|
||||||
> 本文档适用于 Android 客户端,面向对外集成方与内部使用。
|
> 本文档适用于 Android 客户端,面向对外集成方与内部使用。
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -19,6 +19,7 @@ Selly Live SDK 提供完整的音视频直播能力,支持 **推流(直播
|
|||||||
- 支持视频帧处理(美颜 / 滤镜 / 水印)
|
- 支持视频帧处理(美颜 / 滤镜 / 水印)
|
||||||
- 基于 **Token 的安全鉴权机制**
|
- 基于 **Token 的安全鉴权机制**
|
||||||
- 支持 **RTMP H264 + AAC payload XOR 保护(可选)**
|
- 支持 **RTMP H264 + AAC payload XOR 保护(可选)**
|
||||||
|
- 支持 **外部代理地址注入**(如洋葱盾等第三方安全代理)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -34,8 +35,8 @@ Selly Live SDK 提供完整的音视频直播能力,支持 **推流(直播
|
|||||||
|
|
||||||
### 3.1 项目结构参考
|
### 3.1 项目结构参考
|
||||||
|
|
||||||
- `example/`:Android Demo 工程
|
- `example/`:Android Demo 工程
|
||||||
- 推流示例:`example/src/main/java/com/demo/SellyCloudSDK/live/LivePushActivity.kt`
|
- 推流示例:`example/src/main/java/com/demo/SellyCloudSDK/live/LivePushActivity.kt`
|
||||||
- 拉流示例:`example/src/main/java/com/demo/SellyCloudSDK/live/LivePlayActivity.kt`
|
- 拉流示例:`example/src/main/java/com/demo/SellyCloudSDK/live/LivePlayActivity.kt`
|
||||||
- `example/libs/`:本地 AAR 依赖存放目录
|
- `example/libs/`:本地 AAR 依赖存放目录
|
||||||
|
|
||||||
@@ -60,9 +61,81 @@ dependencies {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Token 鉴权机制(重点)
|
## 4. SDK 初始化与代理配置
|
||||||
|
|
||||||
### 4.1 Token 注入方式
|
### 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 注入方式
|
||||||
|
|
||||||
| 场景 | 设置位置 |
|
| 场景 | 设置位置 |
|
||||||
| ---- | ---- |
|
| ---- | ---- |
|
||||||
@@ -76,7 +149,7 @@ dependencies {
|
|||||||
- SDK 内部在建立连接时自动携带当前 Token
|
- SDK 内部在建立连接时自动携带当前 Token
|
||||||
- 直接使用 RTMP 地址推/拉流不需要 Token,可不设置
|
- 直接使用 RTMP 地址推/拉流不需要 Token,可不设置
|
||||||
|
|
||||||
### 4.2 Token 设置时机(强约束)
|
### 5.2 Token 设置时机(强约束)
|
||||||
|
|
||||||
#### 推流
|
#### 推流
|
||||||
|
|
||||||
@@ -91,9 +164,9 @@ dependencies {
|
|||||||
- `prepareToPlay()`
|
- `prepareToPlay()`
|
||||||
- `play()`
|
- `play()`
|
||||||
|
|
||||||
> ⚠️ 在连接建立后修改 Token,不会影响当前连接。
|
> 在连接建立后修改 Token,不会影响当前连接。
|
||||||
|
|
||||||
### 4.3 Token 刷新机制说明
|
### 5.3 Token 刷新机制说明
|
||||||
|
|
||||||
- SDK **不提供自动刷新**
|
- SDK **不提供自动刷新**
|
||||||
- 业务层可在任意时刻 **重新设置 token 属性**
|
- 业务层可在任意时刻 **重新设置 token 属性**
|
||||||
@@ -104,7 +177,7 @@ dependencies {
|
|||||||
2. 调用 `pusher.token = newToken` / `player.token = newToken`
|
2. 调用 `pusher.token = newToken` / `player.token = newToken`
|
||||||
3. 停止并重新开始推流 / 拉流流程
|
3. 停止并重新开始推流 / 拉流流程
|
||||||
|
|
||||||
### 4.4 RTMP Payload XOR 保护(可选)
|
### 5.4 RTMP Payload XOR 保护(可选)
|
||||||
|
|
||||||
用途:
|
用途:
|
||||||
|
|
||||||
@@ -131,9 +204,9 @@ Key 格式:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. 推流接入详解
|
## 6. 推流接入详解
|
||||||
|
|
||||||
### 5.1 创建推流实例
|
### 6.1 创建推流实例
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
val pusher = SellyLiveVideoPusher.initWithLiveMode(
|
val pusher = SellyLiveVideoPusher.initWithLiveMode(
|
||||||
@@ -147,7 +220,7 @@ pusher.delegate = object : SellyLiveVideoPusherDelegate {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5.2 视频参数配置与预览
|
### 6.2 视频参数配置与预览
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
val config = SellyLiveVideoConfiguration.defaultConfiguration().apply {
|
val config = SellyLiveVideoConfiguration.defaultConfiguration().apply {
|
||||||
@@ -166,7 +239,7 @@ pusher.startRunning(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5.3 设置推流 Token(使用 streamId 时)
|
### 6.3 设置推流 Token(使用 streamId 时)
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
pusher.token = pushToken
|
pusher.token = pushToken
|
||||||
@@ -183,7 +256,7 @@ pusher.setXorKey(xorKeyHex)
|
|||||||
|
|
||||||
> 若在推流中修改 key,需停止并重新开始推流后才会使用新 key。
|
> 若在推流中修改 key,需停止并重新开始推流后才会使用新 key。
|
||||||
|
|
||||||
### 5.4 开始/停止推流
|
### 6.4 开始/停止推流
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
pusher.startLiveWithStreamId(streamId)
|
pusher.startLiveWithStreamId(streamId)
|
||||||
@@ -212,7 +285,7 @@ pusher.stopLive { error ->
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5.5 常用控制接口
|
### 6.5 常用控制接口
|
||||||
|
|
||||||
- `setMuted(true/false)`:静音
|
- `setMuted(true/false)`:静音
|
||||||
- `switchCameraPosition(...)`:切换摄像头
|
- `switchCameraPosition(...)`:切换摄像头
|
||||||
@@ -226,7 +299,7 @@ pusher.stopLive { error ->
|
|||||||
- `setBeautyLevel(level)`:设置美颜强度
|
- `setBeautyLevel(level)`:设置美颜强度
|
||||||
- `setBitmapAsVideoSource(...)` / `restoreCameraVideoSource()`:背景图推流
|
- `setBitmapAsVideoSource(...)` / `restoreCameraVideoSource()`:背景图推流
|
||||||
|
|
||||||
### 5.6 生命周期建议
|
### 6.6 生命周期建议
|
||||||
|
|
||||||
在宿主 Activity 中对齐生命周期:
|
在宿主 Activity 中对齐生命周期:
|
||||||
|
|
||||||
@@ -234,7 +307,7 @@ pusher.stopLive { error ->
|
|||||||
- `onPause()` → `pusher.onPause()`
|
- `onPause()` → `pusher.onPause()`
|
||||||
- `onDestroy()` → `pusher.release()`
|
- `onDestroy()` → `pusher.release()`
|
||||||
|
|
||||||
### 5.7 状态与统计回调
|
### 6.7 状态与统计回调
|
||||||
|
|
||||||
**状态枚举:**
|
**状态枚举:**
|
||||||
|
|
||||||
@@ -252,7 +325,7 @@ pusher.stopLive { error ->
|
|||||||
- rttMs
|
- rttMs
|
||||||
- cpu 使用率(Demo 通过 `CpuUsage` 读取)
|
- cpu 使用率(Demo 通过 `CpuUsage` 读取)
|
||||||
|
|
||||||
### 5.8 推流 API 速览(含 Demo 未覆盖)
|
### 6.8 推流 API 速览(含 Demo 未覆盖)
|
||||||
|
|
||||||
初始化与预览:
|
初始化与预览:
|
||||||
|
|
||||||
@@ -293,9 +366,9 @@ pusher.stopLive { error ->
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. 拉流接入详解
|
## 7. 拉流接入详解
|
||||||
|
|
||||||
### 6.1 创建播放器
|
### 7.1 创建播放器
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
val player = SellyLiveVideoPlayer.initWithStreamId(
|
val player = SellyLiveVideoPlayer.initWithStreamId(
|
||||||
@@ -323,14 +396,14 @@ val player = SellyLiveVideoPlayer.initWithStreamId(
|
|||||||
|
|
||||||
> 使用 RTMP 加密流时,请在创建播放器时传入 `xorKeyHex`;后续如需换 key,请重建播放器实例。
|
> 使用 RTMP 加密流时,请在创建播放器时传入 `xorKeyHex`;后续如需换 key,请重建播放器实例。
|
||||||
|
|
||||||
### 6.2 设置拉流 Token(使用 streamId 时)
|
### 7.2 设置拉流 Token(使用 streamId 时)
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
player.token = playToken
|
player.token = playToken
|
||||||
```
|
```
|
||||||
> 直接使用 RTMP 地址拉流不需要 Token,可不设置。
|
> 直接使用 RTMP 地址拉流不需要 Token,可不设置。
|
||||||
|
|
||||||
### 6.3 播放流程
|
### 7.3 播放流程
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
player.attachRenderView(renderContainer)
|
player.attachRenderView(renderContainer)
|
||||||
@@ -351,7 +424,7 @@ player.play()
|
|||||||
- `setRenderView(view)`:手动指定渲染 View
|
- `setRenderView(view)`:手动指定渲染 View
|
||||||
- `seekBy(deltaMs)`:播放进度跳转(仅在流支持快进/回放时有效)
|
- `seekBy(deltaMs)`:播放进度跳转(仅在流支持快进/回放时有效)
|
||||||
|
|
||||||
### 6.4 播放回调
|
### 7.4 播放回调
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
player.delegate = object : SellyLiveVideoPlayerDelegate {
|
player.delegate = object : SellyLiveVideoPlayerDelegate {
|
||||||
@@ -374,7 +447,7 @@ player.delegate = object : SellyLiveVideoPlayerDelegate {
|
|||||||
- `Reconnecting`
|
- `Reconnecting`
|
||||||
- `Failed`
|
- `Failed`
|
||||||
|
|
||||||
### 6.5 播放 API 速览(含 Demo 未覆盖)
|
### 7.5 播放 API 速览(含 Demo 未覆盖)
|
||||||
|
|
||||||
创建与渲染:
|
创建与渲染:
|
||||||
|
|
||||||
@@ -397,7 +470,7 @@ player.delegate = object : SellyLiveVideoPlayerDelegate {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. 错误处理与重试建议
|
## 8. 错误处理与重试建议
|
||||||
|
|
||||||
### Token 错误
|
### Token 错误
|
||||||
|
|
||||||
@@ -414,24 +487,25 @@ player.delegate = object : SellyLiveVideoPlayerDelegate {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. 最佳实践
|
## 9. 最佳实践
|
||||||
|
|
||||||
- 推流前先完成采集预览
|
- 推流前先完成采集预览
|
||||||
- Token 即将过期前提前刷新
|
- Token 即将过期前提前刷新
|
||||||
- 使用统计回调做质量监控
|
- 使用统计回调做质量监控
|
||||||
- 拉流失败避免无限重试
|
- 拉流失败避免无限重试
|
||||||
|
- 使用代理时,确保在推拉流开始前代理地址已设置完毕
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. 常见问题(FAQ)
|
## 10. 常见问题(FAQ)
|
||||||
|
|
||||||
### Q1:Token 可以拼接到 URL 吗?
|
### Q1:Token 可以拼接到 URL 吗?
|
||||||
**A:** 不可以。
|
**A:** 不可以。
|
||||||
SDK 不解析 URL 中的鉴权信息,所有鉴权均通过 `token` 属性完成。
|
SDK 不解析 URL 中的鉴权信息,所有鉴权均通过 `token` 属性完成。
|
||||||
|
|
||||||
### Q2:运行中修改 Token 是否生效?
|
### Q2:运行中修改 Token 是否生效?
|
||||||
**A:**
|
**A:**
|
||||||
运行中修改 Token **不会影响当前已建立的连接**。
|
运行中修改 Token **不会影响当前已建立的连接**。
|
||||||
**下次重连或重新启动推流 / 拉流时会使用新的 Token**。
|
**下次重连或重新启动推流 / 拉流时会使用新的 Token**。
|
||||||
|
|
||||||
### Q3:播放器出现黑屏怎么办?
|
### Q3:播放器出现黑屏怎么办?
|
||||||
@@ -449,3 +523,15 @@ SDK 不解析 URL 中的鉴权信息,所有鉴权均通过 `token` 属性完
|
|||||||
- key 格式是否为合法 hex(偶数长度,支持 `0x` 前缀)
|
- key 格式是否为合法 hex(偶数长度,支持 `0x` 前缀)
|
||||||
- 当前是否为 RTMP + H264 + AAC
|
- 当前是否为 RTMP + H264 + AAC
|
||||||
- 变更 key 后是否已重启推流 / 重建播放器
|
- 变更 key 后是否已重启推流 / 重建播放器
|
||||||
|
|
||||||
|
### Q5:如何接入代理/加速服务(如洋葱盾)?
|
||||||
|
**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` 展示了洋葱盾的完整接入流程,可作为参考。
|
||||||
|
|||||||
@@ -8,16 +8,17 @@ SDK 核心以 `InteractiveRtcEngine` 为中心,通过 `InteractiveRtcEngineEve
|
|||||||
|
|
||||||
## 目录
|
## 目录
|
||||||
|
|
||||||
1. 准备工作
|
1. 准备工作
|
||||||
2. 快速开始
|
2. 快速开始
|
||||||
3. 基础通话流程
|
3. 基础通话流程
|
||||||
4. 常用功能
|
4. 常用功能
|
||||||
5. 屏幕分享
|
5. 屏幕分享
|
||||||
6. 视频帧前后处理
|
6. 视频帧前后处理
|
||||||
7. 事件回调(EventHandler)
|
7. 事件回调(EventHandler)
|
||||||
8. 通话统计
|
8. 通话统计
|
||||||
9. Token 机制
|
9. Token 机制
|
||||||
10. 常见问题(FAQ)
|
10. 代理地址配置
|
||||||
|
11. 常见问题(FAQ)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -38,19 +39,40 @@ SDK 核心以 `InteractiveRtcEngine` 为中心,通过 `InteractiveRtcEngineEve
|
|||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
### 1. 创建引擎
|
### 1. SDK 初始化
|
||||||
|
|
||||||
|
在使用音视频通话功能前,需先初始化 SDK:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
SellyCloudManager.initialize(
|
||||||
|
context = applicationContext,
|
||||||
|
appId = "your-app-id"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
> `initialize` 的 `appId` 参数为权威值。可选传入 `SellyCloudConfig` 配置 `vhost`、`logEnabled` 等,详见推拉流文档。
|
||||||
|
|
||||||
|
### 2. 代理地址设置(可选)
|
||||||
|
|
||||||
|
若需通过代理(如洋葱盾)连接信令服务器,在创建引擎前设置:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
SellyCloudManager.setProxyAddress("http://127.0.0.1:12345")
|
||||||
|
```
|
||||||
|
|
||||||
|
> SDK 内部通过代理地址解析真实信令服务器 IP。不设置则使用直连。详见「代理地址配置」章节。
|
||||||
|
|
||||||
|
### 3. 创建引擎
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
val appId = getString(R.string.signaling_app_id)
|
val appId = getString(R.string.signaling_app_id)
|
||||||
val token = getString(R.string.signaling_token).takeIf { it.isNotBlank() }
|
val token = getString(R.string.signaling_token).takeIf { it.isNotBlank() }
|
||||||
val kiwiRsName = getString(R.string.signaling_kiwi_rsname).trim()
|
|
||||||
|
|
||||||
val rtcEngine = InteractiveRtcEngine.create(
|
val rtcEngine = InteractiveRtcEngine.create(
|
||||||
InteractiveRtcEngineConfig(
|
InteractiveRtcEngineConfig(
|
||||||
context = applicationContext,
|
context = applicationContext,
|
||||||
appId = appId,
|
appId = appId,
|
||||||
defaultToken = token,
|
defaultToken = token
|
||||||
kiwiRsName = kiwiRsName
|
|
||||||
)
|
)
|
||||||
).apply {
|
).apply {
|
||||||
setEventHandler(eventHandler)
|
setEventHandler(eventHandler)
|
||||||
@@ -68,9 +90,20 @@ val rtcEngine = InteractiveRtcEngine.create(
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
> `InteractiveRtcEngineConfig` 与默认 token 配置见 `example/src/main/java/com/demo/SellyCloudSDK/interactive/InteractiveLiveActivity.kt`。
|
`InteractiveRtcEngineConfig` 参数说明:
|
||||||
|
|
||||||
### 2. 设置本地/远端画布
|
| 参数 | 类型 | 说明 |
|
||||||
|
| ---- | ---- | ---- |
|
||||||
|
| `context` | Context | 应用上下文 |
|
||||||
|
| `appId` | String | 应用 ID |
|
||||||
|
| `defaultCallType` | CallType | 默认通话类型,默认 ONE_TO_ONE |
|
||||||
|
| `defaultToken` | String? | 默认 Token |
|
||||||
|
| `signalingUrlPrefix` | String | 信令 URL 前缀,默认 `ws://` |
|
||||||
|
| `signalingUrlSuffix` | String | 信令 URL 后缀,默认 `/ws/signaling` |
|
||||||
|
|
||||||
|
> 完整 Demo 见 `example/src/main/java/com/demo/SellyCloudSDK/interactive/InteractiveLiveActivity.kt`。
|
||||||
|
|
||||||
|
### 4. 设置本地/远端画布
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
val localRenderer = SurfaceViewRenderer(this)
|
val localRenderer = SurfaceViewRenderer(this)
|
||||||
@@ -82,7 +115,7 @@ val remoteRenderer = SurfaceViewRenderer(this)
|
|||||||
rtcEngine.setupRemoteVideo(InteractiveVideoCanvas(remoteRenderer, remoteUserId))
|
rtcEngine.setupRemoteVideo(InteractiveVideoCanvas(remoteRenderer, remoteUserId))
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. 加入通话
|
### 5. 加入通话
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
val options = InteractiveChannelMediaOptions(callType = CallType.ONE_TO_ONE)
|
val options = InteractiveChannelMediaOptions(callType = CallType.ONE_TO_ONE)
|
||||||
@@ -109,23 +142,9 @@ rtcEngine.leaveChannel()
|
|||||||
InteractiveRtcEngine.destroy(rtcEngine)
|
InteractiveRtcEngine.destroy(rtcEngine)
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. 进阶配置(Demo 未覆盖)
|
### 6. 进阶配置(Demo 未覆盖)
|
||||||
|
|
||||||
#### 4.1 InteractiveRtcEngineConfig 高级字段
|
#### 6.1 InteractiveChannelMediaOptions 订阅控制
|
||||||
|
|
||||||
```kotlin
|
|
||||||
val config = InteractiveRtcEngineConfig(
|
|
||||||
context = applicationContext,
|
|
||||||
appId = appId,
|
|
||||||
defaultCallType = CallType.ONE_TO_ONE,
|
|
||||||
defaultToken = token,
|
|
||||||
kiwiRsName = kiwiRsName,
|
|
||||||
signalingUrlPrefix = "https://",
|
|
||||||
signalingUrlSuffix = "/signaling"
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 4.2 InteractiveChannelMediaOptions 订阅控制
|
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
val options = InteractiveChannelMediaOptions(
|
val options = InteractiveChannelMediaOptions(
|
||||||
@@ -135,7 +154,7 @@ val options = InteractiveChannelMediaOptions(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 4.3 InteractiveVideoEncoderConfig 更多参数
|
#### 6.2 InteractiveVideoEncoderConfig 更多参数
|
||||||
|
|
||||||
可选项(按需设置):
|
可选项(按需设置):
|
||||||
|
|
||||||
@@ -150,14 +169,16 @@ val options = InteractiveChannelMediaOptions(
|
|||||||
|
|
||||||
## 基础通话流程
|
## 基础通话流程
|
||||||
|
|
||||||
1. 创建 `InteractiveRtcEngine`
|
1. 初始化 SDK(`SellyCloudManager.initialize`)
|
||||||
2. 设置 `EventHandler`
|
2. 设置代理地址(可选,`SellyCloudManager.setProxyAddress`)
|
||||||
3. 配置 `InteractiveVideoEncoderConfig`
|
3. 创建 `InteractiveRtcEngine`
|
||||||
4. 设置本地画布 `setupLocalVideo`
|
4. 设置 `EventHandler`
|
||||||
5. `joinChannel` 加入频道
|
5. 配置 `InteractiveVideoEncoderConfig`
|
||||||
6. `onUserJoined` 后设置远端画布
|
6. 设置本地画布 `setupLocalVideo`
|
||||||
7. 通话中进行音视频控制
|
7. `joinChannel` 加入频道
|
||||||
8. `leaveChannel` 并释放资源
|
8. `onUserJoined` 后设置远端画布
|
||||||
|
9. 通话中进行音视频控制
|
||||||
|
10. `leaveChannel` 并释放资源
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -260,7 +281,7 @@ rtcEngine.setRenderVideoFrameInterceptor { frame, userId ->
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
> Demo 中的美颜示例见:
|
> Demo 中的美颜示例见:
|
||||||
> `example/src/main/java/com/demo/SellyCloudSDK/beauty/FuVideoFrameInterceptor.kt`
|
> `example/src/main/java/com/demo/SellyCloudSDK/beauty/FuVideoFrameInterceptor.kt`
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -321,6 +342,61 @@ rtcEngine.renewToken(newToken, expiresAtSec)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 代理地址配置
|
||||||
|
|
||||||
|
SDK 支持通过外部代理(如洋葱盾等安全加速服务)连接信令服务器。代理地址由业务方在 SDK 外部获取,然后注入 SDK。
|
||||||
|
|
||||||
|
### 设置方式
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// 设置代理地址(在 joinChannel 之前)
|
||||||
|
SellyCloudManager.setProxyAddress("http://127.0.0.1:12345")
|
||||||
|
|
||||||
|
// 清除代理(恢复直连)
|
||||||
|
SellyCloudManager.setProxyAddress(null)
|
||||||
|
|
||||||
|
// 查询当前代理地址
|
||||||
|
val proxy = SellyCloudManager.getProxyAddress() // null 表示未设置
|
||||||
|
```
|
||||||
|
|
||||||
|
### 格式要求
|
||||||
|
|
||||||
|
- 必须以 `http://` 或 `https://` 开头
|
||||||
|
- 传 `null` 或空字符串表示清除代理
|
||||||
|
- 格式不合法时抛出 `IllegalArgumentException`
|
||||||
|
|
||||||
|
### 生效范围
|
||||||
|
|
||||||
|
设置后,SDK 内部通过代理地址解析真实信令服务器 IP,对上层接口透明。
|
||||||
|
|
||||||
|
### 时机要求
|
||||||
|
|
||||||
|
- 必须在 `joinChannel()` **之前** 设置
|
||||||
|
- 通话过程中修改代理地址,需 `leaveChannel` 后重新 `joinChannel` 才能生效
|
||||||
|
|
||||||
|
### Demo 中的接入示例
|
||||||
|
|
||||||
|
Demo 使用 `KiwiHelper` 封装洋葱盾的初始化与代理获取,采用三阶段模式:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// 阶段 1:Application.onCreate() 异步初始化
|
||||||
|
KiwiHelper.initializeAsync()
|
||||||
|
|
||||||
|
// 阶段 2:Activity 初始化时启动代理获取(非阻塞)
|
||||||
|
KiwiHelper.startProxySetup(enableKiwi = true, rsName = "your-rs-name")
|
||||||
|
|
||||||
|
// 阶段 3:joinChannel 前确保代理已就绪
|
||||||
|
lifecycleScope.launch {
|
||||||
|
KiwiHelper.awaitProxyReady()
|
||||||
|
rtcEngine.joinChannel(...)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> `KiwiHelper` 内部通过 `SellyCloudManager.setProxyAddress()` 将代理地址传给 SDK。
|
||||||
|
> 详见 `example/src/main/java/com/demo/SellyCloudSDK/KiwiHelper.kt`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 更多 API 速览(含 Demo 未覆盖)
|
## 更多 API 速览(含 Demo 未覆盖)
|
||||||
|
|
||||||
引擎创建与销毁:
|
引擎创建与销毁:
|
||||||
@@ -328,6 +404,12 @@ rtcEngine.renewToken(newToken, expiresAtSec)
|
|||||||
- `InteractiveRtcEngine.create(config)`:创建引擎
|
- `InteractiveRtcEngine.create(config)`:创建引擎
|
||||||
- `InteractiveRtcEngine.destroy(engine)` / `engine.destroy()`:释放引擎
|
- `InteractiveRtcEngine.destroy(engine)` / `engine.destroy()`:释放引擎
|
||||||
|
|
||||||
|
SDK 初始化与代理:
|
||||||
|
|
||||||
|
- `SellyCloudManager.initialize(context, appId, config)`:初始化 SDK
|
||||||
|
- `SellyCloudManager.setProxyAddress(address)`:设置代理地址
|
||||||
|
- `SellyCloudManager.getProxyAddress()`:获取当前代理地址
|
||||||
|
|
||||||
通话控制:
|
通话控制:
|
||||||
|
|
||||||
- `setEventHandler(handler)`:设置事件回调
|
- `setEventHandler(handler)`:设置事件回调
|
||||||
@@ -369,7 +451,11 @@ rtcEngine.renewToken(newToken, expiresAtSec)
|
|||||||
1. 检查 `signaling_app_id` 是否正确
|
1. 检查 `signaling_app_id` 是否正确
|
||||||
2. Token 是否为空或已过期
|
2. Token 是否为空或已过期
|
||||||
3. 网络是否受限
|
3. 网络是否受限
|
||||||
|
4. 若使用代理,检查代理地址是否已正确设置
|
||||||
|
|
||||||
### Q:屏幕分享失败?
|
### Q:屏幕分享失败?
|
||||||
1. 是否已获取 `MediaProjection` 授权
|
1. 是否已获取 `MediaProjection` 授权
|
||||||
2. Android 14+ 是否启动前台服务
|
2. Android 14+ 是否启动前台服务
|
||||||
|
|
||||||
|
### Q:如何接入代理/加速服务?
|
||||||
|
SDK 本身不集成任何第三方代理 SDK。业务方需在外部完成代理初始化,获取本地代理地址后,通过 `SellyCloudManager.setProxyAddress()` 注入。详见「代理地址配置」章节。
|
||||||
|
|||||||
@@ -67,7 +67,8 @@ dependencies {
|
|||||||
implementation files(
|
implementation files(
|
||||||
sdkAarPath,
|
sdkAarPath,
|
||||||
"libs/fu_core_all_feature_release.aar",
|
"libs/fu_core_all_feature_release.aar",
|
||||||
"libs/fu_model_all_feature_release.aar"
|
"libs/fu_model_all_feature_release.aar",
|
||||||
|
"libs/Kiwi.aar"
|
||||||
)
|
)
|
||||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||||
implementation 'androidx.appcompat:appcompat:1.7.0-alpha03'
|
implementation 'androidx.appcompat:appcompat:1.7.0-alpha03'
|
||||||
|
|||||||
BIN
example/libs/Kiwi.aar
Normal file
BIN
example/libs/Kiwi.aar
Normal file
Binary file not shown.
Binary file not shown.
@@ -18,6 +18,7 @@
|
|||||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name=".DemoApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:label="SellyCloudRTC Demo"
|
android:label="SellyCloudRTC Demo"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.demo.SellyCloudSDK
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
|
||||||
|
class DemoApplication : Application() {
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
// Kiwi SDK 异步初始化(不阻塞启动)
|
||||||
|
KiwiHelper.initializeAsync()
|
||||||
|
}
|
||||||
|
}
|
||||||
188
example/src/main/java/com/demo/SellyCloudSDK/KiwiHelper.kt
Normal file
188
example/src/main/java/com/demo/SellyCloudSDK/KiwiHelper.kt
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
package com.demo.SellyCloudSDK
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.kiwi.sdk.Kiwi
|
||||||
|
import com.sellycloud.sellycloudsdk.SellyCloudManager
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.ensureActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Demo 端的 Kiwi 盾 SDK 封装
|
||||||
|
*
|
||||||
|
* 三阶段使用模式:
|
||||||
|
* 1. Application.onCreate() → initializeAsync() 异步初始化 Kiwi SDK
|
||||||
|
* 2. Activity 初始化 → startProxySetup(...) 后台获取代理地址
|
||||||
|
* 3. 开播/入会前 → awaitProxyReady() 确保代理已就绪
|
||||||
|
*/
|
||||||
|
object KiwiHelper {
|
||||||
|
private const val TAG = "KiwiHelper"
|
||||||
|
private const val DEFAULT_APP_KEY = "5XTXUZ/aqOwfjA4zQkY7VpjcNBucWxmNGY4vFNhwSMKWkn2WK383dbNgI+96Y+ttSPMFzqhu8fxP5SiCK5+/6cGrBQQt8pDQAOi3EN4Z6lzkC2cJ5mfjBVi4ZpFASG9e3divF5UqLG6sTmFI3eCuJxy9/kHXPSSkKWJe1MnBMQETpf4FRDVuR9d/LzXKQgA9PsjRbPRLx4f3h0TU2P4GEfv1c70FvkdwpqirQt9ik2hAhKuj0vJY60g+yYhGY19a07vBTW4MprN53RnSH8bCs79NNbWyzsg2++t+sKdZP1WPGeOho/xpsQRP8yWCXIOOdvdjiE3YXVltBgmPnA6gOjFS97WVlBAQ1mJE7rQi+/5hhfTuJlWoBH6000SRe7dc5EA0WGQX9U1Aj96ahBQhyHTrHJySmJ/hRMYMudqByF6K4PtrwZ8zugTjtx1dyLPOonZDlTu7hPAIcUfuaQ9xS3Phbq8lP67EYDsr3pkWuwL6AjrPjFwNmi0P1g+hV1ZQUmDQVGhNHmF3cE9Pd5ZOS10/fwaXYGRhcq9PlUSmcbU3scLtrBlzpOslyjlQ6W57EudCrvvJU3mimfs1A2y7cjpnLlJN1CWh6dQAaGcwSG2QA8+88qmlMH1t627fItTgHYrP1DkExpAr2dqgYDvsICJnHaRSBMe608GrPbFaECutRz5y3BEtQKcVKdgA1e6W4TFnxs5HqGrzc8iHPOOKGf8zHWEXkITPBKEiA86Nz46pDrqM9FKx4upPijn4Dahj8pd7yWTUIdHBT8X39Vm3/TSV5xT/lTinmv8rhBieb/2SQamTjVQ22VFq3nQ1h4TxUYTEc0nSjqcz54fWf1cyBy7uh82q1weKXUAJ8vG9W05vmt3/aDZ9+C8cWm53AQ90xgDvW7M1mZveuyfof2qrPsXTpj+jhpDkJgm6qJsvV5ClmGth8gvCM0rHjSIwxhYDZaIDK5TkFWjwLltt+YhhYLKketwuTHdlO/hCxrsFzlXHhXGVRC+kgXusfQUrHIm1WjW9o9EqasHg9ufUgg7cMO/9FRZhJ+Xdw9erprYDvu84Da9jL6NUUOSNIGTCJ/s29Lz4SIwCVG2lzm2UhD6E9ipGfG9gc6e/2vt1emOsP3/ipHVJf16r/9S4+dGKIjPX6QcHIIL2AMu2Je07nPmEoz7KaeOShox4bG3puMQdkdQo6kRIFpUzwUty+4EWqHmyPHGkGGGfI8gj0EreiZwgVJmBQ/8S5wlK+iUp+TVeoXo="
|
||||||
|
private const val INIT_TIMEOUT_SECONDS = 3L
|
||||||
|
private const val CONVERT_TIMEOUT_SECONDS = 1L
|
||||||
|
private const val AWAIT_INIT_TIMEOUT_MS = 4000L
|
||||||
|
|
||||||
|
/** Kiwi.Init 结果 Deferred */
|
||||||
|
private val initDeferred = CompletableDeferred<Boolean>()
|
||||||
|
|
||||||
|
/** 内部受控 scope,不依赖外部 lifecycle */
|
||||||
|
private val helperScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
||||||
|
|
||||||
|
/** 当前代理获取 Job */
|
||||||
|
@Volatile private var currentSetupJob: Job? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单调递增版本号,用于解决并发取消时旧 Job 覆盖新结果的竞态问题。
|
||||||
|
* 每次 startProxySetup 递增,resolveAndSetProxyAddress 在写入前校验版本一致性。
|
||||||
|
*/
|
||||||
|
private val setupVersion = AtomicInteger(0)
|
||||||
|
|
||||||
|
// ──────────────── 阶段 1:初始化 ────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步初始化 Kiwi SDK(Application.onCreate 调用,只调一次)
|
||||||
|
*/
|
||||||
|
fun initializeAsync() {
|
||||||
|
val executor = Executors.newSingleThreadExecutor()
|
||||||
|
val future = executor.submit<Int> { Kiwi.Init(DEFAULT_APP_KEY) }
|
||||||
|
Thread {
|
||||||
|
try {
|
||||||
|
val result = future.get(INIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||||
|
val success = result == 0
|
||||||
|
Log.d(TAG, if (success) "Kiwi 初始化成功" else "Kiwi 初始化失败, code=$result")
|
||||||
|
initDeferred.complete(success)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Kiwi 初始化异常: ${e.message}")
|
||||||
|
future.cancel(true)
|
||||||
|
initDeferred.complete(false)
|
||||||
|
} finally {
|
||||||
|
executor.shutdown()
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────── 阶段 2:启动代理获取 ────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动代理获取(非 suspend,可在主线程安全调用)
|
||||||
|
* - 递增版本号 + cancel 前一次 Job,保证"最后一次调用生效"
|
||||||
|
* - 内部协程 await 初始化 + IO ServerToLocal,不阻塞调用线程
|
||||||
|
*/
|
||||||
|
fun startProxySetup(enableKiwi: Boolean, rsName: String) {
|
||||||
|
val version = setupVersion.incrementAndGet()
|
||||||
|
currentSetupJob?.cancel()
|
||||||
|
if (!enableKiwi || rsName.isBlank()) {
|
||||||
|
SellyCloudManager.setProxyAddress(null)
|
||||||
|
currentSetupJob = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentSetupJob = helperScope.launch {
|
||||||
|
resolveAndSetProxyAddress(rsName, version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────── 阶段 3:等待代理就绪 ────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在开播/入会前调用,suspend 等待代理获取完成
|
||||||
|
* 如果 startProxySetup 未调用或已完成,立即返回
|
||||||
|
*/
|
||||||
|
suspend fun awaitProxyReady() {
|
||||||
|
currentSetupJob?.join()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────── 内部实现 ────────────────
|
||||||
|
|
||||||
|
private suspend fun awaitInitialization(): Boolean {
|
||||||
|
return withTimeoutOrNull(AWAIT_INIT_TIMEOUT_MS) {
|
||||||
|
initDeferred.await()
|
||||||
|
} ?: run {
|
||||||
|
Log.w(TAG, "等待 Kiwi 初始化超时 (${AWAIT_INIT_TIMEOUT_MS}ms)")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun resolveAndSetProxyAddress(rsName: String, version: Int): Boolean {
|
||||||
|
// 等待初始化完成
|
||||||
|
if (!awaitInitialization()) {
|
||||||
|
Log.w(TAG, "Kiwi 初始化失败/超时,清除代理")
|
||||||
|
setProxyIfCurrent(version, null)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// 在 IO 线程执行阻塞的 ServerToLocal
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val proxyUrl = convertRsToLocalUrl(rsName)
|
||||||
|
// 阻塞调用返回后,检查协程是否已取消
|
||||||
|
ensureActive()
|
||||||
|
// 版本校验:只有当前版本一致才写入,防止旧 Job 覆盖新结果
|
||||||
|
if (proxyUrl != null) {
|
||||||
|
Log.d(TAG, "Kiwi 代理地址: $proxyUrl")
|
||||||
|
setProxyIfCurrent(version, proxyUrl)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Kiwi ServerToLocal 失败,清除代理")
|
||||||
|
setProxyIfCurrent(version, null)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "代理解析异常: ${e.message}", e)
|
||||||
|
setProxyIfCurrent(version, null)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仅当 version 与当前 setupVersion 一致时才写入代理地址,
|
||||||
|
* 避免已过期的旧 Job 覆盖最新结果。
|
||||||
|
*/
|
||||||
|
private fun setProxyIfCurrent(version: Int, address: String?) {
|
||||||
|
if (setupVersion.get() == version) {
|
||||||
|
SellyCloudManager.setProxyAddress(address)
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "跳过过期的代理写入 (version=$version, current=${setupVersion.get()})")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kiwi.ServerToLocal + 返回码校验
|
||||||
|
*/
|
||||||
|
private fun convertRsToLocalUrl(rsName: String): String? {
|
||||||
|
val executor = Executors.newSingleThreadExecutor()
|
||||||
|
return try {
|
||||||
|
val future = executor.submit<String?> {
|
||||||
|
val ip = StringBuffer()
|
||||||
|
val port = StringBuffer()
|
||||||
|
val ret = Kiwi.ServerToLocal(rsName, ip, port)
|
||||||
|
if (ret != 0) {
|
||||||
|
Log.w(TAG, "ServerToLocal 返回错误码: $ret, rsName=$rsName")
|
||||||
|
return@submit null
|
||||||
|
}
|
||||||
|
val ipStr = ip.toString().trim()
|
||||||
|
val portStr = port.toString().trim()
|
||||||
|
if (ipStr.isNotEmpty() && portStr.isNotEmpty()) {
|
||||||
|
"http://$ipStr:$portStr"
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "ServerToLocal 返回空 ip/port")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
future.get(CONVERT_TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "ServerToLocal 异常: ${e.message}")
|
||||||
|
null
|
||||||
|
} finally {
|
||||||
|
executor.shutdown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,10 @@ import androidx.activity.result.contract.ActivityResultContracts
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.demo.SellyCloudSDK.KiwiHelper
|
||||||
import com.demo.SellyCloudSDK.R
|
import com.demo.SellyCloudSDK.R
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import com.demo.SellyCloudSDK.beauty.FURenderer
|
import com.demo.SellyCloudSDK.beauty.FURenderer
|
||||||
import com.demo.SellyCloudSDK.beauty.FuVideoFrameInterceptor
|
import com.demo.SellyCloudSDK.beauty.FuVideoFrameInterceptor
|
||||||
import com.demo.SellyCloudSDK.databinding.ActivityInteractiveLiveBinding
|
import com.demo.SellyCloudSDK.databinding.ActivityInteractiveLiveBinding
|
||||||
@@ -182,7 +185,10 @@ class InteractiveLiveActivity : AppCompatActivity() {
|
|||||||
private fun initRtcEngine() {
|
private fun initRtcEngine() {
|
||||||
val appId = getString(R.string.signaling_app_id)
|
val appId = getString(R.string.signaling_app_id)
|
||||||
val token = getString(R.string.signaling_token).takeIf { it.isNotBlank() }
|
val token = getString(R.string.signaling_token).takeIf { it.isNotBlank() }
|
||||||
|
// Kiwi 代理后台获取,rsName 为空时清除残留
|
||||||
val kiwiRsName = getString(R.string.signaling_kiwi_rsname).trim()
|
val kiwiRsName = getString(R.string.signaling_kiwi_rsname).trim()
|
||||||
|
KiwiHelper.startProxySetup(kiwiRsName.isNotBlank(), kiwiRsName)
|
||||||
|
|
||||||
beautyRenderer = FURenderer(this).also { it.setup() }
|
beautyRenderer = FURenderer(this).also { it.setup() }
|
||||||
fuFrameInterceptor = beautyRenderer?.let { FuVideoFrameInterceptor(it).apply {
|
fuFrameInterceptor = beautyRenderer?.let { FuVideoFrameInterceptor(it).apply {
|
||||||
setFrontCamera(isFrontCamera)
|
setFrontCamera(isFrontCamera)
|
||||||
@@ -192,8 +198,7 @@ class InteractiveLiveActivity : AppCompatActivity() {
|
|||||||
InteractiveRtcEngineConfig(
|
InteractiveRtcEngineConfig(
|
||||||
context = applicationContext,
|
context = applicationContext,
|
||||||
appId = appId,
|
appId = appId,
|
||||||
defaultToken = token,
|
defaultToken = token
|
||||||
kiwiRsName = kiwiRsName
|
|
||||||
)
|
)
|
||||||
).apply {
|
).apply {
|
||||||
setEventHandler(rtcEventHandler)
|
setEventHandler(rtcEventHandler)
|
||||||
@@ -596,6 +601,15 @@ class InteractiveLiveActivity : AppCompatActivity() {
|
|||||||
private fun executeJoin(request: JoinRequest) {
|
private fun executeJoin(request: JoinRequest) {
|
||||||
pendingJoinRequest = null
|
pendingJoinRequest = null
|
||||||
InteractiveForegroundService.start(this)
|
InteractiveForegroundService.start(this)
|
||||||
|
// 立即禁用按钮,防止 await 期间重复点击
|
||||||
|
setJoinButtonEnabled(false)
|
||||||
|
lifecycleScope.launch {
|
||||||
|
KiwiHelper.awaitProxyReady()
|
||||||
|
executeJoinInternal(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun executeJoinInternal(request: JoinRequest) {
|
||||||
val renderer = localRenderer ?: createRenderer().also {
|
val renderer = localRenderer ?: createRenderer().also {
|
||||||
localRenderer = it
|
localRenderer = it
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,9 +27,11 @@ import android.widget.Toast
|
|||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.appcompat.widget.AppCompatTextView
|
import androidx.appcompat.widget.AppCompatTextView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import coil.load
|
import coil.load
|
||||||
|
import com.demo.SellyCloudSDK.KiwiHelper
|
||||||
import com.demo.SellyCloudSDK.R
|
import com.demo.SellyCloudSDK.R
|
||||||
import com.demo.SellyCloudSDK.databinding.ActivityLivePlayBinding
|
import com.demo.SellyCloudSDK.databinding.ActivityLivePlayBinding
|
||||||
import com.demo.SellyCloudSDK.live.auth.LiveAuthHelper
|
import com.demo.SellyCloudSDK.live.auth.LiveAuthHelper
|
||||||
@@ -209,7 +211,10 @@ class LivePlayActivity : AppCompatActivity() {
|
|||||||
playerClient.attachRenderView(binding.renderContainer)
|
playerClient.attachRenderView(binding.renderContainer)
|
||||||
|
|
||||||
if (args.autoStart) {
|
if (args.autoStart) {
|
||||||
startPlayback()
|
lifecycleScope.launch {
|
||||||
|
KiwiHelper.awaitProxyReady()
|
||||||
|
startPlayback()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,7 +263,10 @@ class LivePlayActivity : AppCompatActivity() {
|
|||||||
if (currentState == SellyPlayerState.Paused) {
|
if (currentState == SellyPlayerState.Paused) {
|
||||||
playerClient.play()
|
playerClient.play()
|
||||||
} else {
|
} else {
|
||||||
startPlayback()
|
lifecycleScope.launch {
|
||||||
|
KiwiHelper.awaitProxyReady()
|
||||||
|
startPlayback()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,11 @@ import android.view.WindowManager
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.constraintlayout.widget.ConstraintSet
|
import androidx.constraintlayout.widget.ConstraintSet
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.demo.SellyCloudSDK.KiwiHelper
|
||||||
import com.demo.SellyCloudSDK.avdemo.AvDemoSettings
|
import com.demo.SellyCloudSDK.avdemo.AvDemoSettings
|
||||||
import com.demo.SellyCloudSDK.avdemo.AvDemoSettingsStore
|
import com.demo.SellyCloudSDK.avdemo.AvDemoSettingsStore
|
||||||
import com.demo.SellyCloudSDK.beauty.FaceUnityBeautyEngine
|
import com.demo.SellyCloudSDK.beauty.FaceUnityBeautyEngine
|
||||||
@@ -129,31 +131,34 @@ class LivePushActivity : AppCompatActivity() {
|
|||||||
if (isPublishing) {
|
if (isPublishing) {
|
||||||
pusher.stopLive()
|
pusher.stopLive()
|
||||||
} else {
|
} else {
|
||||||
val settings = settingsStore.read()
|
lifecycleScope.launch {
|
||||||
applyStreamConfig(settings)
|
KiwiHelper.awaitProxyReady()
|
||||||
pusher.setXorKey(settings.xorKeyHex)
|
val settings = settingsStore.read()
|
||||||
if (settings.useUrlMode) {
|
applyStreamConfig(settings)
|
||||||
val pushUrl = settings.pushUrl
|
pusher.setXorKey(settings.xorKeyHex)
|
||||||
if (pushUrl.isEmpty()) {
|
if (settings.useUrlMode) {
|
||||||
Toast.makeText(this, "请先在设置中输入推流地址", Toast.LENGTH_SHORT).show()
|
val pushUrl = settings.pushUrl
|
||||||
return@setOnClickListener
|
if (pushUrl.isEmpty()) {
|
||||||
|
Toast.makeText(this@LivePushActivity, "请先在设置中输入推流地址", Toast.LENGTH_SHORT).show()
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
pusher.startLiveWithUrl(pushUrl)
|
||||||
|
} else {
|
||||||
|
val env = envStore.read()
|
||||||
|
val streamId = settings.streamId
|
||||||
|
val authError = LiveAuthHelper.validateAuthConfig(env, streamId)
|
||||||
|
if (authError != null) {
|
||||||
|
Toast.makeText(this@LivePushActivity, authError, Toast.LENGTH_SHORT).show()
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
val auth = LiveAuthHelper.buildAuthParams(
|
||||||
|
env = env,
|
||||||
|
channelId = streamId,
|
||||||
|
type = LiveTokenSigner.TokenType.PUSH
|
||||||
|
)
|
||||||
|
pusher.token = auth?.tokenResult?.token
|
||||||
|
pusher.startLiveWithStreamId(streamId)
|
||||||
}
|
}
|
||||||
pusher.startLiveWithUrl(pushUrl)
|
|
||||||
} else {
|
|
||||||
val env = envStore.read()
|
|
||||||
val streamId = settings.streamId
|
|
||||||
val authError = LiveAuthHelper.validateAuthConfig(env, streamId)
|
|
||||||
if (authError != null) {
|
|
||||||
Toast.makeText(this, authError, Toast.LENGTH_SHORT).show()
|
|
||||||
return@setOnClickListener
|
|
||||||
}
|
|
||||||
val auth = LiveAuthHelper.buildAuthParams(
|
|
||||||
env = env,
|
|
||||||
channelId = streamId,
|
|
||||||
type = LiveTokenSigner.TokenType.PUSH
|
|
||||||
)
|
|
||||||
pusher.token = auth?.tokenResult?.token
|
|
||||||
pusher.startLiveWithStreamId(streamId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ import android.widget.Toast
|
|||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.widget.AppCompatTextView
|
import androidx.appcompat.widget.AppCompatTextView
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.demo.SellyCloudSDK.KiwiHelper
|
||||||
import com.demo.SellyCloudSDK.R
|
import com.demo.SellyCloudSDK.R
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import com.demo.SellyCloudSDK.databinding.ActivityPkPlayBinding
|
import com.demo.SellyCloudSDK.databinding.ActivityPkPlayBinding
|
||||||
import com.demo.SellyCloudSDK.live.auth.LiveAuthHelper
|
import com.demo.SellyCloudSDK.live.auth.LiveAuthHelper
|
||||||
import com.demo.SellyCloudSDK.live.auth.LiveTokenSigner
|
import com.demo.SellyCloudSDK.live.auth.LiveTokenSigner
|
||||||
@@ -184,7 +187,10 @@ class PkPlayActivity : AppCompatActivity() {
|
|||||||
binding.actionMute.setOnClickListener { toggleMute() }
|
binding.actionMute.setOnClickListener { toggleMute() }
|
||||||
|
|
||||||
if (args.autoStart) {
|
if (args.autoStart) {
|
||||||
startPlayback()
|
lifecycleScope.launch {
|
||||||
|
KiwiHelper.awaitProxyReady()
|
||||||
|
startPlayback()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,7 +377,10 @@ class PkPlayActivity : AppCompatActivity() {
|
|||||||
if (mainPaused) mainPlayer.play()
|
if (mainPaused) mainPlayer.play()
|
||||||
if (pkPaused) pkPlayer.play()
|
if (pkPaused) pkPlayer.play()
|
||||||
} else {
|
} else {
|
||||||
startPlayback()
|
lifecycleScope.launch {
|
||||||
|
KiwiHelper.awaitProxyReady()
|
||||||
|
startPlayback()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package com.demo.SellyCloudSDK.live.env
|
package com.demo.SellyCloudSDK.live.env
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import com.demo.SellyCloudSDK.KiwiHelper
|
||||||
import com.sellycloud.sellycloudsdk.SellyCloudConfig
|
import com.sellycloud.sellycloudsdk.SellyCloudConfig
|
||||||
import com.sellycloud.sellycloudsdk.SellyCloudManager
|
import com.sellycloud.sellycloudsdk.SellyCloudManager
|
||||||
import com.sellycloud.sellycloudsdk.SellyLiveMode
|
import com.sellycloud.sellycloudsdk.SellyLiveMode
|
||||||
|
|
||||||
fun LiveEnvSettings.applyToSdkRuntimeConfig(context: Context) {
|
fun LiveEnvSettings.applyToSdkRuntimeConfig(context: Context) {
|
||||||
|
// 1. SDK 初始化(同步,轻量)
|
||||||
SellyCloudManager.initialize(
|
SellyCloudManager.initialize(
|
||||||
context = context,
|
context = context,
|
||||||
appId = appId,
|
appId = appId,
|
||||||
@@ -15,12 +17,13 @@ fun LiveEnvSettings.applyToSdkRuntimeConfig(context: Context) {
|
|||||||
vhost = normalizedVhost(),
|
vhost = normalizedVhost(),
|
||||||
vhostKey = vhostKey,
|
vhostKey = vhostKey,
|
||||||
defaultStreamId = defaultStreamId,
|
defaultStreamId = defaultStreamId,
|
||||||
enableKiwi = enableKiwi,
|
|
||||||
kiwiRsName = kiwiRsName,
|
|
||||||
logEnabled = logEnabled,
|
logEnabled = logEnabled,
|
||||||
defaultLiveMode = protocol.toLiveMode()
|
defaultLiveMode = protocol.toLiveMode()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
// 2. 启动代理获取:内部受控 scope、cancel 旧 Job
|
||||||
|
// 不阻塞主线程,关键 start 点通过 awaitProxyReady() 保证就绪
|
||||||
|
KiwiHelper.startProxySetup(enableKiwi, kiwiRsName)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun LiveEnvSettings.normalizedAppName(): String = normalizedAppId()
|
fun LiveEnvSettings.normalizedAppName(): String = normalizedAppId()
|
||||||
|
|||||||
Reference in New Issue
Block a user