You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
127 lines
8.9 KiB
127 lines
8.9 KiB
#include "call_video_encoder.h" |
|
#include "debug_config.h" |
|
#include "../../../src/call/call_video_rate.h" |
|
#include "../../../src/call/call_video_bitstream.h" |
|
#include <QElapsedTimer> |
|
|
|
// Профиль и envelope задаются явно: автоматический выбор level при смене bitrate запрещён. |
|
bool CallVideoEncoder::open(const call_video_format& format, uint32_t bitrate, const QString& backend) { |
|
if (!call_video_format_valid(&format) || bitrate < CALL_VIDEO_RATE_MIN_BPS || bitrate > format.maximum_bps) { |
|
m_error = "Invalid call video encoder configuration"; |
|
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call video open invalid generation=%llu codec=%u level=%u bitrate=%u cap=%u profile=%u", |
|
(unsigned long long)format.generation, format.codec, format.level, bitrate, format.maximum_bps, format.profile); return false; |
|
} |
|
LiveVideoConfig config; |
|
config.width = format.width; config.height = format.height; config.fps = format.fps; config.rotation = format.rotation; |
|
config.codec = format.codec; config.level = format.level; config.vbvBits = 1000000; |
|
config.bitrate = bitrate; config.references = 1; config.backend = backend; |
|
config.profile = format.codec == CALL_VIDEO_H264 ? LiveVideoProfile::H264ConstrainedBaseline : LiveVideoProfile::H265Main; |
|
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_format = format; |
|
DEBUG_INFO(DEBUG_CATEGORY_CALL, "call video encoder opened generation=%llu codec=%d encoder=%s bitrate=%d level=%d", |
|
(unsigned long long)format.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_format.generation, (unsigned long long)m_request); |
|
m_prepared = {}; // Завершение фоновой подготовки ждём только при остановке/смене режима. |
|
m_encoder.reset(); m_config = {}; m_config.bitrate = 0; m_error.clear(); |
|
m_format = {}; 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_format.generation || !request || request <= m_lastRequest || m_request |
|
|| bitrate < CALL_VIDEO_RATE_MIN_BPS || bitrate > m_format.maximum_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_format.generation, (unsigned long long)request, |
|
(unsigned long long)m_lastRequest, (unsigned long long)m_request, bitrate); return false; |
|
} |
|
if (bitrate != (uint32_t)m_config.bitrate) { |
|
LiveVideoConfig next = m_config; next.bitrate = bitrate; |
|
try { |
|
m_prepared = std::async(std::launch::async, [next] { |
|
Prepared result; QElapsedTimer timer; timer.start(); |
|
try { |
|
result.encoder = std::make_unique<LiveVideoEncoder>(); |
|
if (!result.encoder->open(next)) { result.error = result.encoder->error(); result.encoder.reset(); } |
|
} catch (const std::exception& e) { result.error = QString::fromUtf8(e.what()); result.encoder.reset(); } |
|
result.elapsed = timer.elapsed(); return result; |
|
}); |
|
} catch (const std::exception& e) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call video prepare start failed request=%llu: %s", (unsigned long long)request, e.what()); |
|
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_format.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; |
|
bool prepared = !m_prepared.valid() || m_prepared.wait_for(std::chrono::seconds(0)) == std::future_status::ready; |
|
if (key && m_request && !prepared) |
|
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "call video preparation pending request=%llu; keep current encoder", (unsigned long long)m_request); |
|
if (key && m_request && prepared) { |
|
completion = {m_format.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 replacement = m_prepared.get(); auto& candidate = replacement.encoder; |
|
QElapsedTimer timer; timer.start(); |
|
bool ready = candidate && candidate->encode(image, pts, true, frames) |
|
&& frames.size() == 1 && frames.front().key && frames.front().pts == pts |
|
&& !call_video_check_key(&m_format, reinterpret_cast<const uint8_t*>(frames.front().data.constData()), frames.front().data.size()); |
|
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 background_ms=%lld commit_ms=%lld bytes=%lld", |
|
(unsigned long long)m_format.generation, (unsigned long long)m_request, completion.bitrate, |
|
(unsigned long long)pts, (long long)replacement.elapsed, (long long)timer.elapsed(), (long long)frames.front().data.size()); |
|
} else { |
|
m_error = candidate ? candidate->error() : replacement.error; |
|
if (m_error.isEmpty()) m_error = "Replacement encoder did not produce a compatible immediate IDR"; |
|
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call video replacement failed generation=%llu request=%llu target=%u: %s", |
|
(unsigned long long)m_format.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_format.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_format.generation, (unsigned long long)pts, (long long)frames.size(), key); return false; |
|
} |
|
if (frames.front().key && call_video_check_key(&m_format, |
|
reinterpret_cast<const uint8_t*>(frames.front().data.constData()), frames.front().data.size())) { |
|
m_error = "Encoder produced an incompatible IDR"; |
|
if (completion.request) completion.bitrate = 0; |
|
return false; |
|
} |
|
m_haveFrame = true; m_lastPts = pts; if (frames.front().key) m_lastKey = pts; |
|
output = std::move(frames.front()); return true; |
|
}
|
|
|