diff --git a/doc/service_lifecycle.md b/doc/service_lifecycle.md index b1139da3..261791a5 100644 --- a/doc/service_lifecycle.md +++ b/doc/service_lifecycle.md @@ -104,6 +104,12 @@ Live media хранится в RS-кольце с общим индексом PT destroy отсоединённого cursor не обращается к store. Frontend прекращает audio callback и worker до освобождения своего DSP; borrowed handle/RS pointers между потоками не передаются. +ACK-история принадлежит ядру и обновляется только при первом найденном подтверждении +пакета. REINIT/close соединения не стирает уже подтверждённые за последнюю секунду байты. +Android publisher владеет отдельным 100ms таймером TX-снимков; close/detach отменяют +его в uasync до уничтожения instance. JNI копирует снимок под mutex без доступа к +живым очередям транспорта из Kotlin. Экран подписан на отдельный StateFlow статистики. + Статус транспорта UP подтверждает восстановление физического пути. READY сессии группы означает согласованный JOIN/ACCEPT и завершённый обмен её таблицей; именно его ждёт recovery. REINIT меняет эпохи обмена, сохраняя рабочие маршруты. Эпохи diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/StreamCaptureEngine.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/StreamCaptureEngine.kt index 72a7dd30..d72ab9a6 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/StreamCaptureEngine.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/StreamCaptureEngine.kt @@ -62,6 +62,7 @@ class StreamCaptureEngine( private var originNs = 0L private var csd = byteArrayOf() private val partial = ByteArrayOutputStream() + private val partialConfig = ByteArrayOutputStream() private var partialPts = 0L private var partialKey = false private var requireKey = true @@ -131,7 +132,7 @@ class StreamCaptureEngine( preview.register(token, cameraPose) LogManager.addLog("DEBUG", "video", "preview camera id=$id revision=$token camera=${mode.camera} " + "sensor=${cameraPose.sensor} front=${cameraPose.front} broadcastRotation=$rotation") - csd = byteArrayOf(); partial.reset(); requireKey = true; lastKeyRequest = 0; waitingFirst = true + csd = byteArrayOf(); partial.reset(); partialConfig.reset(); requireKey = true; lastKeyRequest = 0; waitingFirst = true confirming = mode.highSpeed; confirmStartNs = 0 reportNs = System.nanoTime(); reportFrames = encodedFrames; reportBytes = bytesSent if (switchStartedNs == 0L) switchStartedNs = reportNs @@ -166,7 +167,11 @@ class StreamCaptureEngine( buffer.position(info.offset); buffer.limit(info.offset + info.size) val bytes = ByteArray(info.size).also { buffer.get(it) } if (info.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0) { - csd = mode.videoCodec.mergeConfig(csd, bytes) + check(partialConfig.size() + bytes.size <= StreamVideoCodec.MAX_CONFIG) + partialConfig.write(bytes) + if (info.flags and MediaCodec.BUFFER_FLAG_PARTIAL_FRAME == 0) { + csd = mode.videoCodec.mergeConfig(csd, partialConfig.toByteArray()); partialConfig.reset() + } } else encoded(bytes, info, config, rotation, token) } if (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) problem = "unexpected encoder EOS" @@ -352,7 +357,7 @@ class StreamCaptureEngine( val done = releaseDone ?: return try { encoder?.stop() } catch (e: Exception) { LogManager.addLog("WARN", "video", "encoder stop id=$id: ${e.message}") } try { encoder?.release() } catch (e: Exception) { LogManager.addLog("WARN", "video", "encoder release id=$id: ${e.message}") } - encoder = null; input?.release(); input = null; partial.reset(); csd = byteArrayOf(); releaseDone = null + encoder = null; input?.release(); input = null; partial.reset(); partialConfig.reset(); csd = byteArrayOf(); releaseDone = null LogManager.addLog("DEBUG", "video", "video resources released id=$id closed=$closed revision=$generation") done(closed) } diff --git a/tools/chatgui/tests/test_stream_view.cpp b/tools/chatgui/tests/test_stream_view.cpp index 01e8ff9a..e8147df8 100644 --- a/tools/chatgui/tests/test_stream_view.cpp +++ b/tools/chatgui/tests/test_stream_view.cpp @@ -265,6 +265,7 @@ private slots: AVCodecContext* context = avcodec_alloc_context3(encoder); QVERIFY(context); context->width = 160; context->height = 90; context->pix_fmt = AV_PIX_FMT_YUV420P; context->bit_rate = 200000; context->time_base = {1, 30}; context->framerate = {30, 1}; context->max_b_frames = 0; context->thread_count = 1; context->gop_size = 1; + context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; AVDictionary* options = nullptr; if (fixture.type == STREAM_MEDIA_H265) { av_dict_set(&options, "preset", "ultrafast", 0); av_dict_set(&options, "tune", "zerolatency", 0);