diff --git a/doc/service_lifecycle.md b/doc/service_lifecycle.md index f2605ef7..17802ffe 100644 --- a/doc/service_lifecycle.md +++ b/doc/service_lifecycle.md @@ -120,6 +120,9 @@ Android worker держит один рабочий input и один готов input buffer получает лишь для уже готовых данных и сразу возвращает codec. Первый decoded frame разрешает движение clock/audio. Будущий output ждёт PTS и приостанавливает новые video requests, сохраняя независимую обработку audio/control. +Если первый доступный звук позже KEY, cursor отсчитывает стартовый audio lookahead от +первого аудиопакета, сохраняя это основание неизменным. Opus/SoundTouch получает PCM до +запуска audio clock; неподвижные часы не разрешают читать дальше этого ограниченного окна. PTS поданных кадров служат диагностике/проверке запаздывания, а не лимиту заполнения codec. Смена revision заменяет только video decoder и его измеренный защитный guard; GAP/EXPIRED начинают новую точку KEY. Нехватка сети сохраняет decoder references. @@ -145,10 +148,14 @@ Qt capture callback хранит только последний кадр, video вне GUI и hardware callbacks. Отдельные поколения источника и encoder позволяют менять видеокодек/bitrate/rotation без повторного выбора экрана в портале. Ошибки Qt capture обрабатываются через queued callback, чтобы не уничтожать backend из собственного callback. -Сначала stop запрещает producers, закрывает Qt capture и подписку на аудиозахват, затем join workers, -освобождает microphone lease и завершает публикацию через FIN с drain 2s. +Stop запрещает producers, закрывает Qt capture и подписку на аудиозахват. Отдельный join worker +дожидается полного выхода video/audio threads, включая native TLS Qt; GUI продолжает обрабатывать события. +После join отправляется FIN с drain 2s; закрытие последнего микрофона может продолжаться параллельно. +Microphone lease и owner сохраняются до завершения обоих путей. Новый start ждёт также FIN drain. При завершении приложения engine уничтожается до SoundManager и ядра; detach в uasync -закрывает оставшийся publisher. Workers никогда не держат borrowed instance/handle. +закрывает оставшийся publisher. Destructor ждёт workers в локальном event loop без пользовательского ввода, +чтобы thread cleanup мог обратиться к GUI; start и внешние сигналы engine в это время запрещены. +Workers никогда не держат borrowed instance/handle. Microphone lease публикации исключает call/recorder; рация согласует вход через `AudioCapture`. Одинаковый выбранный вход имеет одну аппаратную сессию и профиль трансляции. Разные входы независимы. Подключение/отключение рации не останавливает устройство трансляции: атомарный слот и счётчик readers diff --git a/tools/chatgui/src/stream_publish_engine.cpp b/tools/chatgui/src/stream_publish_engine.cpp index deb825b2..66a9e446 100644 --- a/tools/chatgui/src/stream_publish_engine.cpp +++ b/tools/chatgui/src/stream_publish_engine.cpp @@ -63,7 +63,7 @@ struct StreamPublishEngine::Impl { std::condition_variable wake; std::thread videoThread, audioThread, joinThread; std::atomic workersJoined{false}, videoExited{false}, audioExited{false}; - bool stopping = false, destroying = false; + bool stopping = false, finSent = false, destroying = false; int64_t stopStarted = 0, stopReport = 0; std::atomic running{false}, muted{false}; std::atomic audioDropped{0}, audioMissing{0}; @@ -494,7 +494,7 @@ void StreamPublishEngine::publicationFailed(uint64_t id, const QString& error) { void StreamPublishEngine::stop() { if (!m->running.exchange(false)) return; int64_t started = monotonicUs(); - m->stopping = true; m->stopStarted = m->stopReport = started; + m->stopping = true; m->finSent = false; m->stopStarted = m->stopReport = started; DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop stop begin id=%llu video_stage=%s stage_age_ms=%lld converted=%llu encoded=%llu delivered=%llu", (unsigned long long)m->id, m->videoStage.load(), (long long)((started - m->videoStageAt.load()) / 1000), (unsigned long long)m->convertedFrames.load(), (unsigned long long)m->encodedFrames.load(), @@ -522,11 +522,13 @@ bool StreamPublishEngine::finishStop() { if (!m->workersJoined) return false; m->joinThread.join(); } + if (!m->finSent) { + gui_stream_publish_stop(m->id); m->finSent = true; + DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop workers stopped id=%llu elapsed_ms=%lld microphone_closing=%d; draining publisher", + (unsigned long long)m->id, (long long)((monotonicUs() - m->stopStarted) / 1000), m_capture.closing()); + } if (m_capture.closing()) return false; m_microphone.release(); m->stopping = false; - gui_stream_publish_stop(m->id); - DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop capture stopped id=%llu elapsed_ms=%lld; draining publisher", - (unsigned long long)m->id, (long long)((monotonicUs() - m->stopStarted) / 1000)); return true; }