From 128220dcb7991cb7fabcb65ec95561c399ee9c30 Mon Sep 17 00:00:00 2001 From: tajniak81 <13187254+tajniak81@users.noreply.github.com> Date: Thu, 22 Oct 2026 15:12:43 +0200 Subject: [PATCH] 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 --- .../com/pilotvault/flyapp/LiveStreamBridge.kt | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/Fly App/android/app/src/main/kotlin/com/pilotvault/flyapp/LiveStreamBridge.kt b/Fly App/android/app/src/main/kotlin/com/pilotvault/flyapp/LiveStreamBridge.kt index e3333f1..0adc1aa 100644 --- a/Fly App/android/app/src/main/kotlin/com/pilotvault/flyapp/LiveStreamBridge.kt +++ b/Fly App/android/app/src/main/kotlin/com/pilotvault/flyapp/LiveStreamBridge.kt @@ -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 { + 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 } }