App streaming pipeline, dashboard server status, account enable/disable, game-linked plans
- Add C++ native streaming engine (RTMP client, EGL context, streaming engine, JNI bridge) - Add pre-built arm64-v8a libs (librtmp, libssl, libcrypto, libz) and headers - Add Kotlin streaming layer (NativeStreamingEngine, StreamingManager, StreamingStats) - Add AIDL streaming interface (ILckStreamingService, ILckStreamingCallback, StreamingConfig) - Add LckStreamingServiceImpl with BIND_STREAMING action support - Add APP_STREAMING execution mode with auto-start/stop on plan lifecycle - SDK: add bindStreaming(), submitVideoFrame(), submitAudioFrame() to LckControlClient - Dashboard: replace linked accounts with server status card, move health polling from nav - Remove health check dot overlay from Dashboard nav icon - Accounts: add enable/disable toggle per account (persists locally, excluded from default plans) - Plans: add gameId field linked to game package ID, resolved from ClientTracker for default plans - Service: pass executionMode+gameId through createStreamPlan, filter enabled accounts in createDefaultPlan - Room DB migration 4→5: add isEnabled column to linked_accounts, gameId column to stream_plans - Add docs (hub vs control comparison)
This commit is contained in:
@@ -4,10 +4,14 @@ import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.hardware.HardwareBuffer
|
||||
import android.os.IBinder
|
||||
import android.os.ParcelFileDescriptor
|
||||
import com.omixlab.lckcontrol.shared.ConnectedClientInfo
|
||||
import com.omixlab.lckcontrol.shared.ILckControlCallback
|
||||
import com.omixlab.lckcontrol.shared.ILckControlService
|
||||
import com.omixlab.lckcontrol.shared.ILckStreamingCallback
|
||||
import com.omixlab.lckcontrol.shared.ILckStreamingService
|
||||
import com.omixlab.lckcontrol.shared.LinkedAccount
|
||||
import com.omixlab.lckcontrol.shared.StreamPlan
|
||||
import com.omixlab.lckcontrol.shared.StreamPlanConfig
|
||||
@@ -21,9 +25,11 @@ class LckControlClient(private val context: Context) {
|
||||
private const val SERVICE_PACKAGE = "com.omixlab.lckcontrol"
|
||||
private const val SERVICE_CLASS = "$SERVICE_PACKAGE.service.LckControlService"
|
||||
private const val PERMISSION = "$SERVICE_PACKAGE.permission.USE_LCK_CONTROL"
|
||||
private const val ACTION_BIND_STREAMING = "$SERVICE_PACKAGE.BIND_STREAMING"
|
||||
}
|
||||
|
||||
private var service: ILckControlService? = null
|
||||
private var streamingService: ILckStreamingService? = null
|
||||
private var clientId: String? = null
|
||||
|
||||
private val _connected = MutableStateFlow(false)
|
||||
@@ -35,6 +41,12 @@ class LckControlClient(private val context: Context) {
|
||||
private val _streamPlans = MutableStateFlow<List<StreamPlan>>(emptyList())
|
||||
val streamPlans: StateFlow<List<StreamPlan>> = _streamPlans.asStateFlow()
|
||||
|
||||
private val _streamingState = MutableStateFlow("IDLE")
|
||||
val streamingState: StateFlow<String> = _streamingState.asStateFlow()
|
||||
|
||||
private val _streamingConnected = MutableStateFlow(false)
|
||||
val streamingConnected: StateFlow<Boolean> = _streamingConnected.asStateFlow()
|
||||
|
||||
private val callback = object : ILckControlCallback.Stub() {
|
||||
override fun onStreamPlansChanged(plans: List<StreamPlan>) {
|
||||
_streamPlans.value = plans
|
||||
@@ -54,6 +66,33 @@ class LckControlClient(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
private val streamingCallback = object : ILckStreamingCallback.Stub() {
|
||||
override fun onBufferReleased(bufferIndex: Int) {
|
||||
onBufferReleasedListener?.invoke(bufferIndex)
|
||||
}
|
||||
|
||||
override fun onStreamingStateChanged(state: String) {
|
||||
_streamingState.value = state
|
||||
}
|
||||
|
||||
override fun onStreamingError(code: Int, message: String) {
|
||||
onStreamingErrorListener?.invoke(code, message)
|
||||
}
|
||||
|
||||
override fun onStreamingStats(videoBitrate: Long, audioBitrate: Long, fps: Int, droppedFrames: Int) {
|
||||
onStreamingStatsListener?.invoke(videoBitrate, audioBitrate, fps, droppedFrames)
|
||||
}
|
||||
}
|
||||
|
||||
/** Listener for buffer release events (game can reuse the buffer). */
|
||||
var onBufferReleasedListener: ((Int) -> Unit)? = null
|
||||
|
||||
/** Listener for streaming errors. */
|
||||
var onStreamingErrorListener: ((Int, String) -> Unit)? = null
|
||||
|
||||
/** Listener for streaming stats updates. */
|
||||
var onStreamingStatsListener: ((Long, Long, Int, Int) -> Unit)? = null
|
||||
|
||||
private val connection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
|
||||
service = ILckControlService.Stub.asInterface(binder)
|
||||
@@ -70,6 +109,20 @@ class LckControlClient(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
private val streamingConnection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
|
||||
streamingService = ILckStreamingService.Stub.asInterface(binder)
|
||||
streamingService?.registerStreamingCallback(streamingCallback)
|
||||
_streamingConnected.value = true
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
streamingService = null
|
||||
_streamingConnected.value = false
|
||||
_streamingState.value = "IDLE"
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(): Boolean {
|
||||
val intent = Intent().apply {
|
||||
component = ComponentName(SERVICE_PACKAGE, SERVICE_CLASS)
|
||||
@@ -93,6 +146,51 @@ class LckControlClient(private val context: Context) {
|
||||
_authenticated.value = false
|
||||
}
|
||||
|
||||
// ── Streaming service ────────────────────────────────
|
||||
|
||||
fun bindStreaming(): Boolean {
|
||||
val intent = Intent(ACTION_BIND_STREAMING).apply {
|
||||
component = ComponentName(SERVICE_PACKAGE, SERVICE_CLASS)
|
||||
}
|
||||
return context.bindService(intent, streamingConnection, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
|
||||
fun unbindStreaming() {
|
||||
streamingService?.let { svc ->
|
||||
svc.unregisterStreamingCallback(streamingCallback)
|
||||
}
|
||||
try {
|
||||
context.unbindService(streamingConnection)
|
||||
} catch (_: IllegalArgumentException) {}
|
||||
streamingService = null
|
||||
_streamingConnected.value = false
|
||||
_streamingState.value = "IDLE"
|
||||
}
|
||||
|
||||
// ── Texture pool ─────────────────────────────────────
|
||||
|
||||
fun registerTexturePool(buffers: Array<HardwareBuffer>, width: Int, height: Int, format: Int) {
|
||||
streamingService?.registerTexturePool(buffers, width, height, format)
|
||||
}
|
||||
|
||||
fun unregisterTexturePool() {
|
||||
streamingService?.unregisterTexturePool()
|
||||
}
|
||||
|
||||
// ── Frame submission (called from game render thread) ──
|
||||
|
||||
fun submitVideoFrame(bufferIndex: Int, timestampNs: Long, gpuFenceFd: ParcelFileDescriptor?) {
|
||||
streamingService?.submitVideoFrame(bufferIndex, timestampNs, gpuFenceFd)
|
||||
}
|
||||
|
||||
fun submitAudioFrame(pcmData: ByteArray, timestampNs: Long, sampleRate: Int, channels: Int, bitsPerSample: Int) {
|
||||
streamingService?.submitAudioFrame(pcmData, timestampNs, sampleRate, channels, bitsPerSample)
|
||||
}
|
||||
|
||||
fun isStreaming(): Boolean {
|
||||
return streamingService?.isStreaming ?: false
|
||||
}
|
||||
|
||||
// ── Auth ────────────────────────────────────────────
|
||||
|
||||
fun isAuthenticated(): Boolean {
|
||||
|
||||
Reference in New Issue
Block a user