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:
@@ -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? {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user