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 } }