添加游戏盾代理支持
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
|
||||
<application
|
||||
android:name=".DemoApplication"
|
||||
android:allowBackup="true"
|
||||
android:label="SellyCloudRTC Demo"
|
||||
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.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.demo.SellyCloudSDK.KiwiHelper
|
||||
import com.demo.SellyCloudSDK.R
|
||||
import kotlinx.coroutines.launch
|
||||
import com.demo.SellyCloudSDK.beauty.FURenderer
|
||||
import com.demo.SellyCloudSDK.beauty.FuVideoFrameInterceptor
|
||||
import com.demo.SellyCloudSDK.databinding.ActivityInteractiveLiveBinding
|
||||
@@ -182,7 +185,10 @@ class InteractiveLiveActivity : AppCompatActivity() {
|
||||
private fun initRtcEngine() {
|
||||
val appId = getString(R.string.signaling_app_id)
|
||||
val token = getString(R.string.signaling_token).takeIf { it.isNotBlank() }
|
||||
// Kiwi 代理后台获取,rsName 为空时清除残留
|
||||
val kiwiRsName = getString(R.string.signaling_kiwi_rsname).trim()
|
||||
KiwiHelper.startProxySetup(kiwiRsName.isNotBlank(), kiwiRsName)
|
||||
|
||||
beautyRenderer = FURenderer(this).also { it.setup() }
|
||||
fuFrameInterceptor = beautyRenderer?.let { FuVideoFrameInterceptor(it).apply {
|
||||
setFrontCamera(isFrontCamera)
|
||||
@@ -192,8 +198,7 @@ class InteractiveLiveActivity : AppCompatActivity() {
|
||||
InteractiveRtcEngineConfig(
|
||||
context = applicationContext,
|
||||
appId = appId,
|
||||
defaultToken = token,
|
||||
kiwiRsName = kiwiRsName
|
||||
defaultToken = token
|
||||
)
|
||||
).apply {
|
||||
setEventHandler(rtcEventHandler)
|
||||
@@ -596,6 +601,15 @@ class InteractiveLiveActivity : AppCompatActivity() {
|
||||
private fun executeJoin(request: JoinRequest) {
|
||||
pendingJoinRequest = null
|
||||
InteractiveForegroundService.start(this)
|
||||
// 立即禁用按钮,防止 await 期间重复点击
|
||||
setJoinButtonEnabled(false)
|
||||
lifecycleScope.launch {
|
||||
KiwiHelper.awaitProxyReady()
|
||||
executeJoinInternal(request)
|
||||
}
|
||||
}
|
||||
|
||||
private fun executeJoinInternal(request: JoinRequest) {
|
||||
val renderer = localRenderer ?: createRenderer().also {
|
||||
localRenderer = it
|
||||
}
|
||||
|
||||
@@ -27,9 +27,11 @@ import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import coil.load
|
||||
import com.demo.SellyCloudSDK.KiwiHelper
|
||||
import com.demo.SellyCloudSDK.R
|
||||
import com.demo.SellyCloudSDK.databinding.ActivityLivePlayBinding
|
||||
import com.demo.SellyCloudSDK.live.auth.LiveAuthHelper
|
||||
@@ -209,7 +211,10 @@ class LivePlayActivity : AppCompatActivity() {
|
||||
playerClient.attachRenderView(binding.renderContainer)
|
||||
|
||||
if (args.autoStart) {
|
||||
startPlayback()
|
||||
lifecycleScope.launch {
|
||||
KiwiHelper.awaitProxyReady()
|
||||
startPlayback()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,7 +263,10 @@ class LivePlayActivity : AppCompatActivity() {
|
||||
if (currentState == SellyPlayerState.Paused) {
|
||||
playerClient.play()
|
||||
} else {
|
||||
startPlayback()
|
||||
lifecycleScope.launch {
|
||||
KiwiHelper.awaitProxyReady()
|
||||
startPlayback()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,9 +20,11 @@ import android.view.WindowManager
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.demo.SellyCloudSDK.KiwiHelper
|
||||
import com.demo.SellyCloudSDK.avdemo.AvDemoSettings
|
||||
import com.demo.SellyCloudSDK.avdemo.AvDemoSettingsStore
|
||||
import com.demo.SellyCloudSDK.beauty.FaceUnityBeautyEngine
|
||||
@@ -129,31 +131,34 @@ class LivePushActivity : AppCompatActivity() {
|
||||
if (isPublishing) {
|
||||
pusher.stopLive()
|
||||
} else {
|
||||
val settings = settingsStore.read()
|
||||
applyStreamConfig(settings)
|
||||
pusher.setXorKey(settings.xorKeyHex)
|
||||
if (settings.useUrlMode) {
|
||||
val pushUrl = settings.pushUrl
|
||||
if (pushUrl.isEmpty()) {
|
||||
Toast.makeText(this, "请先在设置中输入推流地址", Toast.LENGTH_SHORT).show()
|
||||
return@setOnClickListener
|
||||
lifecycleScope.launch {
|
||||
KiwiHelper.awaitProxyReady()
|
||||
val settings = settingsStore.read()
|
||||
applyStreamConfig(settings)
|
||||
pusher.setXorKey(settings.xorKeyHex)
|
||||
if (settings.useUrlMode) {
|
||||
val pushUrl = settings.pushUrl
|
||||
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.AppCompatActivity
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.demo.SellyCloudSDK.KiwiHelper
|
||||
import com.demo.SellyCloudSDK.R
|
||||
import kotlinx.coroutines.launch
|
||||
import com.demo.SellyCloudSDK.databinding.ActivityPkPlayBinding
|
||||
import com.demo.SellyCloudSDK.live.auth.LiveAuthHelper
|
||||
import com.demo.SellyCloudSDK.live.auth.LiveTokenSigner
|
||||
@@ -184,7 +187,10 @@ class PkPlayActivity : AppCompatActivity() {
|
||||
binding.actionMute.setOnClickListener { toggleMute() }
|
||||
|
||||
if (args.autoStart) {
|
||||
startPlayback()
|
||||
lifecycleScope.launch {
|
||||
KiwiHelper.awaitProxyReady()
|
||||
startPlayback()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,7 +377,10 @@ class PkPlayActivity : AppCompatActivity() {
|
||||
if (mainPaused) mainPlayer.play()
|
||||
if (pkPaused) pkPlayer.play()
|
||||
} else {
|
||||
startPlayback()
|
||||
lifecycleScope.launch {
|
||||
KiwiHelper.awaitProxyReady()
|
||||
startPlayback()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package com.demo.SellyCloudSDK.live.env
|
||||
|
||||
import android.content.Context
|
||||
import com.demo.SellyCloudSDK.KiwiHelper
|
||||
import com.sellycloud.sellycloudsdk.SellyCloudConfig
|
||||
import com.sellycloud.sellycloudsdk.SellyCloudManager
|
||||
import com.sellycloud.sellycloudsdk.SellyLiveMode
|
||||
|
||||
fun LiveEnvSettings.applyToSdkRuntimeConfig(context: Context) {
|
||||
// 1. SDK 初始化(同步,轻量)
|
||||
SellyCloudManager.initialize(
|
||||
context = context,
|
||||
appId = appId,
|
||||
@@ -15,12 +17,13 @@ fun LiveEnvSettings.applyToSdkRuntimeConfig(context: Context) {
|
||||
vhost = normalizedVhost(),
|
||||
vhostKey = vhostKey,
|
||||
defaultStreamId = defaultStreamId,
|
||||
enableKiwi = enableKiwi,
|
||||
kiwiRsName = kiwiRsName,
|
||||
logEnabled = logEnabled,
|
||||
defaultLiveMode = protocol.toLiveMode()
|
||||
)
|
||||
)
|
||||
// 2. 启动代理获取:内部受控 scope、cancel 旧 Job
|
||||
// 不阻塞主线程,关键 start 点通过 awaitProxyReady() 保证就绪
|
||||
KiwiHelper.startProxySetup(enableKiwi, kiwiRsName)
|
||||
}
|
||||
|
||||
fun LiveEnvSettings.normalizedAppName(): String = normalizedAppId()
|
||||
|
||||
Reference in New Issue
Block a user