3 changed files with 190 additions and 8 deletions
@ -0,0 +1,92 @@
|
||||
#include "call_video_encoder.h" |
||||
#include "debug_config.h" |
||||
#include "../../../src/call/call_video_rate.h" |
||||
#include <QElapsedTimer> |
||||
|
||||
// Профиль и envelope задаются явно: автоматический выбор level при смене bitrate запрещён.
|
||||
bool CallVideoEncoder::open(const LiveVideoConfig& config, uint64_t generation) { |
||||
if (!generation || config.profile == LiveVideoProfile::Automatic || config.references != 1 |
||||
|| config.bitrate < CALL_VIDEO_RATE_MIN_BPS || config.bitrate > CALL_VIDEO_RATE_MAX_BPS) { |
||||
m_error = "Invalid call video encoder configuration"; |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call video open invalid generation=%llu bitrate=%d profile=%d refs=%d", |
||||
(unsigned long long)generation, config.bitrate, (int)config.profile, config.references); return false; |
||||
} |
||||
auto candidate = std::make_unique<LiveVideoEncoder>(); |
||||
if (!candidate->open(config)) { |
||||
m_error = candidate->error(); DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call video open failed: %s", qPrintable(m_error)); return false; |
||||
} |
||||
close(); m_encoder = std::move(candidate); m_config = config; m_config.backend = m_encoder->name(); m_generation = generation; |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "call video encoder opened generation=%llu codec=%d encoder=%s bitrate=%d level=%d", |
||||
(unsigned long long)generation, config.codec, qPrintable(m_config.backend), config.bitrate, config.level); return true; |
||||
} |
||||
|
||||
void CallVideoEncoder::close() { |
||||
if (m_encoder) DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "call video encoder close generation=%llu pending_request=%llu", |
||||
(unsigned long long)m_generation, (unsigned long long)m_request); |
||||
m_encoder.reset(); m_config = {}; m_config.bitrate = 0; m_error.clear(); |
||||
m_generation = m_request = m_lastRequest = m_lastPts = m_lastKey = 0; m_pendingBitrate = 0; m_haveFrame = false; |
||||
} |
||||
|
||||
// Ядро сериализует команды; повторные/чужие requests не заменяют уже ожидающую команду.
|
||||
bool CallVideoEncoder::requestBitrate(uint64_t generation, uint64_t request, uint32_t bitrate) { |
||||
if (!m_encoder || generation != m_generation || !request || request <= m_lastRequest || m_request |
||||
|| bitrate < CALL_VIDEO_RATE_MIN_BPS || bitrate > CALL_VIDEO_RATE_MAX_BPS) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "call video bitrate rejected generation=%llu/%llu request=%llu last=%llu pending=%llu bitrate=%u", |
||||
(unsigned long long)generation, (unsigned long long)m_generation, (unsigned long long)request, |
||||
(unsigned long long)m_lastRequest, (unsigned long long)m_request, bitrate); return false; |
||||
} |
||||
m_request = m_lastRequest = request; m_pendingBitrate = bitrate; |
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "call video bitrate queued generation=%llu request=%llu applied=%d target=%u last_key=%llu", |
||||
(unsigned long long)generation, (unsigned long long)request, m_config.bitrate, bitrate, (unsigned long long)m_lastKey); return true; |
||||
} |
||||
|
||||
// Capture PTS продолжает идти при backpressure, поэтому запрет отправки не задерживает применение.
|
||||
bool CallVideoEncoder::encode(const QImage& image, uint64_t pts, bool forceKey, LiveEncodedFrame& output, |
||||
CallVideoRateCompletion& completion) { |
||||
completion = {}; |
||||
if (!m_encoder || image.isNull() || pts > INT64_MAX || (m_haveFrame && pts <= m_lastPts)) { |
||||
m_error = "Invalid call video frame or non-monotonic PTS"; |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call video encode invalid generation=%llu pts=%llu last=%llu active=%d image_null=%d", |
||||
(unsigned long long)m_generation, (unsigned long long)pts, (unsigned long long)m_lastPts, |
||||
m_encoder != nullptr, image.isNull()); return false; |
||||
} |
||||
bool key = forceKey || !m_haveFrame || pts - m_lastKey >= 1000000; |
||||
QList<LiveEncodedFrame> frames; |
||||
if (key && m_request) { |
||||
completion = {m_generation, m_request, 0}; |
||||
if (m_pendingBitrate == (uint32_t)m_config.bitrate) completion.bitrate = m_pendingBitrate; |
||||
else { |
||||
LiveVideoConfig next = m_config; next.bitrate = m_pendingBitrate; |
||||
auto candidate = std::make_unique<LiveVideoEncoder>(); QElapsedTimer timer; timer.start(); |
||||
bool ready = candidate->open(next) && candidate->encode(image, pts, true, frames) |
||||
&& frames.size() == 1 && frames.front().key && frames.front().pts == pts; |
||||
if (ready) { |
||||
m_encoder = std::move(candidate); m_config = next; completion.bitrate = m_pendingBitrate; |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, |
||||
"call video bitrate committed generation=%llu request=%llu bitrate=%u pts=%llu prepare_ms=%lld bytes=%lld", |
||||
(unsigned long long)m_generation, (unsigned long long)m_request, completion.bitrate, |
||||
(unsigned long long)pts, (long long)timer.elapsed(), (long long)frames.front().data.size()); |
||||
} else { |
||||
m_error = candidate->error(); if (m_error.isEmpty()) m_error = "Replacement encoder did not produce an immediate IDR"; |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call video replacement failed generation=%llu request=%llu target=%u: %s", |
||||
(unsigned long long)m_generation, (unsigned long long)m_request, m_pendingBitrate, qPrintable(m_error)); |
||||
frames.clear(); |
||||
} |
||||
} |
||||
m_request = 0; m_pendingBitrate = 0; |
||||
} |
||||
if (frames.isEmpty() && !m_encoder->encode(image, pts, key, frames)) { |
||||
m_error = m_encoder->error(); |
||||
if (completion.request) completion.bitrate = 0; |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call video encoding failed generation=%llu: %s", |
||||
(unsigned long long)m_generation, qPrintable(m_error)); return false; |
||||
} |
||||
if (frames.size() != 1 || frames.front().pts != pts || (key && !frames.front().key)) { |
||||
m_error = "Call encoder delayed output or failed to produce IDR"; |
||||
if (completion.request) completion.bitrate = 0; |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call video output contract failed generation=%llu pts=%llu frames=%lld key_requested=%d", |
||||
(unsigned long long)m_generation, (unsigned long long)pts, (long long)frames.size(), key); return false; |
||||
} |
||||
m_haveFrame = true; m_lastPts = pts; if (frames.front().key) m_lastKey = pts; |
||||
output = std::move(frames.front()); return true; |
||||
} |
||||
@ -0,0 +1,30 @@
|
||||
/* Кодер видеотрека звонка; только video worker, без камеры, транспорта и таймеров.
|
||||
* Смена bitrate ожидает ближайший IDR (период <= 1с по capture PTS). Новый encoder |
||||
* заменяет старый только после полного первого IDR. Completion передаётся в uasync |
||||
* независимо от admission видео. PTS, generation и frame_id принадлежат владельцу. |
||||
* open получает уже согласованный profile/level/VBV; сетевой CAPS проверяет владелец. */ |
||||
#pragma once |
||||
#include "live_video_encoder.h" |
||||
|
||||
struct CallVideoRateCompletion { |
||||
uint64_t generation = 0, request = 0; |
||||
uint32_t bitrate = 0; // request != 0, bitrate == 0 — отказ; request == 0 — нет результата.
|
||||
}; |
||||
|
||||
class CallVideoEncoder { |
||||
public: |
||||
bool open(const LiveVideoConfig& config, uint64_t generation); |
||||
void close(); |
||||
bool requestBitrate(uint64_t generation, uint64_t request, uint32_t bitrate); |
||||
// Один вход -> один выход. На false output не меняется; completion нужно обработать и на ошибке.
|
||||
bool encode(const QImage& image, uint64_t pts, bool forceKey, LiveEncodedFrame& output, CallVideoRateCompletion& completion); |
||||
uint32_t bitrate() const { return m_config.bitrate; } |
||||
QString error() const { return m_error; } |
||||
private: |
||||
std::unique_ptr<LiveVideoEncoder> m_encoder; |
||||
LiveVideoConfig m_config; |
||||
QString m_error; |
||||
uint64_t m_generation = 0, m_request = 0, m_lastRequest = 0, m_lastPts = 0, m_lastKey = 0; |
||||
uint32_t m_pendingBitrate = 0; |
||||
bool m_haveFrame = false; |
||||
}; |
||||
Loading…
Reference in new issue