Stop reading live-stream stats while the SDK tears the stream down

Stopping a stream to an RTMP receiver crashed the app with SIGSEGV inside
LiveStreamManager.stopStream (Mavic Pro, 2026-10-22). stopStream returns at
once and frees the native streamer on an SDK thread, while the bridge kept
reading bitrate/fps/time through native getters from its poll and right
after the stop. The poll now ends first, nothing reads those getters while
stopping, and the bridge waits for the SDK to report the stream stopped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
tajniak81andClaude Opus 5.5 committed 2026-10-22 15:12:43 +02:00
1 parent 727850bb0e
commit 128220dcb7
1 file changed
+27 -2
@@ -30,6 +30,13 @@ class LiveStreamBridge(private val ctx: BridgeCtx) : SubBridge {
private var lastError: String? = null
private var polling = false
/**
* `stopStream` returns at once and frees the native streamer on an SDK
* thread; reading the bitrate/fps/time getters meanwhile crashed the app
* with SIGSEGV (Mavic Pro, 2026-10-22). While this is set, nothing reads them.
*/
@Volatile private var stopping = false
private val statusListener = LiveStreamManager.OnLiveChangeListener { status ->
lastStatus = status
ctx.mainHandler.post {
@@ -91,7 +98,12 @@ class LiveStreamBridge(private val ctx: BridgeCtx) : SubBridge {
lastError = null
worker.execute {
try {
if (m.isStreaming) m.stopStream()
if (m.isStreaming) {
stopping = true
m.stopStream()
Thread.sleep(STOP_GRACE_MS)
stopping = false
}
m.setLiveUrl(url)
m.setAudioStreamingEnabled(audio)
val code = m.startStream()
@@ -113,14 +125,24 @@ class LiveStreamBridge(private val ctx: BridgeCtx) : SubBridge {
private fun stop(result: MethodChannel.Result) {
val m = manager() ?: run { ctx.mainHandler.post { result.success(null) }; return }
// Stop reading the native getters before the teardown starts.
polling = false
ctx.mainHandler.removeCallbacks(poll)
stopping = true
worker.execute {
try {
m.stopStream()
} catch (_: Throwable) {
// Already stopped.
}
// Give the SDK's own teardown time to finish, or until it says it stopped.
var waited = 0L
while (waited < STOP_SETTLE_MS && (try { m.isStreaming } catch (_: Throwable) { false })) {
Thread.sleep(100); waited += 100
}
Thread.sleep(STOP_GRACE_MS)
ctx.mainHandler.post {
polling = false
stopping = false
publish()
result.success(null)
}
@@ -139,6 +161,7 @@ class LiveStreamBridge(private val ctx: BridgeCtx) : SubBridge {
}
private fun state(m: LiveStreamManager): Map<String, Any?> {
if (stopping) return mapOf("streaming" to false, "status" to "STOPPED", "videoKbps" to 0, "audioKbps" to 0, "fps" to 0.0, "seconds" to 0L, "cacheSize" to 0, "error" to lastError)
val streaming = try { m.isStreaming } catch (_: Throwable) { false }
return mapOf(
"streaming" to streaming,
@@ -160,5 +183,7 @@ class LiveStreamBridge(private val ctx: BridgeCtx) : SubBridge {
private companion object {
const val POLL_MS = 1000L
const val STOP_SETTLE_MS = 3000L
const val STOP_GRACE_MS = 500L
}
}