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:
2026-02-28 20:05:21 +01:00
parent 1480a2944b
commit 097cd24ea9
59 changed files with 13609 additions and 89 deletions

View File

@@ -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 {