Browse Source

Add verified call profiles and defer bitrate replacement until IDR

master
evgeny 5 days ago
parent
commit
3b3ba3c495
  1. 92
      tools/chatgui/src/call_video_encoder.cpp
  2. 30
      tools/chatgui/src/call_video_encoder.h
  3. 76
      tools/chatgui/src/live_video_encoder.cpp

92
tools/chatgui/src/call_video_encoder.cpp

@ -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;
}

30
tools/chatgui/src/call_video_encoder.h

@ -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;
};

76
tools/chatgui/src/live_video_encoder.cpp

@ -66,18 +66,62 @@ AVPixelFormat softwareFormat(const AVCodec* codec) {
}
return AV_PIX_FMT_NONE;
}
// Проверяем фактический bitstream: одного принятого encoder option недостаточно.
bool verifiedProfile(const LiveVideoConfig& config, const QList<LiveEncodedFrame>& output) {
if (output.size() != 1 || !output.front().key) return false;
const auto& data = output.front().data;
const uint8_t *header, *frame; size_t headerSize, frameSize;
if (stream_media_key_parse(reinterpret_cast<const uint8_t*>(data.constData()), data.size(),
&header, &headerSize, &frame, &frameSize) != STREAM_OK) return false;
QByteArray annex(reinterpret_cast<const char*>(header), headerSize); annex.append(reinterpret_cast<const char*>(frame), frameSize);
bool idr = false, mainTier = config.codec == STREAM_MEDIA_H264;
for (qsizetype i = 0; i + 4 < annex.size(); i++) {
const auto* p = reinterpret_cast<const uint8_t*>(annex.constData() + i);
int prefix = p[0] == 0 && p[1] == 0 ? (p[2] == 1 ? 3 : p[2] == 0 && p[3] == 1 ? 4 : 0) : 0;
if (!prefix || i + prefix >= annex.size()) continue;
int type = config.codec == STREAM_MEDIA_H264 ? p[prefix] & 31 : (p[prefix] >> 1) & 63;
idr |= config.codec == STREAM_MEDIA_H264 ? type == 5 : type == 19 || type == 20;
if (config.codec == STREAM_MEDIA_H265 && type == 33 && i + prefix + 3 < annex.size())
mainTier = (p[prefix + 3] & 0x20) == 0;
}
const AVCodec* codec = avcodec_find_decoder(config.codec == STREAM_MEDIA_H264 ? AV_CODEC_ID_H264 : AV_CODEC_ID_HEVC);
AVCodecContext* decoder = codec ? avcodec_alloc_context3(codec) : nullptr;
AVPacket* packet = av_packet_alloc(); AVFrame* decoded = av_frame_alloc();
int result = decoder && packet && decoded ? 0 : AVERROR(ENOMEM);
if (result >= 0) { decoder->thread_count = 1; result = avcodec_open2(decoder, codec, nullptr); }
if (result >= 0) result = av_new_packet(packet, annex.size());
if (result >= 0) { memcpy(packet->data, annex.constData(), annex.size()); result = avcodec_send_packet(decoder, packet); }
if (result >= 0) result = avcodec_receive_frame(decoder, decoded);
int profile = config.codec == STREAM_MEDIA_H264 ? AV_PROFILE_H264_CONSTRAINED_BASELINE : AV_PROFILE_HEVC_MAIN;
bool valid = result >= 0 && idr && mainTier && decoder->profile == profile && decoder->level == config.level
&& !decoder->has_b_frames && decoded->format == AV_PIX_FMT_YUV420P
&& decoded->width == config.width && decoded->height == config.height;
if (!valid) DEBUG_ERROR(DEBUG_CATEGORY_VIDEO,
"live profile verification failed codec=%d result=%d idr=%d main_tier=%d profile=%d/%d level=%d/%d pixel=%d B=%d",
config.codec, result, idr, mainTier, decoder ? decoder->profile : -1, profile,
decoder ? decoder->level : -1, config.level, decoded ? decoded->format : -1, decoder ? decoder->has_b_frames : -1);
av_frame_free(&decoded); av_packet_free(&packet); avcodec_free_context(&decoder); return valid;
}
}
#endif
bool LiveVideoEncoder::open(const LiveVideoConfig& config) {
close();
bool constrained = config.profile != LiveVideoProfile::Automatic;
if (config.width < 16 || config.height < 16 || config.width > 4096 || config.height > 4096
|| config.width % 2 || config.height % 2 || config.fps < 1 || config.fps > 60
|| config.bitrate < 64000 || config.bitrate > 16000000 || config.codec < 1 || config.codec > 5) {
|| config.bitrate < 64000 || config.bitrate > 16000000 || config.codec < 1 || config.codec > 5
|| config.vbvBits < 0 || config.vbvBits > 16000000 || config.references < 0 || config.references > 16
|| (constrained && (config.level <= 0 || config.level > 255 || config.references != 1 || !config.vbvBits
|| !((config.codec == 1 && config.profile == LiveVideoProfile::H264ConstrainedBaseline)
|| (config.codec == 2 && config.profile == LiveVideoProfile::H265Main))))) {
m->error = "Invalid video configuration"; DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop encoder invalid configuration"); return false;
}
#ifdef HAVE_FFMPEG
for (const auto& name : encoders(config.codec)) {
if (!config.backend.isEmpty() && name != config.backend.toLatin1()) continue;
if (constrained && name != "libx264" && name != "libx265") continue;
const AVCodec* encoder = avcodec_find_encoder_by_name(name.constData());
if (!encoder) continue;
// Проба не оставляет references/PTS в live-контексте: после двух KEY encoder открывается заново.
@ -87,12 +131,14 @@ bool LiveVideoEncoder::open(const LiveVideoConfig& config) {
if (!m->codec) { m->error = "Video encoder allocation failed"; break; }
auto* c = m->codec;
c->width = config.width; c->height = config.height; c->time_base = {1, 1000000}; c->framerate = {config.fps, 1};
c->bit_rate = config.bitrate; c->rc_max_rate = config.bitrate; c->rc_buffer_size = config.bitrate / 4;
c->bit_rate = config.bitrate; c->rc_max_rate = config.bitrate;
c->rc_buffer_size = config.vbvBits ? config.vbvBits : config.bitrate / 4;
c->gop_size = config.fps; c->max_b_frames = 0; c->thread_count = 2;
if (config.references) c->refs = config.references;
c->flags |= AV_CODEC_FLAG_GLOBAL_HEADER | AV_CODEC_FLAG_CLOSED_GOP;
c->color_range = AVCOL_RANGE_MPEG; c->colorspace = AVCOL_SPC_BT709;
c->color_primaries = AVCOL_PRI_BT709; c->color_trc = AVCOL_TRC_BT709;
AVPixelFormat sw = softwareFormat(encoder);
AVPixelFormat sw = constrained ? AV_PIX_FMT_YUV420P : softwareFormat(encoder);
int result = 0;
if (name.endsWith("_vaapi")) {
sw = AV_PIX_FMT_NV12; c->pix_fmt = AV_PIX_FMT_VAAPI;
@ -112,8 +158,17 @@ bool LiveVideoEncoder::open(const LiveVideoConfig& config) {
if (name == "libx264" || name == "libx265") {
av_dict_set(&options, "preset", "ultrafast", 0); av_dict_set(&options, "tune", "zerolatency", 0);
av_dict_set(&options, "forced-idr", "1", 0);
if (name == "libx264") av_dict_set(&options, "x264-params", "scenecut=0:open-gop=0:repeat-headers=0", 0);
else av_dict_set(&options, "x265-params", "bframes=0:rc-lookahead=0:open-gop=0:pools=1:frame-threads=1:log-level=error", 0);
QByteArray params = name == "libx264" ? "scenecut=0:open-gop=0:repeat-headers=0"
: "bframes=0:rc-lookahead=0:open-gop=0:pools=1:frame-threads=1:log-level=error";
if (constrained) {
c->thread_count = 1;
av_dict_set(&options, "profile", name == "libx264" ? "baseline" : "main", 0);
QByteArray level = QByteArray::number(config.level / (name == "libx264" ? 10.0 : 30.0), 'f', 1);
params += ":ref=1:";
if (name == "libx264") params += "level=" + level;
else params += "level-idc=" + level + ":high-tier=0:hrd=0:info=0:scenecut=0";
}
av_dict_set(&options, name == "libx264" ? "x264-params" : "x265-params", params.constData(), 0);
} else if (name.endsWith("_nvenc")) {
av_dict_set(&options, "preset", "p1", 0); av_dict_set(&options, "tune", "ull", 0);
av_dict_set(&options, "zerolatency", "1", 0); av_dict_set(&options, "rc-lookahead", "0", 0);
@ -162,10 +217,15 @@ bool LiveVideoEncoder::open(const LiveVideoConfig& config) {
DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "desktop encoder cannot force an independent KEY name=%s error=%s", name.constData(), qPrintable(m->error));
break;
}
if (constrained && !verifiedProfile(config, output)) {
m->error = "Encoder output violates the requested profile"; break;
}
continue;
}
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop encoder ready name=%s codec=%d size=%dx%d fps=%d bitrate=%d two_KEYs=verified",
name.constData(), config.codec, config.width, config.height, config.fps, config.bitrate);
DEBUG_INFO(DEBUG_CATEGORY_VIDEO,
"desktop encoder ready name=%s codec=%d size=%dx%d fps=%d bitrate=%d level=%d refs=%d vbv=%d profile_verified=%d",
name.constData(), config.codec, config.width, config.height, config.fps, config.bitrate,
config.level, config.references, c->rc_buffer_size, constrained);
return true;
}
}
@ -264,7 +324,7 @@ bool LiveVideoEncoder::setBitrate(int bitrate) {
if (bitrate < 64000 || bitrate > 16000000) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop live bitrate invalid value=%d", bitrate); return false; }
#ifdef HAVE_FFMPEG
// libx264 перечитывает эти три значения через reconfig_encoder при подаче следующего кадра.
if (m->codec && m->name == "libx264") {
if (m->codec && m->name == "libx264" && m->config.profile == LiveVideoProfile::Automatic) {
m->codec->bit_rate = m->codec->rc_max_rate = bitrate; m->codec->rc_buffer_size = bitrate / 4; m->config.bitrate = bitrate;
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop encoder bitrate updated in place name=libx264 bitrate=%d", bitrate); return true;
}

Loading…
Cancel
Save