diff --git a/docs/SellySDK_直播推拉流接入文档_Android.md b/docs/SellySDK_直播推拉流接入文档_Android.md new file mode 100644 index 0000000..842a804 --- /dev/null +++ b/docs/SellySDK_直播推拉流接入文档_Android.md @@ -0,0 +1,401 @@ +# Selly Live SDK 推拉流接入文档(Android) + +> 统一 SDK 名称:**SellyCloudSDK** +> 本文档适用于 Android 客户端,面向对外集成方与内部使用。 + +--- + +## 1. 概述 + +Selly Live SDK 提供完整的音视频直播能力,支持 **推流(直播发布)** 与 **拉流(直播播放)** 两大核心场景,适用于泛直播、互动直播、实时音视频等业务。 + +### 主要能力 + +- 支持 **RTMP / RTC** 推流与播放模式 +- 高性能音视频采集与编码 +- 灵活的视频参数配置(分辨率 / 帧率 / 码率) +- 推流状态与统计回调 +- 拉流播放状态与错误回调 +- 支持视频帧处理(美颜 / 滤镜 / 水印) +- 基于 **Token 的安全鉴权机制** + +--- + +## 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.0.aar") +} +``` + +> 若接入美颜等能力,请按业务侧 SDK 要求额外引入第三方美颜库(Demo 使用 FaceUnity AAR)。 + +### 3.3 权限配置(AndroidManifest.xml) + +```xml + + +``` + +> 如需截图保存且兼容 Android 9 及以下,可额外申请 `WRITE_EXTERNAL_STORAGE`。 + +--- + +## 4. Token 鉴权机制(重点) + +### 4.1 Token 注入方式 + +| 场景 | 设置位置 | +| ---- | ---- | +| 推流 | `SellyLiveVideoPusher.token` | +| 拉流 | `SellyLiveVideoPlayer.token` | + +说明: + +- Token **不拼接到 URL** +- Token **不绑定 streamId** +- SDK 内部在建立连接时自动携带当前 Token +- 直接使用 RTMP 地址推/拉流不需要 Token,可不设置 + +### 4.2 Token 设置时机(强约束) + +#### 推流 + +必须在以下接口调用 **之前** 设置: + +- `startLiveWithStreamId(...)` + +#### 拉流 + +必须在以下接口调用 **之前** 设置: + +- `prepareToPlay()` +- `play()` + +> ⚠️ 在连接建立后修改 Token,不会影响当前连接。 + +### 4.3 Token 刷新机制说明 + +- SDK **不提供自动刷新** +- 业务层可在任意时刻 **重新设置 token 属性** + +推荐流程: + +1. 业务侧向服务端获取新 Token +2. 调用 `pusher.token = newToken` / `player.token = newToken` +3. 停止并重新开始推流 / 拉流流程 + +--- + +## 5. 推流接入详解 + +### 5.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) {} +} +``` + +### 5.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) +pusher.startRunning( + cameraPosition = SellyLiveCameraPosition.FRONT, + videoConfig = config, + audioConfig = null +) +``` + +### 5.3 设置推流 Token(使用 streamId 时) + +```kotlin +pusher.token = pushToken +``` + +### 5.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) { + // 处理停止失败 + } +} +``` + +### 5.5 常用控制接口 + +- `setMuted(true/false)`:静音 +- `switchCameraPosition(...)`:切换摄像头 +- `switchCamera()`:前后摄像头切换(自动切换) +- `startCamera()` / `stopCamera()`:控制摄像头 +- `startMicrophone()` / `stopMicrophone()`:控制麦克风 +- `setCameraEnabled(true/false)`:关闭/开启摄像头 +- `setStreamOrientation(...)`:切换推流方向 +- `setVideoConfiguration(...)` + `changeResolution(...)`:动态调整分辨率 +- `setBeautyEngine(...)` + `setBeautyEnabled(...)`:接入美颜 +- `setBeautyLevel(level)`:设置美颜强度 +- `setBitmapAsVideoSource(...)` / `restoreCameraVideoSource()`:背景图推流 + +### 5.6 生命周期建议 + +在宿主 Activity 中对齐生命周期: + +- `onResume()` → `pusher.onResume()` +- `onPause()` → `pusher.onPause()` +- `onDestroy()` → `pusher.release()` + +### 5.7 状态与统计回调 + +**状态枚举:** + +- `Idle` +- `Connecting` +- `Publishing` +- `Reconnecting` +- `Stopped` +- `Failed` + +**统计字段:** + +- fps +- videoBitrateKbps / audioBitrateKbps +- rttMs +- cpu 使用率(Demo 通过 `CpuUsage` 读取) + +### 5.8 推流 API 速览(含 Demo 未覆盖) + +初始化与预览: + +- `initWithLiveMode(context, liveMode)`:创建推流实例 +- `setPreviewView(view)`:设置预览 View +- `attachPreview(container)`:将预览 View 添加到容器 +- `getPreviewView()`:获取当前预览 View + +采集与推流: + +- `startRunning(cameraPosition, videoConfig, audioConfig)`:开始采集预览 +- `setVideoConfiguration(config)`:更新视频参数 +- `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)`:设置美颜强度 +- `setStreamOrientation(orientation)`:设置推流方向 +- `changeResolution(width, height)`:动态调整分辨率 +- `setBitmapAsVideoSource(bitmap)` / `restoreCameraVideoSource()`:背景图推流 + +生命周期: + +- `onPause()` / `onResume()` / `release()`:与 Activity 生命周期对齐 + +--- + +## 6. 拉流接入详解 + +### 6.1 创建播放器 + +```kotlin +val player = SellyLiveVideoPlayer.initWithStreamId( + context = this, + streamId = streamId, + liveMode = SellyLiveMode.RTC +) +// 或直接使用完整 URL +// val player = SellyLiveVideoPlayer.initWithUrl(this, playUrl) +``` + +若需要指定 `vhost` / `appName`: + +```kotlin +val player = SellyLiveVideoPlayer.initWithStreamId( + context = this, + streamId = streamId, + liveMode = SellyLiveMode.RTMP, + vhost = "your-vhost", + appName = "live" +) +``` + +### 6.2 设置拉流 Token(使用 streamId 时) + +```kotlin +player.token = playToken +``` +> 直接使用 RTMP 地址拉流不需要 Token,可不设置。 + +### 6.3 播放流程 + +```kotlin +player.attachRenderView(renderContainer) +player.prepareToPlay() +player.play() +``` + +控制接口: + +- `pause()` +- `stop()` +- `play()` +- `setMuted(true/false)` +- `release()` + +补充接口(Demo 未覆盖): + +- `setRenderView(view)`:手动指定渲染 View +- `seekBy(deltaMs)`:播放进度跳转(仅在流支持快进/回放时有效) + +### 6.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` + +### 6.5 播放 API 速览(含 Demo 未覆盖) + +创建与渲染: + +- `initWithStreamId(context, streamId, liveMode, vhost, appName)`:使用 streamId 创建播放器 +- `initWithUrl(context, url)`:使用完整 URL 创建播放器 +- `attachRenderView(container)` / `setRenderView(view)`:设置渲染 View +- `getRenderView()`:获取当前渲染 View + +播放控制: + +- `prepareToPlay()` / `play()` / `pause()` / `stop()`:播放流程控制 +- `seekBy(deltaMs)`:播放进度跳转(流支持回放时有效) +- `isPlaying()`:查询播放状态 +- `setMuted(true/false)`:静音播放 + +统计与释放: + +- `setStatsListener { snapshot -> }`:播放统计回调 +- `release()`:释放播放器资源 + +--- + +## 7. 错误处理与重试建议 + +### Token 错误 + +1. 停止当前推 / 拉 +2. 获取新 Token +3. 重新设置 Token +4. 重新开始推流 / 拉流流程 + +### 网络错误 + +- 监听 `onStatisticsUpdate` 或播放器状态 +- 弱网时适当降低分辨率 / 码率 +- 必要时重启连接 + +--- + +## 8. 最佳实践 + +- 推流前先完成采集预览 +- Token 即将过期前提前刷新 +- 使用统计回调做质量监控 +- 拉流失败避免无限重试 + +--- + +## 9. 常见问题(FAQ) + +### Q1:Token 可以拼接到 URL 吗? +**A:** 不可以。 +SDK 不解析 URL 中的鉴权信息,所有鉴权均通过 `token` 属性完成。 + +### Q2:运行中修改 Token 是否生效? +**A:** +运行中修改 Token **不会影响当前已建立的连接**。 +**下次重连或重新启动推流 / 拉流时会使用新的 Token**。 + +### Q3:播放器出现黑屏怎么办? +**A:** 可按以下步骤排查: + +- 检查播放地址是否正确 +- 确认当前网络连接正常 +- 查看播放器回调中的错误信息 +- 确认视频流格式是否被 SDK 支持 diff --git a/docs/SellySDK_音视频通话接入文档_Android.md b/docs/SellySDK_音视频通话接入文档_Android.md new file mode 100644 index 0000000..df5d9e6 --- /dev/null +++ b/docs/SellySDK_音视频通话接入文档_Android.md @@ -0,0 +1,375 @@ +# SellyRTC Android SDK 接入文档 + +本文档用于指导 **Android App 开发者** 快速接入 SellyRTC,完成一对一或多人实时音视频通话能力。 + +SDK 核心以 `InteractiveRtcEngine` 为中心,通过 `InteractiveRtcEngineEventHandler` 回调通话状态、用户事件、音视频状态及异常。 + +--- + +## 目录 + +1. 准备工作 +2. 快速开始 +3. 基础通话流程 +4. 常用功能 +5. 屏幕分享 +6. 视频帧前后处理 +7. 事件回调(EventHandler) +8. 通话统计 +9. Token 机制 +10. 常见问题(FAQ) + +--- + +## 准备工作 + +### 1. 环境要求 +- Android 8.0+(Demo `minSdk` 为 26) +- 需真机运行(摄像头 / 麦克风) + +### 2. 权限配置(AndroidManifest.xml) + +```xml + + +``` + +--- + +## 快速开始 + +### 1. 创建引擎 + +```kotlin +val appId = getString(R.string.signaling_app_id) +val token = getString(R.string.signaling_token).takeIf { it.isNotBlank() } +val kiwiRsName = getString(R.string.signaling_kiwi_rsname).trim() + +val rtcEngine = InteractiveRtcEngine.create( + InteractiveRtcEngineConfig( + context = applicationContext, + appId = appId, + defaultToken = token, + kiwiRsName = kiwiRsName + ) +).apply { + setEventHandler(eventHandler) + setClientRole(InteractiveRtcEngine.ClientRole.BROADCASTER) + setVideoEncoderConfiguration( + InteractiveVideoEncoderConfig( + width = 640, + height = 480, + fps = 20, + minBitrateKbps = 150, + maxBitrateKbps = 850 + ) + ) + setDefaultAudioRoutetoSpeakerphone(true) +} +``` + +> `InteractiveRtcEngineConfig` 与默认 token 配置见 `example/src/main/java/com/demo/SellyCloudSDK/interactive/InteractiveLiveActivity.kt`。 + +### 2. 设置本地/远端画布 + +```kotlin +val localRenderer = SurfaceViewRenderer(this) +rtcEngine.setupLocalVideo(InteractiveVideoCanvas(localRenderer, userId)) +``` + +```kotlin +val remoteRenderer = SurfaceViewRenderer(this) +rtcEngine.setupRemoteVideo(InteractiveVideoCanvas(remoteRenderer, remoteUserId)) +``` + +### 3. 加入通话 + +```kotlin +val options = InteractiveChannelMediaOptions(callType = CallType.ONE_TO_ONE) +rtcEngine.joinChannel( + token = token, + channel = callId, + userId = userId, + options = options, + tokenSecret = tokenSecret, + tokenExpiresAtSec = tokenExpiresAtSec, + tokenTtlSeconds = tokenTtlSeconds +) +``` + +结束通话: + +```kotlin +rtcEngine.leaveChannel() +``` + +销毁引擎: + +```kotlin +InteractiveRtcEngine.destroy(rtcEngine) +``` + +### 4. 进阶配置(Demo 未覆盖) + +#### 4.1 InteractiveRtcEngineConfig 高级字段 + +```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 +val options = InteractiveChannelMediaOptions( + callType = CallType.GROUP, + autoSubscribeAudio = true, + autoSubscribeVideo = false +) +``` + +#### 4.3 InteractiveVideoEncoderConfig 更多参数 + +可选项(按需设置): + +- `codecType`:编码类型 +- `bitrateMode`:码率控制模式 +- `minBitrateKbps` / `maxBitrateKbps`:码率范围 +- `orientationMode`:画面方向策略 +- `degradationPreference`:弱网降级策略 +- `mirrorMode`:镜像策略 + +--- + +## 基础通话流程 + +1. 创建 `InteractiveRtcEngine` +2. 设置 `EventHandler` +3. 配置 `InteractiveVideoEncoderConfig` +4. 设置本地画布 `setupLocalVideo` +5. `joinChannel` 加入频道 +6. `onUserJoined` 后设置远端画布 +7. 通话中进行音视频控制 +8. `leaveChannel` 并释放资源 + +--- + +## 常用功能 + +### 本地音视频控制 + +```kotlin +rtcEngine.enableLocalVideo(true) +rtcEngine.enableLocalAudio(false) +``` + +### 切换摄像头 + +```kotlin +rtcEngine.switchCamera() +``` + +### 音频输出(扬声器 / 听筒) + +```kotlin +rtcEngine.setDefaultAudioRoutetoSpeakerphone(true) +``` + +### 发送自定义消息 + +```kotlin +rtcEngine.sendMessage("hello") { error -> + // error == null 表示成功 +} +``` + +### 按用户静音远端音频/视频 + +```kotlin +rtcEngine.muteRemoteAudioStream(remoteUserId, true) +rtcEngine.muteRemoteVideoStream(remoteUserId, true) +``` + +### 全量静音远端音视频(Demo 未覆盖) + +```kotlin +rtcEngine.muteAllRemoteAudioStreams(true) +rtcEngine.muteAllRemoteVideoStreams(true) +``` + +### 清理远端画布(Demo 未覆盖) + +```kotlin +rtcEngine.clearRemoteVideo(remoteUserId) +``` + +### 向指定用户发送消息(Demo 未覆盖) + +```kotlin +rtcEngine.sendMessage("hello", targetUserId) { error -> + // error == null 表示成功 +} +``` + +--- + +## 屏幕分享 + +```kotlin +val started = rtcEngine.startScreenShare( + resultCode = resultCode, + data = data, + width = 720, + height = 1280, + fps = 15 +) +``` + +```kotlin +rtcEngine.stopScreenShare() +``` + +```kotlin +val isSharing = rtcEngine.isScreenSharing() +``` + +> Android 14+ 屏幕捕获需要前台服务(Demo 使用 `InteractiveForegroundService` 处理)。 + +--- + +## 视频帧前后处理 + +```kotlin +rtcEngine.setCaptureVideoFrameInterceptor { frame -> + // 在此处理美颜/滤镜,返回新的 frame + frame +} +``` + +```kotlin +rtcEngine.setRenderVideoFrameInterceptor { frame, userId -> + // 远端渲染前处理,返回 true 表示继续渲染 + true +} +``` + +> Demo 中的美颜示例见: +> `example/src/main/java/com/demo/SellyCloudSDK/beauty/FuVideoFrameInterceptor.kt` + +--- + +## 事件回调(EventHandler) + +```kotlin +val eventHandler = 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 onMessageReceived(message: String, userId: String?) {} + override fun onError(code: String, message: String) {} + override fun onTokenWillExpire(token: String?, expiresAt: Long) {} + override fun onTokenExpired(token: String?, expiresAt: Long) {} + override fun onLocalVideoStats(stats: InteractiveStreamStats) {} + override fun onRemoteVideoStats(stats: InteractiveStreamStats) {} + 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?) {} +} +``` + +--- + +## 通话统计 + +`InteractiveStreamStats` 常用字段: + +- width / height +- fps +- videoBitrateKbps / audioBitrateKbps +- rttMs +- videoCodec / audioCodec + +--- + +## Token 机制 + +### Token 来源 + +- 业务侧服务端生成 Token 并下发 +- Demo 可在 `strings.xml` 配置 `signaling_token` 或 `signaling_secret` 进行本地生成 + +### Token 过期处理 + +- `onTokenWillExpire` / `onTokenExpired` 回调提示即将过期或已过期 +- 建议获取新 Token 并 **重新 joinChannel** + +Token 续期接口(Demo 未覆盖): + +```kotlin +rtcEngine.renewToken(newToken, expiresAtSec) +``` + +--- + +## 更多 API 速览(含 Demo 未覆盖) + +引擎创建与销毁: + +- `InteractiveRtcEngine.create(config)`:创建引擎 +- `InteractiveRtcEngine.destroy(engine)` / `engine.destroy()`:释放引擎 + +通话控制: + +- `setEventHandler(handler)`:设置事件回调 +- `setClientRole(role)`:设置角色(主播/观众) +- `setVideoEncoderConfiguration(config)`:设置编码参数 +- `setDefaultAudioRoutetoSpeakerphone(true/false)`:设置音频输出 +- `joinChannel(...)` / `leaveChannel()`:加入 / 离开频道 + +本地与远端控制: + +- `setupLocalVideo(canvas)` / `setupRemoteVideo(canvas)`:设置画布 +- `clearRemoteVideo(userId)`:清理远端画面 +- `enableLocalVideo(true/false)` / `enableLocalAudio(true/false)`:开关本地音视频 +- `muteRemoteAudioStream(userId, true/false)` / `muteRemoteVideoStream(userId, true/false)`:按用户静音 +- `muteAllRemoteAudioStreams(true/false)` / `muteAllRemoteVideoStreams(true/false)`:全量静音 +- `switchCamera()`:切换摄像头 + +帧处理与屏幕共享: + +- `setCaptureVideoFrameInterceptor(...)`:采集前帧处理 +- `setRenderVideoFrameInterceptor(...)`:渲染前帧处理 +- `startScreenShare(...)` / `stopScreenShare()` / `isScreenSharing()`:屏幕共享 + +消息与 Token: + +- `sendMessage(message, targetUserId, callback)`:发送消息(`targetUserId` 可为空) +- `renewToken(newToken, expiresAtSec)`:续期 Token + +--- + +## 常见问题(FAQ) + +### Q:远端画面不显示? +1. 是否设置了正确的 `remoteUserId` +2. 是否在 `onUserJoined` 后调用 `setupRemoteVideo` +3. 远端是否关闭了视频 + +### Q:加入频道失败? +1. 检查 `signaling_app_id` 是否正确 +2. Token 是否为空或已过期 +3. 网络是否受限 + +### Q:屏幕分享失败? +1. 是否已获取 `MediaProjection` 授权 +2. Android 14+ 是否启动前台服务 diff --git a/example/build.gradle b/example/build.gradle index b75979b..6ac8db9 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -3,12 +3,9 @@ plugins { id 'org.jetbrains.kotlin.android' } -def sdkGroupId = rootProject.findProperty("sellySdkGroupId") ?: "com.sellycloud" -def sdkArtifactId = rootProject.findProperty("sellySdkArtifactId") ?: "sellycloudsdk" -def sdkVersion = rootProject.findProperty("sellySdkVersion") ?: "1.0.0" -def hasLocalSdk = rootProject.file("SellyCloudSDK").exists() +def sdkAarPath = "libs/${findProperty("sellySdkArtifactId") ?: "sellycloudsdk"}-${findProperty("sellySdkVersion") ?: "1.0.0"}.aar" def releaseStorePath = project.rootProject.file(findProperty("MY_STORE_FILE") ?: "release.keystore") -def hasReleaseKeystore = releaseStorePath != null && releaseStorePath.exists() +def hasReleaseKeystore = releaseStorePath.exists() android { namespace 'com.demo.SellyCloudSDK' @@ -67,34 +64,19 @@ android { } dependencies { - if (hasLocalSdk) { - implementation project(':SellyCloudSDK') - } else { - implementation files( - "libs/${sdkArtifactId}-${sdkVersion}.aar", - "libs/ijkplayer-cmake-release.aar", - "libs/Kiwi.aar", - "libs/libwebrtc.aar" - ) - implementation 'com.google.code.gson:gson:2.10.1' - implementation 'com.github.pedroSG94.RootEncoder:library:2.6.6' - } - - implementation fileTree( - dir: "libs", - include: ["*.jar", "*.aar"], - exclude: [ - "${sdkArtifactId}-${sdkVersion}.aar", - "ijkplayer-cmake-release.aar", - "Kiwi.aar", - "libwebrtc.aar" - ] + implementation files( + sdkAarPath, + "libs/fu_core_all_feature_release.aar", + "libs/fu_model_all_feature_release.aar" ) + implementation fileTree(dir: "libs", include: ["*.jar"]) implementation 'androidx.appcompat:appcompat:1.7.0-alpha03' - implementation 'androidx.constraintlayout:constraintlayout:2.2.0-alpha13' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3' implementation 'androidx.core:core-ktx:1.13.1' + implementation "com.squareup.okhttp3:okhttp:4.12.0" + + implementation 'androidx.constraintlayout:constraintlayout:2.2.0-alpha13' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.activity:activity-ktx:1.9.2' @@ -102,7 +84,6 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.4' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.recyclerview:recyclerview:1.3.2' - implementation "com.squareup.okhttp3:okhttp:4.12.0" implementation "io.coil-kt:coil:2.6.0" testImplementation 'junit:junit:4.13.2' diff --git a/example/libs/Kiwi.aar b/example/libs/Kiwi.aar deleted file mode 100644 index 63042fe..0000000 Binary files a/example/libs/Kiwi.aar and /dev/null differ diff --git a/example/libs/ijkplayer-cmake-release.aar b/example/libs/ijkplayer-cmake-release.aar deleted file mode 100644 index 88e3792..0000000 Binary files a/example/libs/ijkplayer-cmake-release.aar and /dev/null differ diff --git a/example/libs/libwebrtc.aar b/example/libs/libwebrtc.aar deleted file mode 100644 index f2df838..0000000 Binary files a/example/libs/libwebrtc.aar and /dev/null differ diff --git a/example/libs/sellycloudsdk-1.0.0.aar b/example/libs/sellycloudsdk-1.0.0.aar index 51f2925..1a9471c 100644 Binary files a/example/libs/sellycloudsdk-1.0.0.aar and b/example/libs/sellycloudsdk-1.0.0.aar differ diff --git a/example/src/main/java/com/demo/SellyCloudSDK/interactive/InteractiveLiveActivity.kt b/example/src/main/java/com/demo/SellyCloudSDK/interactive/InteractiveLiveActivity.kt index 776ab03..e1dfd11 100644 --- a/example/src/main/java/com/demo/SellyCloudSDK/interactive/InteractiveLiveActivity.kt +++ b/example/src/main/java/com/demo/SellyCloudSDK/interactive/InteractiveLiveActivity.kt @@ -173,7 +173,7 @@ class InteractiveLiveActivity : AppCompatActivity() { val token = getString(R.string.signaling_token).takeIf { it.isNotBlank() } val kiwiRsName = getString(R.string.signaling_kiwi_rsname).trim() beautyRenderer = FURenderer(this).also { it.setup() } - fuFrameInterceptor = beautyRenderer?.let { FuVideoFrameInterceptor(it).apply { + fuFrameInterceptor = beautyRenderer?.let { FuVideoFrameInterceptor(it).apply { setFrontCamera(isFrontCamera) setEnabled(beautyEnabled) } } diff --git a/example/src/main/java/com/demo/SellyCloudSDK/live/LivePlayActivity.kt b/example/src/main/java/com/demo/SellyCloudSDK/live/LivePlayActivity.kt index 133aca9..5cc313d 100644 --- a/example/src/main/java/com/demo/SellyCloudSDK/live/LivePlayActivity.kt +++ b/example/src/main/java/com/demo/SellyCloudSDK/live/LivePlayActivity.kt @@ -125,14 +125,6 @@ class LivePlayActivity : AppCompatActivity() { } updatePreviewVisibility() logEvent("状态变更: ${formatState(state)}") - if (state == SellyPlayerState.Playing && firstVideoFrameElapsedMs == null) { - val startMs = playAttemptStartElapsedMs - firstVideoFrameElapsedMs = SystemClock.elapsedRealtime() - if (startMs != null) { - firstVideoFrameCostMs = firstVideoFrameElapsedMs!! - startMs - logEvent("首帧视频耗时=${firstVideoFrameCostMs}ms") - } - } } } @@ -140,6 +132,11 @@ class LivePlayActivity : AppCompatActivity() { runOnUiThread { hasFirstVideoFrameRendered = true updatePreviewVisibility() + if (firstVideoFrameElapsedMs != null) return@runOnUiThread + val startMs = playAttemptStartElapsedMs ?: return@runOnUiThread + firstVideoFrameElapsedMs = SystemClock.elapsedRealtime() + firstVideoFrameCostMs = firstVideoFrameElapsedMs!! - startMs + logEvent("首帧视频耗时=${firstVideoFrameCostMs}ms") } } diff --git a/example/src/main/java/com/demo/SellyCloudSDK/live/LivePushActivity.kt b/example/src/main/java/com/demo/SellyCloudSDK/live/LivePushActivity.kt index cbb189f..947eb82 100644 --- a/example/src/main/java/com/demo/SellyCloudSDK/live/LivePushActivity.kt +++ b/example/src/main/java/com/demo/SellyCloudSDK/live/LivePushActivity.kt @@ -220,8 +220,10 @@ class LivePushActivity : AppCompatActivity() { try { val videoConfig = buildVideoConfig(settings) + pusherClient.setVideoConfiguration(videoConfig) pusherClient.attachPreview(binding.previewContainer) pusherClient.startRunning(currentFacing, videoConfig, null) + applyStreamConfig(settings) } catch (t: Throwable) { Toast.makeText(this, "初始化预览失败: ${t.message}", Toast.LENGTH_LONG).show() } diff --git a/gradle.properties b/gradle.properties index 84ed79a..edb550f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,6 @@ android.useAndroidX=true android.enableJetifier=false +android.experimental.fusedLibrarySupport=true # Increase Gradle daemon heap to avoid OOM during packaging large AAR/assets org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g -Dkotlin.daemon.jvm.options="-Xmx2g" diff --git a/settings.gradle b/settings.gradle index 7a48d7e..270d4e9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,9 +6,6 @@ pluginManagement { } } -def sdkDir = file('SellyCloudSDK') -def hasLocalSdk = sdkDir.exists() - dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS) repositories { @@ -16,11 +13,8 @@ dependencyResolutionManagement { google() mavenCentral() maven { url 'https://jitpack.io' } - // Local AARs for the demo or the SDK when present + // Local AARs for the demo flatDir { - if (hasLocalSdk) { - dirs sdkDir.toPath().resolve('libs').toFile() - } dirs file('example/libs') } } @@ -28,7 +22,3 @@ dependencyResolutionManagement { rootProject.name = "SellyCLoudSDKExample" include ':example' -// Use the SDK module only when it exists locally; otherwise rely on the published/prebuilt AAR. -if (hasLocalSdk) { - include ':SellyCloudSDK' -}