Fix streaming pipeline: timestamps, buffer release, resolution, orientation

- Fix encoder PTS: use wall-clock relative timestamps to prevent backward
  jumps when transitioning from standby to game frames (MediaCodec drops)
- Suppress standby frames while game is active (500ms timeout) to prevent
  flickering between game video and color bars
- Remove standby color bar pattern, use plain dark background
- Fix vertical flip and BGR→RGB swizzle in composition base pass shader
- Pass buffer index through native pipeline for pool slot release callback
- Start engine for already-LIVE plans when APP_STREAMING mode is active
- Use texture pool dimensions for encoder resolution instead of hardcoded 1920x1080
This commit is contained in:
2026-03-01 14:33:57 +01:00
parent c632e22033
commit ef221ca132
7 changed files with 181 additions and 50 deletions

View File

@@ -155,7 +155,19 @@ class LckControlService : Service() {
override fun startStreamPlan(planId: String): Boolean = runBlocking {
val plan = streamPlanRepository.getPlan(planId) ?: return@runBlocking false
if (plan.status == "LIVE") return@runBlocking true
if (plan.status == "LIVE") {
// Plan already LIVE — ensure streaming engine is running for APP_STREAMING
if (plan.executionMode == "APP_STREAMING" && !streamingManager.isStreaming()) {
Log.d(TAG, "startStreamPlan: plan already LIVE but engine not running, starting engine")
streamingManager.startStreaming(
plan = plan,
config = StreamingConfig(),
width = 1920,
height = 1080,
)
}
return@runBlocking true
}
if (plan.status != "READY") return@runBlocking false
try {
streamPlanRepository.startPlan(planId)
@@ -278,6 +290,11 @@ class LckControlService : Service() {
)
}
}
// Forward buffer release events to AIDL callbacks
streamingManager.onBufferReleased = { bufferIndex ->
streamingServiceImpl?.broadcastBufferReleased(bufferIndex)
}
}
override fun onBind(intent: Intent?): IBinder? {

View File

@@ -51,9 +51,9 @@ class NativeStreamingEngine {
return nativeStart(nativePtr)
}
fun submitVideoFrame(hardwareBuffer: HardwareBuffer, timestampNs: Long, fenceFd: Int) {
fun submitVideoFrame(hardwareBuffer: HardwareBuffer, timestampNs: Long, fenceFd: Int, bufferIndex: Int) {
if (nativePtr == 0L) return
nativeSubmitVideoFrame(nativePtr, hardwareBuffer, timestampNs, fenceFd)
nativeSubmitVideoFrame(nativePtr, hardwareBuffer, timestampNs, fenceFd, bufferIndex)
}
fun submitAudioFrame(pcmData: ByteArray, timestampNs: Long) {
@@ -151,7 +151,7 @@ class NativeStreamingEngine {
private external fun nativeAddDestination(ptr: Long, rtmpUrl: String): Int
private external fun nativeStart(ptr: Long): Boolean
private external fun nativeSubmitVideoFrame(ptr: Long, hardwareBuffer: HardwareBuffer, timestampNs: Long, fenceFd: Int)
private external fun nativeSubmitVideoFrame(ptr: Long, hardwareBuffer: HardwareBuffer, timestampNs: Long, fenceFd: Int, bufferIndex: Int)
private external fun nativeSubmitAudioFrame(ptr: Long, pcmData: ByteArray, timestampNs: Long)
private external fun nativeStop(ptr: Long)
private external fun nativeDestroy(ptr: Long)

View File

@@ -31,6 +31,8 @@ class StreamingManager @Inject constructor() {
private var engine: NativeStreamingEngine? = null
private var texturePoolBuffers: Array<HardwareBuffer>? = null
private var texturePoolWidth: Int = 0
private var texturePoolHeight: Int = 0
private val _state = MutableStateFlow(StreamingState.IDLE)
val state: StateFlow<StreamingState> = _state.asStateFlow()
@@ -61,14 +63,19 @@ class StreamingManager @Inject constructor() {
return
}
// Use texture pool dimensions if available, otherwise use caller-provided defaults
val actualWidth = if (texturePoolWidth > 0) texturePoolWidth else width
val actualHeight = if (texturePoolHeight > 0) texturePoolHeight else height
Log.d(TAG, "Starting streaming at ${actualWidth}x${actualHeight} (pool=${texturePoolWidth}x${texturePoolHeight}, requested=${width}x${height})")
_state.value = StreamingState.STARTING
_error.value = null
try {
val eng = NativeStreamingEngine()
eng.create(
width = width,
height = height,
width = actualWidth,
height = actualHeight,
videoBitrate = config.videoBitrate,
audioBitrate = config.audioBitrate,
sampleRate = config.audioSampleRate,
@@ -93,6 +100,10 @@ class StreamingManager @Inject constructor() {
_state.value = StreamingState.ERROR
}
eng.onBufferReleased = { index ->
onBufferReleased?.invoke(index)
}
if (eng.start()) {
engine = eng
_state.value = StreamingState.LIVE
@@ -116,19 +127,43 @@ class StreamingManager @Inject constructor() {
*/
fun registerTexturePool(buffers: Array<HardwareBuffer>, width: Int, height: Int, format: Int) {
texturePoolBuffers = buffers
texturePoolWidth = width
texturePoolHeight = height
Log.d(TAG, "Texture pool registered: ${buffers.size} buffers, ${width}x${height}")
}
fun unregisterTexturePool() {
texturePoolBuffers = null
texturePoolWidth = 0
texturePoolHeight = 0
Log.d(TAG, "Texture pool unregistered")
}
private var videoFrameCount = 0
/** Callback when a buffer is released after processing. */
var onBufferReleased: ((Int) -> Unit)? = null
/** Forward a video frame from the game to the native engine. */
fun submitVideoFrame(bufferIndex: Int, timestampNs: Long, fenceFd: Int) {
val buffers = texturePoolBuffers ?: return
if (bufferIndex < 0 || bufferIndex >= buffers.size) return
engine?.submitVideoFrame(buffers[bufferIndex], timestampNs, fenceFd)
val buffers = texturePoolBuffers
if (buffers == null) {
if (videoFrameCount++ % 30 == 0) Log.w(TAG, "submitVideoFrame: no texture pool")
return
}
if (bufferIndex < 0 || bufferIndex >= buffers.size) {
if (videoFrameCount++ % 30 == 0) Log.w(TAG, "submitVideoFrame: index $bufferIndex out of range [0,${buffers.size})")
return
}
val eng = engine
if (eng == null) {
if (videoFrameCount++ % 30 == 0) Log.w(TAG, "submitVideoFrame: engine is null (state=${_state.value})")
return
}
eng.submitVideoFrame(buffers[bufferIndex], timestampNs, fenceFd, bufferIndex)
if (++videoFrameCount % 30 == 0) {
Log.d(TAG, "submitVideoFrame: forwarded frame #$videoFrameCount idx=$bufferIndex")
}
}
/** Forward audio PCM from the game to the native engine. */
@@ -138,6 +173,7 @@ class StreamingManager @Inject constructor() {
/** Stop streaming and release all resources. */
fun stopStreaming() {
Log.w(TAG, "stopStreaming() called from state=${_state.value}", Exception("Caller trace"))
if (_state.value != StreamingState.LIVE && _state.value != StreamingState.ERROR) {
return
}