diff --git a/doc/call_video.txt b/doc/call_video.txt index d238577a..4bb7eefc 100644 --- a/doc/call_video.txt +++ b/doc/call_video.txt @@ -944,15 +944,14 @@ GPU/MediaCodec отдают готовые выходы независимо о Явные stop/смена режима/отсоединение Surface отменяют соответствующие старые очереди. Decoder получает кадры строго последовательно, без искусственных интервалов 30fps между decode. -Готовые изображения остаются ограничены двумя; Android inputs+outputs — восемью. +В актуальной схеме ниже три decoded места, включая ожидающий подтверждения GUI/Surface кадр; +входы decoder ограничены отдельно восемью, чтобы output lookahead не блокировал запуск GPU. Основной backlog хранится encoded, а не большим RGB FIFO или удержанием всех аппаратных outputs. Playout больше не содержит DROP_LATE/DROP_SCHEDULE. Invalid/order остаётся диагностируемой ошибкой. CallTimeline публикует rate_ppm из фактического SourceSpan SoundTouch -> PCM на текущей позиции устройства. -База темпа видео max(1x, audio rate), коррекция 2 * max(0, audioPTS-framePTS) ppm, предел 1.6x. -После показа следующий интервал равен min(deltaPTS, 1000000/format.fps)/speed; после опоздания отсчитывается от фактической -выдачи, поэтому очередь не выдаётся мгновенной пачкой. После отсутствия готовых кадров привязка снимается: -прошедшее ожидание данных не добавляется второй раз. Дополнительный стартовый резерв не создаётся. +Первоначальный вариант управлял темпом по audioPTS-framePTS. Его заменила описанная ниже схема +с подтверждённым временем показа, общей глубиной конвейера и небольшим рабочим запасом. Видео может временно отставать от звука более 50мс, затем догоняет; опережение ограничено 40мс. Невалидные аудиочасы приостанавливают показ, сохраняют decoder и порядок кадров. @@ -998,3 +997,63 @@ source_gaps/source_gap_max_us и первый video source gap за секунд - video GUI paint: ожидание UpdateRequest -> paintEvent, prepare/draw/overlay/painter.end, размеры изображения/окна, DPR при первом замедлении, число move/resize событий. Замена renderer или изменение Windows system timer без этих измерений не выполнялись. + +Часы показа и сквозная очередь, 19.10.2026 (актуальная схема) +N = полные encoded в port + pending input worker + принятые decoder inputs без output + + decoded FIFO + один выбранный, но ещё не подтверждённый GUI/Surface кадр. +Перемещение между стадиями не меняет N; исключение кадра происходит после подтверждения показа. +Снимок port.rx_frames берётся под mutex после локальных перемещений RX worker. Сетевая частичная +сборка не считается полным кадром. Очереди отправителя не видны этому счётчику. +RX admission возобновляется при появлении места, без искусственного ожидания полного drain FIFO. +Уведомление port будит desktop RX немедленно; callback снимается до освобождения владельца. + +Декодирование не следует FPS показа. Оно запускается при готовых данных и свободном decoded месте. +Три decoded места включают GUI/Surface pending; внутренний pipeline GPU ограничен восемью входами отдельно. +submit/receive независимы: EAGAIN сохраняет непринятый packet. Metadata PTS/frame_id/generation/received_us +переходит в выход ровно один раз. Перед новым format старый decoder завершается EOS и отдаёт все принятые входы. +Уже готовые изображения старого поколения остаются в FIFO до последовательного показа. + +SHOW резервирует кадр, presented(PTS, MONOTONIC) подтверждает его потребление и продвигает часы. +Desktop ACK приходит после paintEvent; Android — после SurfaceTexture/RenderThread commit, не по OnFrameRendered. +Это подтверждения платформенного потребления, а не измерение физического scanout экрана. +Срок следующей головы фиксируется один раз: последнее ACK + период format / speed - оценка dispatch-to-ACK. +Оценка lead сглаживается и не обучается на GUI freeze длительнее половины периода. +Пустая очередь сохраняет привязку: готовый через 12мс следующий кадр не показывается раньше своего обычного срока. +Поздно пришедшая голова выходит сразу после готовности; разрыв capture PTS не повторяется ожиданием. + +Целевой N: текущая голова + один будущий кадр + необходимый pipeline выбранного decoder. +Избыток каждого кадра добавляет 0.1x к базовому max(1x, audio rate), предел 1.6x. +Недостаток рабочего запаса временно уменьшает темп на 0.02x; обязательного старта с тремя кадрами нет. +Изменение темпа сглаживается с постоянной 200мс и применяется к следующему назначаемому сроку. +Слабая A/V-коррекция (до 0.05x) видит скрытый сетевой долг сверх рабочего запаса +100мс. +Опережение аудио по-прежнему ограничено 40мс; невалидные аудиочасы запрещают новый показ без очистки FIFO. + +Windows ожидает абсолютный срок через high-resolution waitable timer + event, без spin/timeBeginPeriod. +Отказ high-resolution API явно логируется, остаётся regular timer; полный отказ handles использует condition_variable. +Stop и данные прерывают ожидание. Фактический overshoot остаётся в video RX worker wake_late_max_us. + +CPU encoder: libx264 threads=4/sliced-threads=1; libx265 pools=4/wpp=1/frame-threads=1. +Это четыре вычислительных worker, не обещание ровно четырёх OS threads вместе со служебными. +GPU encoder сохраняет приоритет NVENC/QSV/AMF/VAAPI, после отказа кандидатов разрешён CPU fallback. +CPU decoder: thread_count=4, FF_THREAD_SLICE, без кадровой многопоточности. +Реальный параллелизм зависит от slices/WPP bitstream; H.264 с единственным slice не даёт четыре независимые задачи. +Desktop GPU decode: D3D11VA на Windows, VAAPI на Linux, затем CPU. Backend проверяется paced-пробой +восьми независимых KEY с точными PTS, выдачей до EOS и чтением всех восьми выходов. +Минимальная сборка FFmpeg v5 явно включает h264/hevc_d3d11va2 либо h264/hevc_vaapi. +Для Windows DLL старого профиля необходимо запустить tools/chatgui/ffmpeg/build_ffmpeg.sh и пересобрать GUI. +Android предпочитает аппаратные MediaCodec, проверяет pipeline при paced input и включает заявленный FEATURE_LowLatency. +Числом внутренних CPU threads MediaCodec управляет платформа; параметры FFmpeg к нему не применяются. + +Обычная пауза/EAGAIN не теряет кадры и не запрашивает KEY. Реальный отказ decoder/device логируется +с числом потерянных аппаратных inputs и сохранённых ready кадров; после GPU fault используется CPU и новый KEY. +Ещё не принятый KEY при отказе открытия GPU повторяется на CPU без потери. Отказ самого устройства +может уничтожить уже принятые им references/inputs; гарантии короткого backpressure к такому отказу не относятся. +Watchdog 2с проверяет output progress только без готового/ожидающего показа и при достаточном числе decoder inputs. + +Диагностика call=debug: playout queued/reserve/awaiting/lead/speed/cadence, RX submitted/presented/encoded/inflight/total; +video decoder pipeline показывает backend, threads/active_thread_type, submit/decode/GPU transfer/RGB conversion. +Тесты: 16 сценариев 480/480 с паузами 50–500мс, фиксированный deadline после starvation, снижение 1.596x -> 1.004x; +отложенные outputs и EOS между поколениями, CPU fallback, настоящие H.264/H.265 bitstream и rate switches. +Linux Qt RX+GUI: 90/90 без KEY/reject, normal ACK p50=33.4мс/p95=37.7мс, stop=2.1мс; +с RX80мс/GUI60мс: 90/90, p95=39.1мс, максимальная пауза94.4мс, stop=5.0мс. +Эти числа относятся к локальному тесту с имитацией paint ACK, не Windows драйверу или панели. diff --git a/tools/chatgui/src/call_video_decoder.cpp b/tools/chatgui/src/call_video_decoder.cpp index 06b6f645..85ad5cb1 100644 --- a/tools/chatgui/src/call_video_decoder.cpp +++ b/tools/chatgui/src/call_video_decoder.cpp @@ -97,6 +97,7 @@ void CallVideoDecoder::close() { avcodec_free_context(&m->codec); sws_freeContext(m->scale); m->scale = nullptr; m->pixel = AV_PIX_FMT_NONE; #endif m->format = {}; m->pending.clear(); m->pipeline = 0; m->gpu = m->eos = m->outputSeen = false; + m->reportAt = m->submitMax = m->decodeMax = m->transferMax = m->convertMax = 0; m->submitted = m->received = 0; } int CallVideoDecoder::finish() { @@ -230,21 +231,25 @@ call_video_caps CallVideoDecoder::capabilities() { call_video_frame input{}; input.call_id = 1; input.flags = CALL_VIDEO_KEY; input.data = bytes; input.size = input.length = size; input.format = {1, 1280, 720, 30, 0, (uint8_t)codec, (uint8_t)(codec == 1 ? 31 : 93), 4000000, (uint8_t)profile}; - unsigned sent = 0, received = 0; bool valid = true; + unsigned sent = 0, received = 0; bool valid = true, outputBeforeEos = false; const int64_t start = call_video_playout_now_us(); - while (valid && received < 4 && call_video_playout_now_us() - start < 1500000) { - if (sent < 4 && call_video_playout_now_us() - start >= sent * 33333ll) { + while (valid && received < 8 && call_video_playout_now_us() - start < 1500000) { + if (sent < 8 && call_video_playout_now_us() - start >= sent * 33333ll) { input.frame_id = sent + 1; input.pts_us = 1000000 + sent * 33333ll; int result = decoder.submit(input); if (result < 0) valid = false; else if (result) ++sent; } call_video_frame output{}; QImage image; int result = decoder.receive(output, image); if (result < 0) valid = false; - else if (result) { valid = output.pts_us == 1000000 + received * 33333ll; ++received; } - if (sent == 4 && decoder.finish() < 0) valid = false; - if (valid && received < 4) QThread::usleep(1000); + else if (result) { + valid = output.pts_us == 1000000 + received * 33333ll; + if (!decoder.m->eos) outputBeforeEos = true; + ++received; + } + if (sent == 8 && decoder.finish() < 0) valid = false; + if (valid && received < 8) QThread::usleep(1000); } - valid = valid && received == 4; + valid = valid && received == 8 && outputBeforeEos; DEBUG_INFO(DEBUG_CATEGORY_CALL, "video decoder probe codec=%u profile=%u backend=%d valid=%d inputs=%u outputs=%u pipeline=%u", codec, profile, int(backend), valid, sent, received, decoder.pipelineFrames()); if (valid) {