diff --git a/tools/chatgui/src/call_video_decoder.cpp b/tools/chatgui/src/call_video_decoder.cpp new file mode 100644 index 00000000..6d20b1ee --- /dev/null +++ b/tools/chatgui/src/call_video_decoder.cpp @@ -0,0 +1,80 @@ +#include "call_video_decoder.h" +#include "debug_config.h" +#include +#ifdef HAVE_FFMPEG +extern "C" { +#include +#include +#include +} +#endif +struct CallVideoDecoder::Impl { + call_video_format format{}; +#ifdef HAVE_FFMPEG + AVCodecContext* codec = nullptr; + SwsContext* scale = nullptr; +#endif +}; +CallVideoDecoder::CallVideoDecoder() : m(std::make_unique()) {} +CallVideoDecoder::~CallVideoDecoder() { close(); } +void CallVideoDecoder::close() { +#ifdef HAVE_FFMPEG + avcodec_free_context(&m->codec); sws_freeContext(m->scale); m->scale = nullptr; +#endif + m->format = {}; +} +bool CallVideoDecoder::decode(const call_video_frame& f, QImage& image) { +#ifdef HAVE_FFMPEG + auto failed = [this, &f](const char* reason) { + DEBUG_WARN(DEBUG_CATEGORY_CALL, "video decode rejected frame=%llu generation=%llu reason=%s", + (unsigned long long)f.frame_id, (unsigned long long)f.format.generation, reason); + close(); return false; + }; + if (!call_video_format_valid(&f.format) || !f.data || !f.size || f.size > CALL_VIDEO_MAX_FRAME) return failed("format"); + bool changed = !call_video_same_format(&m->format, &f.format); + const uint8_t* data = f.data; unsigned size = f.size; + if (f.flags & CALL_VIDEO_KEY) { + if (size < 5) return failed("KEY envelope"); + uint32_t config = (uint32_t)data[0] << 24 | (uint32_t)data[1] << 16 | (uint32_t)data[2] << 8 | data[3]; + if (config > 65536 || config >= size - 4) return failed("KEY config length"); + data += 4; size -= 4; // Annex B configuration и access unit подаются вместе. + } else if (changed || !m->codec) return failed("KEY required"); + if (changed || !m->codec) { + close(); + const AVCodec* codec = avcodec_find_decoder(f.format.codec == CALL_VIDEO_H265 ? AV_CODEC_ID_HEVC : AV_CODEC_ID_H264); + if (!codec || !(m->codec = avcodec_alloc_context3(codec))) return failed("decoder unavailable"); + m->codec->thread_count = 1; m->codec->flags |= AV_CODEC_FLAG_LOW_DELAY; + if (avcodec_open2(m->codec, codec, nullptr) < 0) return failed("decoder open"); + m->format = f.format; + DEBUG_INFO(DEBUG_CATEGORY_CALL, "video decoder opened codec=%u level=%u generation=%llu", + f.format.codec, f.format.level, (unsigned long long)f.format.generation); + } + AVPacket* packet = av_packet_alloc(); AVFrame* decoded = av_frame_alloc(); + if (!packet || !decoded) { av_packet_free(&packet); av_frame_free(&decoded); return failed("allocation"); } + int result = av_new_packet(packet, size); + if (result >= 0) { + memcpy(packet->data, data, size); packet->pts = packet->dts = f.pts_us; + result = avcodec_send_packet(m->codec, packet); + if (result >= 0) result = avcodec_receive_frame(m->codec, decoded); + } + bool valid = result >= 0 && decoded->pts == f.pts_us && decoded->width == f.format.width && decoded->height == f.format.height + && decoded->format == AV_PIX_FMT_YUV420P && !m->codec->has_b_frames && m->codec->level <= f.format.level; + int profile = f.format.codec == CALL_VIDEO_H265 ? FF_PROFILE_HEVC_MAIN : FF_PROFILE_H264_CONSTRAINED_BASELINE; + valid = valid && m->codec->profile == profile; + if (valid) { + QImage output(decoded->width, decoded->height, QImage::Format_RGB888); + m->scale = sws_getCachedContext(m->scale, decoded->width, decoded->height, AV_PIX_FMT_YUV420P, + decoded->width, decoded->height, AV_PIX_FMT_RGB24, SWS_FAST_BILINEAR, nullptr, nullptr, nullptr); + if (!m->scale || output.isNull()) valid = false; + else { + uint8_t* planes[4] = {output.bits(), nullptr, nullptr, nullptr}; int strides[4] = {output.bytesPerLine(), 0, 0, 0}; + valid = sws_scale(m->scale, decoded->data, decoded->linesize, 0, decoded->height, planes, strides) == decoded->height; + if (valid) image = f.format.rotation ? output.transformed(QTransform().rotate(f.format.rotation)) : output; + } + } + av_packet_free(&packet); av_frame_free(&decoded); + return valid ? true : failed("PTS/profile/level/geometry/output"); +#else + Q_UNUSED(f); Q_UNUSED(image); DEBUG_WARN(DEBUG_CATEGORY_CALL, "video decoder unavailable: no FFmpeg"); return false; +#endif +} diff --git a/tools/chatgui/src/call_video_decoder.h b/tools/chatgui/src/call_video_decoder.h new file mode 100644 index 00000000..437d343d --- /dev/null +++ b/tools/chatgui/src/call_video_decoder.h @@ -0,0 +1,16 @@ +#pragma once +#include "../../../src/call/call_video.h" +#include +#include + +// Один RX worker; декодированные изображения принадлежат очереди показа по аудиочасам. +class CallVideoDecoder { +public: + CallVideoDecoder(); + ~CallVideoDecoder(); + bool decode(const call_video_frame& frame, QImage& image); + void close(); +private: + struct Impl; + std::unique_ptr m; +};