Files
utun2/tools/chatgui/src/call_video_engine.cpp
T

347 lines
18 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "call_video_engine.h"
#include "call_video_encoder.h"
#include "call_video_decoder.h"
#include "call_video_cadence.h"
#include "call_video_capture_clock.h"
#include "call_camera_rate.h"
#include "../../../src/call/call_video_port.h"
#include "../../../src/call/call_audio.h"
#include "debug_config.h"
#include "mem.h"
#include <thread>
#include <mutex>
#include <atomic>
#include <deque>
#include <chrono>
#ifdef HAVE_STREAM_CAPTURE
#include <QCamera>
#include <QCameraDevice>
#include <QMediaDevices>
#include <QMediaCaptureSession>
#include <QVideoSink>
#include <QVideoFrame>
#endif
namespace {
int64_t nowUs() { return std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now().time_since_epoch()).count(); }
void idle() { std::this_thread::sleep_for(std::chrono::milliseconds(5)); }
}
struct CallVideoEngine::Impl {
std::mutex lock;
std::thread tx, rx;
std::atomic<bool> running{false};
uint64_t id = 0, revision = 1;
bool enabled = false;
int width = 640, height = 480, codec = 0;
int64_t pts = -1;
CallVideoCaptureClock captureClock;
uint64_t captured = 0;
QImage shown;
uint64_t shownSerial = 0, deliveredSerial = 0;
int64_t previewPts = -1, previewReportAt = 0, previewMaxUs = 0;
unsigned previewFrames = 0;
bool previewVisible = false;
QString status;
bool statusDirty = false;
void report(const QString& text) {
std::lock_guard<std::mutex> guard(lock); status = text; statusDirty = true;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "desktop video: %s", qPrintable(text));
}
#ifdef HAVE_STREAM_CAPTURE
std::unique_ptr<QCamera> camera;
std::unique_ptr<QMediaCaptureSession> capture;
std::unique_ptr<QVideoSink> sink;
CallCameraRate cameraRate;
QVideoFrame raw;
#endif
};
CallVideoEngine::CallVideoEngine(QObject* parent) : QObject(parent), m(std::make_unique<Impl>()) {
m_timer.setInterval(20);
m_timer.setTimerType(Qt::PreciseTimer);
connect(&m_timer, &QTimer::timeout, this, [this] {
QImage image; QString status; bool changed = false, report = false;
{ std::lock_guard<std::mutex> guard(m->lock);
if (m->shownSerial != m->deliveredSerial) { image = m->shown; m->deliveredSerial = m->shownSerial; changed = true; }
if (m->statusDirty) { status = m->status; m->statusDirty = false; report = true; } }
if (changed) emit imageChanged(image);
if (report) emit statusChanged(status);
#ifdef HAVE_STREAM_CAPTURE
QVideoFrame preview; bool clear = false;
int64_t now = nowUs();
{ std::lock_guard<std::mutex> guard(m->lock);
if (!m->enabled && m->previewVisible) { m->previewVisible = false; clear = true; }
if (m->enabled && m->raw.isValid() && m->pts != m->previewPts) {
preview = m->raw; m->previewPts = m->pts;
} }
if (clear) emit localImageChanged({});
if (preview.isValid()) {
QImage local = preview.toImage();
if (!local.isNull()) {
m->previewVisible = true;
emit localImageChanged(local.scaled(320, 240, Qt::KeepAspectRatio, Qt::SmoothTransformation));
++m->previewFrames;
m->previewMaxUs = std::max(m->previewMaxUs, nowUs() - now);
} else DEBUG_WARN(DEBUG_CATEGORY_CALL, "call camera preview conversion failed");
}
if (m->previewVisible && now - m->previewReportAt >= 1000000) {
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "video local preview call=%llu interval_us=%lld frames=%u update_max_us=%lld",
(unsigned long long)m->id, (long long)(now - m->previewReportAt), m->previewFrames, (long long)m->previewMaxUs);
m->previewReportAt = now; m->previewFrames = 0; m->previewMaxUs = 0;
}
#endif
});
}
CallVideoEngine::~CallVideoEngine() { stop(); }
QList<QPair<QString, QByteArray>> CallVideoEngine::cameras() {
QList<QPair<QString, QByteArray>> result;
#ifdef HAVE_STREAM_CAPTURE
for (const auto& camera : QMediaDevices::videoInputs()) result.append({camera.description(), camera.id()});
#endif
return result;
}
void CallVideoEngine::start(quint64 id) {
if (m->running && m->id == id) return;
stop(); m->id = id; m->running = true; m_timer.start();
try { m->tx = std::thread(&CallVideoEngine::transmit, this); m->rx = std::thread(&CallVideoEngine::receive, this); }
catch (const std::exception& error) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "video worker start: %s", error.what()); stop(); emit statusChanged(error.what()); }
}
void CallVideoEngine::closeCamera() {
#ifdef HAVE_STREAM_CAPTURE
if (m->sink) disconnect(m->sink.get(), nullptr, this, nullptr);
if (m->camera) { disconnect(m->camera.get(), nullptr, this, nullptr); m->camera->stop(); }
m->cameraRate.restore();
m->capture.reset(); m->camera.reset(); m->sink.reset();
{ std::lock_guard<std::mutex> guard(m->lock); m->raw = {}; m->pts = -1; m->captureClock = {}; }
#endif
m->previewPts = -1; m->previewVisible = false; m->previewReportAt = nowUs(); m->previewFrames = 0; m->previewMaxUs = 0;
emit localImageChanged({});
}
void CallVideoEngine::stop() {
m->running = false; m_timer.stop(); closeCamera();
if (m->tx.joinable()) m->tx.join();
if (m->rx.joinable()) m->rx.join();
if (m->id) call_video_port_close(m->id);
m->id = 0; m->enabled = false; m->shown = {}; emit imageChanged({});
}
void CallVideoEngine::setVideo(bool enabled, const QByteArray& cameraId, int width, int height, int codec) {
closeCamera();
uint64_t revision;
{ std::lock_guard<std::mutex> guard(m->lock);
m->enabled = enabled; m->width = width; m->height = height; m->codec = codec; revision = ++m->revision; }
if (!enabled || !m->running) return;
#ifdef HAVE_STREAM_CAPTURE
QCameraDevice selected;
for (const auto& camera : QMediaDevices::videoInputs()) if (camera.id() == cameraId) selected = camera;
if (selected.isNull()) { m->report(tr("Camera is unavailable")); return; }
m->capture = std::make_unique<QMediaCaptureSession>(); m->sink = std::make_unique<QVideoSink>();
m->camera = std::make_unique<QCamera>(selected); m->capture->setCamera(m->camera.get()); m->capture->setVideoSink(m->sink.get());
QCameraFormat best; int distance = INT_MAX;
for (const auto& f : selected.videoFormats()) {
if (f.maxFrameRate() < 29.9) continue;
int score = std::abs(f.resolution().width() - width) + std::abs(f.resolution().height() - height);
if (score < distance) { best = f; distance = score; }
}
if (!best.isNull()) m->camera->setCameraFormat(best);
const auto selectedFormat = m->camera->cameraFormat();
DEBUG_INFO(DEBUG_CATEGORY_CALL, "call camera selected device=%s size=%dx%d pixel=%d fps=%.3f..%.3f",
selected.id().constData(), selectedFormat.resolution().width(), selectedFormat.resolution().height(),
selectedFormat.pixelFormat(), selectedFormat.minFrameRate(), selectedFormat.maxFrameRate());
connect(m->camera.get(), &QCamera::activeChanged, this, [this, device = selected.id()](bool active) {
if (active) m->cameraRate.activate(device);
else m->cameraRate.restore();
});
connect(m->sink.get(), &QVideoSink::videoFrameChanged, this, [this, revision](const QVideoFrame& frame) {
if (!frame.isValid() || !m->running) return;
std::lock_guard<std::mutex> guard(m->lock);
if (revision != m->revision) return;
const int64_t arrival = nowUs(), source = frame.startTime();
if (m->pts < 0) DEBUG_INFO(DEBUG_CATEGORY_CALL, "camera clock source=%lld monotonic=%lld calibration=arrival-upper-bound",
(long long)source, (long long)arrival);
if (m->captureClock.source >= 0 && source < m->captureClock.source)
DEBUG_WARN(DEBUG_CATEGORY_CALL, "camera clock reset source=%lld previous=%lld", (long long)source, (long long)m->captureClock.source);
const int64_t pts = m->captureClock.map(source, arrival);
if (pts > m->pts) { m->raw = frame; m->pts = pts; ++m->captured; }
}, Qt::DirectConnection);
connect(m->camera.get(), &QCamera::errorOccurred, this, [this](QCamera::Error, const QString& error) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call camera failed: %s", qPrintable(error)); m->report(error);
});
m->camera->start();
#else
Q_UNUSED(cameraId); Q_UNUSED(revision); m->report(tr("Video capture is unavailable in this build"));
#endif
}
void CallVideoEngine::transmit() {
call_video_caps caps = CallVideoDecoder::capabilities();
// Не связываем приём с наличием собственного encoder.
for (unsigned codec = 1; codec <= 2 && m->running; ++codec) {
call_video_format probe{1, 1280, 720, 30, 0, (uint8_t)codec, (uint8_t)(codec == 1 ? 31 : 93), 4000000,
(uint8_t)(codec == 1 ? CALL_VIDEO_CONSTRAINED_BASELINE : CALL_VIDEO_MAIN)};
CallVideoEncoder encoder;
if (encoder.open(probe, 1000000)) {
call_video_limit limit{1280, 720, 30, 1280 * 720 * 30, 4000000, probe.profile, probe.level};
caps.encode[codec - 1] = limit;
} else DEBUG_WARN(DEBUG_CATEGORY_CALL, "call codec probe unavailable codec=%u: %s", codec, qPrintable(encoder.error()));
}
if (!m->running) return;
if (call_video_port_open(m->id, &caps) < 0) { m->report(tr("Video transport is unavailable")); return; }
CallVideoEncoder encoder;
call_video_format format{};
uint64_t appliedRevision = 0, keySerial = 0, request = 0, frameId = 0;
uint64_t userRevision = 0, rejectedGeneration = 0;
unsigned excluded = 0;
int64_t lastPts = -1, lastKey = -1;
bool forceKey = true;
int64_t reportAt = nowUs(), encodeMax = 0;
uint64_t captureBase = 0;
unsigned encodedCount = 0, keyCount = 0, sendBusy = 0, paced = 0;
CallVideoCadence cadence;
while (m->running) {
struct call_video_port_status state{};
if (call_video_port_status(m->id, &state) < 0) break;
if (state.error) { m->report(tr("Core rejected video format")); break; }
uint64_t revision, captured; bool enabled; int width, height, codec; int64_t pts, clockCorrection;
#ifdef HAVE_STREAM_CAPTURE
QVideoFrame raw;
#endif
{ std::lock_guard<std::mutex> guard(m->lock);
revision = m->revision; enabled = m->enabled; width = m->width; height = m->height; codec = m->codec; pts = m->pts;
captured = m->captured;
clockCorrection = m->captureClock.correction;
#ifdef HAVE_STREAM_CAPTURE
raw = m->raw;
#endif
}
int64_t now = nowUs();
if (now - reportAt >= 1000000) {
DEBUG_DEBUG(DEBUG_CATEGORY_CALL,
"video capture flow call=%llu interval_us=%lld captured=%llu encoded=%u keys=%u send_busy=%u paced=%u "
"encode_max_us=%lld capture_age_us=%lld pts=%lld clock_correction_us=%lld",
(unsigned long long)m->id, (long long)(now - reportAt), (unsigned long long)(captured - captureBase),
encodedCount, keyCount, sendBusy, paced, (long long)encodeMax,
(long long)(pts >= 0 ? now - pts : 0), (long long)pts, (long long)clockCorrection);
reportAt = now; captureBase = captured; encodedCount = keyCount = sendBusy = paced = 0; encodeMax = 0;
}
if (revision != userRevision) { userRevision = revision; excluded = 0; }
if (format.generation && state.decoded_generation == format.generation && !state.decoded_accepted
&& rejectedGeneration != format.generation) {
rejectedGeneration = format.generation; excluded |= 1u << format.codec;
if (!codec) { appliedRevision = 0; lastKey = -1; m->report(tr("Receiver rejected video; selecting fallback")); }
else {
m->report(tr("Receiver cannot decode selected codec"));
std::lock_guard<std::mutex> guard(m->lock); m->enabled = false; continue;
}
}
if (format.generation && state.have_peer && !call_video_format_supported(&caps, &state.peer, &format)) appliedRevision = 0;
if (!enabled || !state.have_peer || pts <= lastPts) { idle(); continue; }
if (revision != appliedRevision) {
if (lastKey >= 0 && pts - lastKey < 1000000) { idle(); continue; }
call_video_format next{format.generation + 1, (uint16_t)width, (uint16_t)height, 30, 0, (uint8_t)codec, 0, 4000000, 0};
bool opened = false;
uint32_t minimum = state.minimum_bps ? state.minimum_bps : 300000;
uint32_t maximum = state.maximum_bps ? state.maximum_bps : 4000000;
uint32_t initialRate = std::max(minimum, std::min(maximum, encoder.bitrate() ? encoder.bitrate() : 1000000));
for (int candidate = 2; candidate >= 1; --candidate) {
if ((codec && codec != candidate) || (excluded & (1u << candidate))) continue;
next.codec = candidate; next.level = next.profile = 0;
if (call_video_choose(&caps, &state.peer, &next) > 0 && encoder.open(next, initialRate)) { opened = true; break; }
}
if (!opened) {
m->report(tr("Selected video mode is unavailable to the peer"));
std::lock_guard<std::mutex> guard(m->lock); if (m->revision == revision) m->enabled = false;
continue;
}
format = next; appliedRevision = revision; forceKey = true; request = 0;
if (call_video_port_format(m->id, &format, encoder.bitrate()) < 0) break;
m->report(QString("%1 · %2×%3 · %4 kbit/s").arg(format.codec == 2 ? "H.265" : "H.264").arg(width).arg(height).arg(encoder.bitrate() / 1000));
}
if (state.configured_generation != format.generation) { idle(); continue; }
if (state.key_serial != keySerial) { keySerial = state.key_serial; forceKey = true; }
if (state.rate_generation == format.generation && state.rate_request > request) {
request = state.rate_request;
if (!encoder.requestBitrate(format.generation, request, state.rate_bps))
call_video_port_applied(m->id, format.generation, request, 0);
}
#ifdef HAVE_STREAM_CAPTURE
if (!cadence.accept(pts)) { lastPts = pts; ++paced; idle(); continue; }
QImage image = raw.toImage();
if (image.isNull()) { DEBUG_WARN(DEBUG_CATEGORY_CALL, "camera frame conversion failed"); lastPts = pts; continue; }
LiveEncodedFrame output; CallVideoRateCompletion completion;
int64_t encodeStarted = nowUs();
bool encoded = encoder.encode(image, pts, forceKey, output, completion); lastPts = pts;
encodeMax = std::max(encodeMax, nowUs() - encodeStarted);
if (completion.request) {
call_video_port_applied(m->id, completion.generation, completion.request, completion.bitrate);
m->report(QString("%1 · %2×%3 · %4 kbit/s").arg(format.codec == 2 ? "H.265" : "H.264")
.arg(format.width).arg(format.height).arg(encoder.bitrate() / 1000));
}
if (!encoded) { forceKey = true; m->report(encoder.error()); continue; }
++encodedCount; if (output.key) ++keyCount;
if (output.key) lastKey = pts;
call_video_frame packet{};
packet.call_id = m->id; packet.frame_id = ++frameId; packet.pts_us = pts; packet.format = format;
packet.size = packet.length = output.data.size(); packet.data = reinterpret_cast<const uint8_t*>(output.data.constData());
packet.flags = output.key ? CALL_VIDEO_KEY : 0;
forceKey = call_video_port_send(&packet) != 1;
if (forceKey) ++sendBusy;
#else
idle();
#endif
}
}
void CallVideoEngine::receive() {
struct Picture { int64_t pts; QImage image; };
std::deque<Picture> pictures;
CallVideoDecoder decoder;
uint64_t sourceEpoch = 0; int64_t lastShown = -1, report = 0, lastPictureAt = 0; unsigned shown = 0;
uint64_t reportedGeneration = 0, rejectedGeneration = 0;
while (m->running) {
int64_t now = nowUs(); call_clock_snapshot clock{};
call_audio_get_clock(m->id, now, &clock);
if (clock.valid) {
if (sourceEpoch && sourceEpoch != clock.source_epoch) {
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "video audio clock epoch %llu -> %llu; decoder retained",
(unsigned long long)sourceEpoch, (unsigned long long)clock.source_epoch);
}
sourceEpoch = clock.source_epoch;
QImage selected;
while (!pictures.empty() && pictures.front().pts <= clock.pts_us) {
if (pictures.front().pts > lastShown && pictures.front().pts >= clock.pts_us - 500000) {
selected = pictures.front().image; lastShown = pictures.front().pts;
}
pictures.pop_front();
}
if (!selected.isNull()) {
std::lock_guard<std::mutex> guard(m->lock); m->shown = selected; ++m->shownSerial; ++shown; lastPictureAt = now;
}
}
// VIDEO_OFF пока отсутствует в протоколе: убираем застывшую картинку после прекращения показа.
if (lastPictureAt && now - lastPictureAt >= 5000000) {
std::lock_guard<std::mutex> guard(m->lock); m->shown = {}; ++m->shownSerial; lastPictureAt = 0;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "video playback hidden: no playable frame for 5s");
}
if (pictures.size() < 16) {
call_video_frame packet{};
if (call_video_port_pull(m->id, &packet) == 1) {
if (packet.format.generation == rejectedGeneration) { u_free((void*)packet.data); continue; }
QImage image;
if (decoder.decode(packet, image)) {
pictures.push_back({packet.pts_us, image});
if (reportedGeneration != packet.format.generation) {
reportedGeneration = packet.format.generation; call_video_port_decoded(m->id, reportedGeneration, 1);
}
} else {
rejectedGeneration = packet.format.generation; call_video_port_decoded(m->id, rejectedGeneration, 0);
}
u_free((void*)packet.data); continue;
}
}
if (now - report >= 1000000) {
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "video playback shown=%u queue=%zu audio_valid=%d audio_pts=%lld video_pts=%lld",
shown, pictures.size(), clock.valid, (long long)clock.pts_us, (long long)lastShown); report = now;
}
idle();
}
}