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

@@ -0,0 +1,8 @@
package com.omixlab.lckcontrol.shared;
interface ILckStreamingCallback {
oneway void onBufferReleased(int bufferIndex);
oneway void onStreamingStateChanged(String state);
oneway void onStreamingError(int code, String message);
oneway void onStreamingStats(long videoBitrate, long audioBitrate, int fps, int droppedFrames);
}

View File

@@ -0,0 +1,22 @@
package com.omixlab.lckcontrol.shared;
import android.hardware.HardwareBuffer;
import android.os.ParcelFileDescriptor;
import com.omixlab.lckcontrol.shared.ILckStreamingCallback;
interface ILckStreamingService {
// Texture pool (game allocates, app receives)
void registerTexturePool(in HardwareBuffer[] buffers, int width, int height, int format);
void unregisterTexturePool();
// Frame submission (game -> app, one-way for performance)
oneway void submitVideoFrame(int bufferIndex, long timestampNs, in ParcelFileDescriptor gpuFence);
oneway void submitAudioFrame(in byte[] pcmData, long timestampNs, int sampleRate, int channels, int bitsPerSample);
// Streaming lifecycle
boolean isStreaming();
// Callbacks
void registerStreamingCallback(ILckStreamingCallback callback);
void unregisterStreamingCallback(ILckStreamingCallback callback);
}

View File

@@ -0,0 +1,3 @@
package com.omixlab.lckcontrol.shared;
parcelable StreamingConfig;

View File

@@ -10,6 +10,7 @@ data class LinkedAccount(
val accountId: String,
val avatarUrl: String? = null,
val isAuthenticated: Boolean = false,
val isEnabled: Boolean = true,
) : Parcelable {
constructor(parcel: Parcel) : this(
@@ -19,6 +20,7 @@ data class LinkedAccount(
accountId = parcel.readString()!!,
avatarUrl = parcel.readString(),
isAuthenticated = parcel.readInt() != 0,
isEnabled = if (parcel.dataAvail() > 0) parcel.readInt() != 0 else true,
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
@@ -28,6 +30,7 @@ data class LinkedAccount(
parcel.writeString(accountId)
parcel.writeString(avatarUrl)
parcel.writeInt(if (isAuthenticated) 1 else 0)
parcel.writeInt(if (isEnabled) 1 else 0)
}
override fun describeContents(): Int = 0

View File

@@ -8,6 +8,8 @@ data class StreamPlan(
val name: String,
val status: String = "DRAFT",
val destinations: List<StreamDestination> = emptyList(),
val executionMode: String = "IN_GAME",
val gameId: String = "",
) : Parcelable {
constructor(parcel: Parcel) : this(
@@ -15,6 +17,8 @@ data class StreamPlan(
name = parcel.readString()!!,
status = parcel.readString() ?: "DRAFT",
destinations = parcel.createTypedArrayList(StreamDestination.CREATOR) ?: emptyList(),
executionMode = if (parcel.dataAvail() > 0) parcel.readString() ?: "IN_GAME" else "IN_GAME",
gameId = if (parcel.dataAvail() > 0) parcel.readString() ?: "" else "",
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
@@ -22,6 +26,8 @@ data class StreamPlan(
parcel.writeString(name)
parcel.writeString(status)
parcel.writeTypedList(destinations)
parcel.writeString(executionMode)
parcel.writeString(gameId)
}
override fun describeContents(): Int = 0

View File

@@ -6,16 +6,22 @@ import android.os.Parcelable
data class StreamPlanConfig(
val name: String,
val destinations: List<StreamDestination> = emptyList(),
val executionMode: String = "IN_GAME",
val gameId: String = "",
) : Parcelable {
constructor(parcel: Parcel) : this(
name = parcel.readString()!!,
destinations = parcel.createTypedArrayList(StreamDestination.CREATOR) ?: emptyList(),
executionMode = if (parcel.dataAvail() > 0) parcel.readString() ?: "IN_GAME" else "IN_GAME",
gameId = if (parcel.dataAvail() > 0) parcel.readString() ?: "" else "",
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(name)
parcel.writeTypedList(destinations)
parcel.writeString(executionMode)
parcel.writeString(gameId)
}
override fun describeContents(): Int = 0

View File

@@ -0,0 +1,39 @@
package com.omixlab.lckcontrol.shared
import android.os.Parcel
import android.os.Parcelable
data class StreamingConfig(
val videoBitrate: Int = 6_000_000,
val videoCodec: String = "h264",
val audioBitrate: Int = 128_000,
val audioSampleRate: Int = 48_000,
val audioChannels: Int = 2,
val keyFrameInterval: Int = 2,
) : Parcelable {
constructor(parcel: Parcel) : this(
videoBitrate = parcel.readInt(),
videoCodec = parcel.readString() ?: "h264",
audioBitrate = parcel.readInt(),
audioSampleRate = parcel.readInt(),
audioChannels = parcel.readInt(),
keyFrameInterval = parcel.readInt(),
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(videoBitrate)
parcel.writeString(videoCodec)
parcel.writeInt(audioBitrate)
parcel.writeInt(audioSampleRate)
parcel.writeInt(audioChannels)
parcel.writeInt(keyFrameInterval)
}
override fun describeContents(): Int = 0
companion object CREATOR : Parcelable.Creator<StreamingConfig> {
override fun createFromParcel(parcel: Parcel) = StreamingConfig(parcel)
override fun newArray(size: Int) = arrayOfNulls<StreamingConfig>(size)
}
}