766 lines
47 KiB
C++
766 lines
47 KiB
C++
#include "streamplayer_window.h"
|
||
#include "videoplayer_window.h"
|
||
#include "video_frame_image.h"
|
||
#include "sound_manager.h"
|
||
#include "../transport/stream_bridge.h"
|
||
#include <QVBoxLayout>
|
||
#include <QHBoxLayout>
|
||
#include <QLabel>
|
||
#include <QScrollArea>
|
||
#include <QStyle>
|
||
#include <QPushButton>
|
||
#include <QSlider>
|
||
#include <QCloseEvent>
|
||
#include <QKeyEvent>
|
||
#include <QTransform>
|
||
#include <atomic>
|
||
#include <thread>
|
||
#include <mutex>
|
||
#include <condition_variable>
|
||
#include <deque>
|
||
#include <chrono>
|
||
#include <algorithm>
|
||
#include <cstring>
|
||
#include <cerrno>
|
||
|
||
extern "C" {
|
||
#include "../../../lib/debug_config.h"
|
||
#include "../../../src/stream/stream.h"
|
||
#include "../../../src/stream/stream_playback_clock.h"
|
||
#include "../../../src/stream/stream_playback_audio.h"
|
||
#ifdef HAVE_FFMPEG
|
||
#include <libavcodec/avcodec.h>
|
||
#include <libswscale/swscale.h>
|
||
#endif
|
||
}
|
||
|
||
namespace {
|
||
int64_t nowUs() {
|
||
return std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
|
||
}
|
||
std::atomic<uint64_t> nextId{0};
|
||
QString text(const char* value) { return QCoreApplication::translate("ChatGui", value); }
|
||
|
||
/* Ровно восемь Unicode code points и один компактный знак многоточия; HTML имён экранируется. */
|
||
QString shortNodeName(const char* name) {
|
||
QString value = QString::fromUtf8(name); int end = 0, count = 0;
|
||
while (end < value.size() && count < 8) {
|
||
if (value.at(end).isHighSurrogate() && end + 1 < value.size() && value.at(end + 1).isLowSurrogate()) end++;
|
||
end++; count++;
|
||
}
|
||
return (value.left(end) + (end < value.size() ? QStringLiteral("…") : QString())).toHtmlEscaped();
|
||
}
|
||
|
||
QString bufferTime(int64_t us) {
|
||
if (us < 0) return QStringLiteral("?");
|
||
QString value = QString::number(us / 1000000.0, 'f', 1);
|
||
if (value.endsWith(QStringLiteral(".0"))) value.chop(2);
|
||
return value + QStringLiteral("S");
|
||
}
|
||
}
|
||
|
||
struct StreamPlayerWindow::Playback {
|
||
struct Video { QImage image; int64_t pts; };
|
||
struct AudioGap {
|
||
uint64_t count = 0, silentFrames = 0, episodes = 0;
|
||
int64_t at = 0;
|
||
unsigned requested = 0, received = 0;
|
||
unsigned pcmBefore = 0, pcmAfter = 0;
|
||
int tempo = 1000;
|
||
int64_t reserve = 0, feedAge = -1, workerAge = -1, audioPts = -1;
|
||
int64_t deviceLead = 0, guard = 0, minimum = 0, callbackGap = 0, pendingGain = 0;
|
||
size_t videoQueue = 0;
|
||
} audioGap;
|
||
uint64_t id = ++nextId, group = 0, source = 0, epoch = 0;
|
||
int64_t createdAt = nowUs(), firstInputAt = 0;
|
||
std::atomic<bool> running{true}, audioAvailable{false};
|
||
std::atomic<bool> complete{false}, failed{false};
|
||
std::atomic<float> volume{1.0f};
|
||
float outputVolume = 0.0f;
|
||
uint64_t audioGeneration = 0;
|
||
std::thread worker, videoWorker;
|
||
std::deque<StreamRecord> videoInput; // Audio worker -> video worker, под lock; максимум два входа.
|
||
uint64_t videoGeneration = 1;
|
||
bool videoBusy = false, videoInputEnded = false;
|
||
std::mutex lock;
|
||
std::mutex wakeLock;
|
||
std::condition_variable wake;
|
||
uint64_t wakeVersion = 0, dataSignals = 0;
|
||
std::deque<Video> videos;
|
||
size_t videoBytes = 0;
|
||
stream_playback_audio* sound = nullptr;
|
||
stream_playback_clock adaptation{};
|
||
stream_playback_state timing{};
|
||
stream_media_status media{};
|
||
int64_t position = -1, clockAt = 0, ready = -1, audioDacAt = 0, deviceLead = 0, lastShownPts = -1;
|
||
int64_t decoderLead = 0, framePeriod = 33333, lastInputPts = -1, lastDecodedPts = -1;
|
||
uint64_t audioFrameStart = 0, shown = 0, discarded = 0, underruns = 0, endFrames = UINT64_MAX;
|
||
bool started = false, presenting = false, videoReady = false, ended = false, videoEnded = false;
|
||
bool activeAudio = false, audioEnded = false, resync = false, audioUnderrunning = false;
|
||
int64_t audioFedAt = 0, audioFedPts = -1, workerAt = 0, guiAt = 0, guiGapMax = 0, audioCallbackAt = 0;
|
||
~Playback() { stream_playback_audio_destroy(sound); }
|
||
|
||
// Отдельный lock: уведомление из ядра не ждёт codec/PCM lock и не вызывает bridge.
|
||
void notify(bool data = false) {
|
||
{ std::lock_guard<std::mutex> guard(wakeLock); wakeVersion++; if (data) dataSignals++; }
|
||
wake.notify_all();
|
||
}
|
||
|
||
uint64_t played(int64_t now) const {
|
||
if (!sound || !audioDacAt) return 0;
|
||
long double frames = (long double)audioFrameStart + (long double)(now - audioDacAt) * 48000 / 1000000;
|
||
uint64_t count = frames <= 0 ? 0 : (uint64_t)frames;
|
||
return std::min(count, stream_playback_audio_written(sound));
|
||
}
|
||
int64_t clock(int64_t now) {
|
||
if (!started) return 0;
|
||
if (!presenting) return position;
|
||
if (activeAudio && sound && audioAvailable && !(audioEnded && endFrames != UINT64_MAX && played(now) >= endFrames))
|
||
return std::max(position, stream_playback_audio_position(sound, played(now)));
|
||
int64_t proposed = position + (now - clockAt) * adaptation.tempo_milli / 1000;
|
||
return std::max(position, std::min(ready, proposed));
|
||
}
|
||
};
|
||
|
||
StreamPlayerWindow::StreamPlayerWindow(uint64_t group, uint64_t source, uint64_t epoch, const QString& name, QWidget* parent, bool embedded)
|
||
: QWidget(parent), m_playback(std::make_unique<Playback>()) {
|
||
m_playback->group = group; m_playback->source = source; m_playback->epoch = epoch;
|
||
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop viewer requested id=%llu source=%llx epoch=%llx",
|
||
(unsigned long long)m_playback->id, (unsigned long long)source, (unsigned long long)epoch);
|
||
resize(800, 500);
|
||
setObjectName(QStringLiteral("streamPlayer_%1").arg(source));
|
||
auto* layout = new QVBoxLayout(this); m_title = new QLabel(this); layout->addWidget(m_title);
|
||
setSourceName(name);
|
||
m_status = new QLabel(text("Connecting…"), this); layout->addWidget(m_status);
|
||
m_render = new VideoRenderWidget(this, true); layout->addWidget(m_render, 1);
|
||
m_telemetry = new QLabel(this); m_telemetry->setObjectName(QStringLiteral("streamTelemetry"));
|
||
m_telemetry->setTextFormat(Qt::RichText); m_telemetry->setAlignment(Qt::AlignCenter);
|
||
auto* telemetryScroll = new QScrollArea(this); telemetryScroll->setFrameShape(QFrame::NoFrame);
|
||
telemetryScroll->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); telemetryScroll->setAlignment(Qt::AlignCenter);
|
||
telemetryScroll->setFixedHeight(fontMetrics().height() * 2 + style()->pixelMetric(QStyle::PM_ScrollBarExtent) + 4);
|
||
telemetryScroll->setWidget(m_telemetry); layout->addWidget(telemetryScroll);
|
||
auto* controls = new QHBoxLayout;
|
||
auto* volume = new QSlider(Qt::Horizontal, this); volume->setRange(0, 100); volume->setValue(100);
|
||
volume->setAccessibleName(text("Stream volume")); controls->addWidget(new QLabel(text("Volume"), this)); controls->addWidget(volume);
|
||
auto* fullscreen = new QPushButton(text("Fullscreen"), this); controls->addWidget(fullscreen);
|
||
auto* close = new QPushButton(text("Close player"), this); controls->addWidget(close); layout->addLayout(controls);
|
||
if (embedded) {
|
||
fullscreen->hide(); close->hide(); layout->setContentsMargins(3, 3, 3, 3); layout->setSpacing(2);
|
||
m_title->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
|
||
m_status->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred); setMinimumSize(0, 0);
|
||
}
|
||
connect(volume, &QSlider::valueChanged, this, [this](int value) { m_playback->volume = value / 100.0f; });
|
||
connect(fullscreen, &QPushButton::clicked, this, [this] { if (isFullScreen()) showNormal(); else showFullScreen(); });
|
||
connect(close, &QPushButton::clicked, this, &QWidget::close);
|
||
connect(this, &StreamPlayerWindow::statusChanged, m_status, &QLabel::setText, Qt::QueuedConnection);
|
||
attachAudio();
|
||
connect(&m_timer, &QTimer::timeout, this, &StreamPlayerWindow::tick); m_timer.start(15);
|
||
m_playback->videoWorker = std::thread(&StreamPlayerWindow::decodeVideo, this);
|
||
m_playback->worker = std::thread(&StreamPlayerWindow::decode, this);
|
||
}
|
||
|
||
StreamPlayerWindow::~StreamPlayerWindow() { stop(); }
|
||
|
||
/* Имя относится к представлению, а не идентичности media-сессии. */
|
||
void StreamPlayerWindow::setSourceName(const QString& name) {
|
||
m_title->setText(name); m_title->setToolTip(name);
|
||
setWindowTitle(text("Live stream: %1").arg(name));
|
||
DEBUG_DEBUG(DEBUG_CATEGORY_STREAM, "desktop viewer name id=%llu source=%llx epoch=%llx name=%s",
|
||
(unsigned long long)m_playback->id, (unsigned long long)m_playback->source,
|
||
(unsigned long long)m_playback->epoch, qPrintable(name));
|
||
}
|
||
|
||
bool StreamPlayerWindow::finished() const { return m_playback->complete; }
|
||
bool StreamPlayerWindow::failed() const { return m_playback->failed; }
|
||
int StreamPlayerWindow::drainTimeoutMs() const {
|
||
auto& p = *m_playback; std::lock_guard<std::mutex> guard(p.lock);
|
||
return p.complete ? 0 : static_cast<int>(std::clamp<int64_t>((p.ready - p.clock(nowUs())) / 1000 + 2000, 2000, 30000));
|
||
}
|
||
|
||
/* Hardware callback заполняет только ближайшую порцию; source time тишины остаётся неподвижным. */
|
||
void StreamPlayerWindow::attachAudio() {
|
||
if (!m_playback->running || m_audioAttached) return;
|
||
m_playback->audioAvailable = SoundManager::instance()->outputAvailable();
|
||
m_audioAttached = SoundManager::instance()->attachStream(m_playback->id,
|
||
[this](float* output, unsigned frames, int64_t firstTimeUs, uint64_t generation) {
|
||
int16_t pcm[960 * 2] = {};
|
||
auto& p = *m_playback; std::lock_guard<std::mutex> guard(p.lock);
|
||
p.notify();
|
||
int64_t now = nowUs();
|
||
int64_t callbackGap = p.audioCallbackAt ? now - p.audioCallbackAt : 0; p.audioCallbackAt = now;
|
||
bool restored = !p.audioAvailable.exchange(true);
|
||
if ((p.audioGeneration && p.audioGeneration != generation) || (restored && p.started)) p.resync = true;
|
||
p.audioGeneration = generation;
|
||
p.deviceLead = std::max<int64_t>(0, firstTimeUs - now);
|
||
if (!p.sound || !p.started || p.resync) return;
|
||
p.audioFrameStart = stream_playback_audio_written(p.sound); p.audioDacAt = firstTimeUs;
|
||
unsigned pcmBefore = stream_playback_audio_buffered(p.sound);
|
||
bool buffering = stream_playback_audio_buffering(p.sound);
|
||
int count = !p.presenting ? 0 : stream_playback_audio_pull(p.sound, pcm, frames, INT64_MAX);
|
||
if (count < 0) { p.resync = true; count = 0; }
|
||
if ((unsigned)count < frames) {
|
||
stream_playback_audio_silence(p.sound, frames - (unsigned)count);
|
||
if (p.presenting && !p.audioEnded) p.audioGap.silentFrames += frames - (unsigned)count;
|
||
if (p.presenting && !p.audioEnded && !buffering) {
|
||
p.underruns++; p.audioGap.count++;
|
||
if (!p.audioUnderrunning) { p.audioGap.episodes++; p.audioUnderrunning = true; }
|
||
p.audioGap.at = now;
|
||
p.audioGap.requested = frames; p.audioGap.received = (unsigned)count;
|
||
p.audioGap.reserve = p.ready - p.position; p.audioGap.videoQueue = p.videos.size();
|
||
p.audioGap.feedAge = p.audioFedAt ? now - p.audioFedAt : -1; p.audioGap.audioPts = p.audioFedPts;
|
||
p.audioGap.workerAge = p.workerAt ? now - p.workerAt : -1;
|
||
p.audioGap.pcmBefore = pcmBefore; p.audioGap.pcmAfter = stream_playback_audio_buffered(p.sound);
|
||
p.audioGap.tempo = p.adaptation.tempo_milli; p.audioGap.deviceLead = p.deviceLead;
|
||
p.audioGap.guard = p.timing.guard_us; p.audioGap.minimum = p.timing.minimum_us;
|
||
p.audioGap.callbackGap = callbackGap; p.audioGap.pendingGain = p.timing.pending_gain_us;
|
||
}
|
||
} else p.audioUnderrunning = false;
|
||
float volume = p.volume.load();
|
||
for (int i = 0; i < count; i++) {
|
||
p.outputVolume += std::clamp(volume - p.outputVolume, -1.0f / 480, 1.0f / 480);
|
||
output[2 * i] += pcm[2 * i] * (p.outputVolume / 32768.0f);
|
||
output[2 * i + 1] += pcm[2 * i + 1] * (p.outputVolume / 32768.0f);
|
||
}
|
||
if (p.audioEnded && p.endFrames == UINT64_MAX && stream_playback_audio_empty(p.sound))
|
||
p.endFrames = stream_playback_audio_written(p.sound);
|
||
});
|
||
if (!m_audioAttached) {
|
||
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "desktop stream audio attach failed id=%llu", (unsigned long long)m_playback->id);
|
||
m_playback->audioAvailable = false;
|
||
}
|
||
}
|
||
|
||
/* GUI держит только ближайшие готовые изображения; накопленный поток остаётся в RS-кольце. */
|
||
void StreamPlayerWindow::tick() {
|
||
auto& p = *m_playback;
|
||
if (!SoundManager::instance()->outputAvailable()) p.audioAvailable = false;
|
||
QImage image; bool finished = false, first = false;
|
||
int64_t shownPts = -1, readyPts = -1, mediaPosition = -1, deviceLead = 0;
|
||
{
|
||
std::lock_guard<std::mutex> guard(p.lock);
|
||
int64_t now = nowUs();
|
||
if (p.guiAt) p.guiGapMax = std::max(p.guiGapMax, now - p.guiAt);
|
||
p.guiAt = now;
|
||
int64_t clock = p.clock(now);
|
||
while (p.started && !p.videos.empty() && p.videos.front().pts <= clock) {
|
||
if (!image.isNull()) p.discarded++;
|
||
p.videoBytes -= static_cast<size_t>(p.videos.front().image.sizeInBytes());
|
||
p.lastShownPts = p.videos.front().pts; image = std::move(p.videos.front().image); p.videos.pop_front();
|
||
}
|
||
if (!image.isNull()) {
|
||
first = p.shown++ == 0;
|
||
shownPts = p.lastShownPts; readyPts = p.ready; mediaPosition = clock; deviceLead = p.deviceLead;
|
||
}
|
||
bool audioDone = !p.activeAudio || !p.audioAvailable || (p.endFrames != UINT64_MAX && p.played(nowUs()) >= p.endFrames);
|
||
finished = p.ended && p.videoEnded && p.videos.empty() && audioDone;
|
||
}
|
||
p.notify();
|
||
if (nowUs() - m_telemetryAt >= STREAM_TELEMETRY_PERIOD_MS * 1000) {
|
||
m_telemetryAt = nowUs();
|
||
stream_viewer_telemetry t{};
|
||
if (gui_stream_telemetry(p.id, t)) {
|
||
int64_t reserve = -1; int tempo = 1000;
|
||
{
|
||
std::lock_guard<std::mutex> guard(p.lock);
|
||
int64_t ready = p.activeAudio && !p.media.audio_ended ? p.media.ready_us : p.media.video_us;
|
||
if (p.started && ready >= 0) reserve = std::max<int64_t>(0, ready - p.clock(nowUs()));
|
||
if (p.presenting && !p.timing.waiting && !finished && !p.failed) tempo = p.adaptation.tempo_milli;
|
||
}
|
||
QString times, names, tooltip;
|
||
for (unsigned i = 0; i <= t.chain.count; i++) {
|
||
bool own = i == t.chain.count;
|
||
uint8_t flags = own ? STREAM_TELEMETRY_KNOWN : t.chain.rows[i].flags;
|
||
int64_t us = own ? reserve : (flags & STREAM_TELEMETRY_KNOWN) ? (int64_t)t.chain.rows[i].buffered_us : -1;
|
||
QString color = flags & STREAM_TELEMETRY_DOWN ? QStringLiteral("#c88888")
|
||
: flags & STREAM_TELEMETRY_STALE ? QStringLiteral("#888888") : m_telemetry->palette().color(QPalette::WindowText).name();
|
||
QString value = bufferTime(us);
|
||
if (flags & STREAM_TELEMETRY_DOWN) value += QStringLiteral(" ×");
|
||
else if ((flags & STREAM_TELEMETRY_STALE) && us >= 0) value += QStringLiteral(" ?");
|
||
if (i) { times += QStringLiteral("<td> → </td>"); names += QStringLiteral("<td></td>"); tooltip += QStringLiteral(" → "); }
|
||
times += QStringLiteral("<td align='center' style='color:%1'>%2</td>").arg(color, value);
|
||
names += QStringLiteral("<td align='center' style='color:%1'>%2</td>").arg(color, shortNodeName(t.names[i]));
|
||
tooltip += QString::fromUtf8(t.names[i]) + QStringLiteral(": ") + bufferTime(us);
|
||
}
|
||
if (tempo > 1000) {
|
||
QString rate = QString::number(tempo / 1000.0, 'f', 2);
|
||
if (rate.endsWith(QLatin1Char('0'))) rate.chop(1);
|
||
times += QStringLiteral("<td> %1x</td>").arg(rate); names += QStringLiteral("<td></td>");
|
||
}
|
||
m_telemetry->setText(QStringLiteral("<table cellspacing='0'><tr>%1</tr><tr>%2</tr></table>").arg(times, names));
|
||
m_telemetry->adjustSize();
|
||
m_telemetry->setToolTip(tooltip);
|
||
}
|
||
}
|
||
if (!image.isNull()) {
|
||
m_render->setFrame(image); m_status->setText(text("Live"));
|
||
if (first) DEBUG_INFO(DEBUG_CATEGORY_STREAM,
|
||
"desktop first frame id=%llu source=%llx epoch=%llx %dx%d RGB_stride=%lld stage=display_submit "
|
||
"elapsed_us=%lld pts=%lld clock_us=%lld ready_us=%lld ready_ahead_us=%lld av_us=%lld device_lead_us=%lld",
|
||
(unsigned long long)p.id, (unsigned long long)p.source, (unsigned long long)p.epoch,
|
||
image.width(), image.height(), (long long)image.bytesPerLine(), (long long)(nowUs() - p.createdAt),
|
||
(long long)shownPts, (long long)mediaPosition, (long long)readyPts, (long long)(readyPts - shownPts),
|
||
(long long)(shownPts - mediaPosition), (long long)deviceLead);
|
||
emit framePresented();
|
||
}
|
||
if (finished || p.failed) {
|
||
if (finished) m_status->setText(text("Stream ended"));
|
||
p.complete = true;
|
||
SoundManager::instance()->detachStream(p.id); m_audioAttached = false; m_timer.stop();
|
||
}
|
||
}
|
||
|
||
/* Callback устройства прекращается до освобождения DSP; worker снимает только свой reader. */
|
||
void StreamPlayerWindow::stop() {
|
||
if (!m_playback->running.exchange(false) && !m_playback->worker.joinable()) return;
|
||
m_playback->notify();
|
||
m_timer.stop(); gui_stream_stop(m_playback->id);
|
||
SoundManager::instance()->detachStream(m_playback->id); m_audioAttached = false; m_playback->audioAvailable = false;
|
||
if (m_playback->worker.joinable()) m_playback->worker.join();
|
||
if (m_playback->videoWorker.joinable()) m_playback->videoWorker.join();
|
||
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop player stopped id=%llu shown=%llu underruns=%llu",
|
||
(unsigned long long)m_playback->id, (unsigned long long)m_playback->shown, (unsigned long long)m_playback->underruns);
|
||
}
|
||
|
||
void StreamPlayerWindow::closeEvent(QCloseEvent* event) { stop(); QWidget::closeEvent(event); }
|
||
void StreamPlayerWindow::keyPressEvent(QKeyEvent* event) {
|
||
if (event->key() == Qt::Key_Escape) { if (isFullScreen()) showNormal(); else close(); }
|
||
else QWidget::keyPressEvent(event);
|
||
}
|
||
|
||
/* Один владелец reader и DSP. Видео никогда не блокирует получение/подготовку аудио. */
|
||
void StreamPlayerWindow::decode() {
|
||
auto& p = *m_playback;
|
||
#ifndef HAVE_FFMPEG
|
||
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "desktop live decoder unavailable: FFmpeg disabled");
|
||
p.failed = p.complete = true; emit statusChanged(text("Live video playback requires FFmpeg"));
|
||
#else
|
||
std::deque<StreamRecord> records;
|
||
uint64_t reportedGaps = 0, observedGaps = 0, reportedEpisodes = 0;
|
||
int64_t reportAt = nowUs(), gapReportAt = 0;
|
||
int64_t playReportAt = reportAt;
|
||
uint64_t shownBase = 0, discardedBase = 0, underrunBase = 0, silenceBase = 0;
|
||
uint64_t audioPackets = 0, videoPackets = 0, batches = 0, batchMax = 0;
|
||
int64_t feedMax = 0, pullMax = 0, workMax = 0;
|
||
auto fail = [&](const QString& reason, bool retry = true) {
|
||
p.failed = retry; p.complete = true; p.running = false;
|
||
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "desktop audio worker failed id=%llu retry=%d: %s",
|
||
(unsigned long long)p.id, retry, qPrintable(reason));
|
||
emit statusChanged(reason); p.notify();
|
||
};
|
||
auto reset = [&](bool request) {
|
||
records.clear();
|
||
stream_playback_audio* previous;
|
||
{
|
||
std::lock_guard<std::mutex> guard(p.lock);
|
||
previous = p.sound; p.sound = nullptr; ++p.videoGeneration;
|
||
p.videoInput.clear(); p.videoInputEnded = p.videoBusy = false;
|
||
p.videos.clear(); p.videoBytes = 0; p.started = p.presenting = p.videoReady = false;
|
||
p.position = p.ready = -1; p.decoderLead = 0; p.framePeriod = 33333;
|
||
p.lastInputPts = p.lastDecodedPts = p.lastShownPts = -1;
|
||
p.ended = p.videoEnded = p.audioEnded = p.resync = false;
|
||
p.audioDacAt = 0; p.audioFrameStart = 0; p.endFrames = UINT64_MAX;
|
||
p.audioFedAt = 0; p.audioFedPts = -1; observedGaps = p.underruns; p.audioUnderrunning = false;
|
||
}
|
||
stream_playback_audio_destroy(previous); p.notify();
|
||
if (request && !gui_stream_resync(p.id)) DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop playback resync refused id=%llu",
|
||
(unsigned long long)p.id);
|
||
emit statusChanged(text("Waiting for a key frame…"));
|
||
};
|
||
auto observe = [&] {
|
||
stream_media_status status{};
|
||
if (!gui_stream_status(p.id, status)) return;
|
||
if (status.initialized && !p.started) {
|
||
bool audio = status.audio_enabled && p.audioAvailable;
|
||
auto* sound = audio ? stream_playback_audio_create(status.start_us, 250) : nullptr;
|
||
if (audio && !sound) { fail(text("Stream audio decoder failed")); return; }
|
||
std::lock_guard<std::mutex> guard(p.lock);
|
||
p.sound = sound; p.activeAudio = audio; p.position = status.start_us;
|
||
p.clockAt = nowUs(); p.started = true;
|
||
stream_playback_clock_reset(&p.adaptation, p.id);
|
||
DEBUG_INFO(DEBUG_CATEGORY_STREAM,
|
||
"desktop playback starts id=%llu pts=%lld elapsed_us=%lld audio=%d PCM_prefill_resume_ms=250 separate_video=1",
|
||
(unsigned long long)p.id, (long long)p.position, (long long)(nowUs() - p.createdAt), audio);
|
||
}
|
||
std::lock_guard<std::mutex> guard(p.lock);
|
||
p.media = status;
|
||
if (status.initialized)
|
||
p.ready = p.activeAudio && !status.audio_ended ? status.audio_us : std::max(status.audio_us, status.video_us);
|
||
};
|
||
if (!gui_stream_open(p.id, p.group, p.source, p.epoch,
|
||
[](void* arg) { static_cast<Playback*>(arg)->notify(true); }, &p)) fail(text("Could not connect to the stream"));
|
||
else while (p.running && !p.failed) {
|
||
uint64_t version;
|
||
{ std::lock_guard<std::mutex> guard(p.wakeLock); version = p.wakeVersion; }
|
||
int64_t began = nowUs();
|
||
bool resync;
|
||
Playback::AudioGap gap;
|
||
{ std::lock_guard<std::mutex> guard(p.lock); resync = p.resync; p.workerAt = began; gap = p.audioGap; }
|
||
if (gap.episodes != reportedEpisodes || (gap.count != reportedGaps && began - gapReportAt >= 1000000)) {
|
||
DEBUG_WARN(DEBUG_CATEGORY_STREAM,
|
||
"desktop audio gap id=%llu count=%llu delta=%llu episodes=%llu event_us=%lld report_delay_us=%lld "
|
||
"silence_frames=%llu got=%u/%u reserve_us=%lld "
|
||
"feed_age_us=%lld worker_age_us=%lld audio_pts=%lld videoQ=%zu "
|
||
"pcm_before=%u pcm_after=%u tempo=%d device_us=%lld guard_us=%lld minimum_us=%lld callback_gap_us=%lld pending_gain_us=%lld",
|
||
(unsigned long long)p.id, (unsigned long long)gap.count, (unsigned long long)(gap.count - reportedGaps),
|
||
(unsigned long long)gap.episodes, (long long)gap.at, (long long)(began - gap.at),
|
||
(unsigned long long)gap.silentFrames, gap.received, gap.requested, (long long)gap.reserve,
|
||
(long long)gap.feedAge, (long long)gap.workerAge, (long long)gap.audioPts, gap.videoQueue,
|
||
gap.pcmBefore, gap.pcmAfter, gap.tempo, (long long)gap.deviceLead, (long long)gap.guard,
|
||
(long long)gap.minimum, (long long)gap.callbackGap, (long long)gap.pendingGain);
|
||
reportedGaps = gap.count; reportedEpisodes = gap.episodes; gapReportAt = began;
|
||
}
|
||
if (resync) reset(true);
|
||
observe();
|
||
if (p.sound && stream_playback_audio_prepare(p.sound) < 0) { fail(text("Stream audio decoder failed")); break; }
|
||
int64_t position, audioAhead;
|
||
unsigned videoBudget;
|
||
int tempo = 1000;
|
||
{
|
||
std::lock_guard<std::mutex> guard(p.lock);
|
||
if (p.ended && p.videoEnded && (!p.sound || stream_playback_audio_empty(p.sound))) break;
|
||
int64_t now = nowUs(); position = p.started ? p.clock(now) : -1;
|
||
videoBudget = p.videoInputEnded ? 0 : (unsigned)std::max(0,
|
||
2 - (int)p.videos.size() - (int)p.videoInput.size() - (p.videoBusy ? 1 : 0));
|
||
audioAhead = !p.audioEnded && (!p.sound || stream_playback_audio_needs_input(p.sound)) ? p.deviceLead + 500000 : -1;
|
||
if (p.presenting && p.ready >= 0) {
|
||
uint64_t played = p.played(now);
|
||
bool starved = p.underruns != observedGaps; observedGaps = p.underruns;
|
||
if (p.sound && stream_playback_audio_buffering(p.sound)) starved = true;
|
||
if (!p.activeAudio && p.position + (now - p.clockAt) * p.adaptation.tempo_milli / 1000 > p.ready) starved = true;
|
||
int64_t guardUs = p.activeAudio ? std::max<int64_t>(40000, p.deviceLead + 20000) : p.framePeriod;
|
||
if (p.sound) guardUs += stream_playback_audio_latency_us(p.sound);
|
||
int64_t pending = p.sound ? stream_playback_audio_pending_gain(p.sound, played) : 0;
|
||
if (stream_playback_clock_update(&p.adaptation, now, std::max(p.position, position), p.ready,
|
||
guardUs, pending, !p.ended && starved, &p.timing) < 0) p.resync = true;
|
||
else { p.position = std::max(p.position, position); p.clockAt = now; tempo = p.timing.tempo_milli; }
|
||
}
|
||
if (began - reportAt >= 1000000) {
|
||
DEBUG_TRACE(DEBUG_CATEGORY_VIDEO,
|
||
"desktop viewer work id=%llu interval_us=%lld audio=%llu video=%llu batches=%llu batch_max=%llu "
|
||
"feed_max_us=%lld pull_max_us=%lld work_max_us=%lld PCM_frames=%u tempo=%d clock_us=%lld ready_us=%lld "
|
||
"reserve_us=%lld underruns=%llu silence_frames=%llu videoQ=%zu video_input=%zu video_busy=%d "
|
||
"gui_gap_max_us=%lld generation=%llu",
|
||
(unsigned long long)p.id, (long long)(began - reportAt), (unsigned long long)audioPackets,
|
||
(unsigned long long)videoPackets, (unsigned long long)batches, (unsigned long long)batchMax,
|
||
(long long)feedMax, (long long)pullMax, (long long)workMax, stream_playback_audio_buffered(p.sound),
|
||
tempo, (long long)p.position, (long long)p.ready, (long long)(p.ready - p.position),
|
||
(unsigned long long)p.underruns, (unsigned long long)p.audioGap.silentFrames, p.videos.size(),
|
||
p.videoInput.size(), p.videoBusy, (long long)p.guiGapMax, (unsigned long long)p.videoGeneration);
|
||
reportAt = began; audioPackets = videoPackets = batches = batchMax = 0;
|
||
feedMax = pullMax = workMax = 0; p.guiGapMax = 0;
|
||
}
|
||
if (began - playReportAt >= 500000) {
|
||
if (debug_should_output(DEBUG_LEVEL_DEBUG, DEBUG_CATEGORY_VIDEO)) {
|
||
auto ms = [](int64_t us, bool valid = true) {
|
||
return valid ? QByteArray::number(us / 1000.0, 'f', 1) : QByteArray("na");
|
||
};
|
||
bool timing = p.started && p.adaptation.initialized;
|
||
DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO,
|
||
"stream PLAY group=%llx source=%llx epoch=%llx id=%llu dt_ms=%lld generation=%llu "
|
||
"TIME{position_ms=%s ready_ms=%s reserve_ms=%s minimum_ms=%s guard_ms=%s tempo=%d reason=%s "
|
||
"correction_ms=%s pending_gain_ms=%s} "
|
||
"VIDEO{shown=%llu drop=%llu pts_ms=%s av_ms=%s queue=%zu input=%zu decoder_lead_ms=%s wait_key=%d} "
|
||
"AUDIO{enabled=%d pcm_ms=%.1f buffering=%d device_ms=%s underruns=%llu silence_frames=%llu} waiting=%d ended=%d",
|
||
(unsigned long long)p.group, (unsigned long long)p.source, (unsigned long long)p.epoch,
|
||
(unsigned long long)p.id, (long long)((began - playReportAt) / 1000), (unsigned long long)p.media.generation,
|
||
ms(p.position, p.started).constData(), ms(p.ready, p.ready >= 0).constData(),
|
||
ms(p.ready - p.position, p.started && p.ready >= 0).constData(), ms(p.timing.minimum_us, timing).constData(),
|
||
ms(p.timing.guard_us, timing).constData(), p.adaptation.tempo_milli ? p.adaptation.tempo_milli : 1000,
|
||
timing ? stream_playback_reason_name(p.timing.reason) : p.started ? "key" : "opening",
|
||
ms(p.timing.correction_us, timing).constData(), ms(p.timing.pending_gain_us, timing).constData(),
|
||
(unsigned long long)(p.shown - shownBase), (unsigned long long)(p.discarded - discardedBase),
|
||
ms(p.lastShownPts, p.lastShownPts >= 0).constData(),
|
||
ms(p.lastShownPts - position, p.lastShownPts >= 0 && position >= 0).constData(),
|
||
p.videos.size(), p.videoInput.size(), ms(p.decoderLead, p.videoReady).constData(), !p.videoReady,
|
||
p.activeAudio, p.sound ? stream_playback_audio_buffered(p.sound) / 48.0 : 0,
|
||
stream_playback_audio_buffering(p.sound),
|
||
ms(p.deviceLead, p.activeAudio && p.audioDacAt).constData(),
|
||
(unsigned long long)(p.underruns - underrunBase),
|
||
(unsigned long long)(p.audioGap.silentFrames - silenceBase), timing && p.timing.waiting, p.ended);
|
||
}
|
||
playReportAt = began; shownBase = p.shown; discardedBase = p.discarded;
|
||
underrunBase = p.underruns; silenceBase = p.audioGap.silentFrames;
|
||
}
|
||
}
|
||
if (p.sound) stream_playback_audio_set_tempo(p.sound, tempo);
|
||
auto urgent = std::find_if(records.begin(), records.end(), [](const StreamRecord& r) {
|
||
return r.kind == StreamRecordKind::Gap || r.kind == StreamRecordKind::Closed;
|
||
});
|
||
if (urgent != records.end()) records.erase(records.begin(), urgent);
|
||
auto next = records.end();
|
||
unsigned pendingVideo = 0, pendingAudio = 0; bool barrier = false;
|
||
for (auto it = records.begin(); it != records.end(); ++it) {
|
||
bool video = it->kind == StreamRecordKind::Video, audio = it->kind == StreamRecordKind::Audio;
|
||
pendingVideo += video; pendingAudio += audio;
|
||
if (it->kind == StreamRecordKind::VideoEnd || it->kind == StreamRecordKind::AudioEnd) {
|
||
if (next == records.end() && (it->kind == StreamRecordKind::VideoEnd ? !pendingVideo : !pendingAudio)) next = it;
|
||
continue;
|
||
}
|
||
if (!video && !audio) { barrier = true; if (it == records.begin()) next = it; break; }
|
||
if (next == records.end() && ((video && videoBudget) || (audio && audioAhead >= 0))) next = it;
|
||
}
|
||
if (next == records.end()) {
|
||
size_t previous = records.size(); int64_t at = nowUs();
|
||
bool received = !p.ended && !barrier && gui_stream_pull_batch(p.id, records, position,
|
||
videoBudget > pendingVideo ? videoBudget - pendingVideo : 0, pendingAudio ? -1 : audioAhead);
|
||
pullMax = std::max(pullMax, nowUs() - at); workMax = std::max(workMax, nowUs() - began);
|
||
if (received) { ++batches; batchMax = std::max<uint64_t>(batchMax, records.size() - previous); continue; }
|
||
if (!gui_stream_ready()) { fail(text("Chat service is stopped")); break; }
|
||
/* Отсутствие DATA восстанавливает ядро через 5с; сборка большого KEY не является ошибкой соединения. */
|
||
std::unique_lock<std::mutex> guard(p.wakeLock);
|
||
p.wake.wait_for(guard, std::chrono::milliseconds(50), [&] { return !p.running || p.failed || p.wakeVersion != version; });
|
||
continue;
|
||
}
|
||
StreamRecord record = std::move(*next); records.erase(next);
|
||
if (record.kind == StreamRecordKind::Opened) continue;
|
||
if (record.kind == StreamRecordKind::Gap) { reset(false); continue; }
|
||
if (record.kind == StreamRecordKind::Closed) {
|
||
const char* reason = record.reason == STREAM_CLOSE_SOURCE_CHANGED ? "The stream restarted. Select a source again."
|
||
: record.reason == STREAM_CLOSE_MEMBER ? "The source is no longer a channel member"
|
||
: record.reason == STREAM_CLOSE_SERVICE ? "Chat service is stopped"
|
||
: record.reason == STREAM_CLOSE_GROUP ? "The channel is closed" : "Could not continue the stream";
|
||
fail(text(reason), record.reason == STREAM_CLOSE_FAILED); break;
|
||
}
|
||
if (record.kind == StreamRecordKind::VideoEnd || record.kind == StreamRecordKind::AudioEnd || record.kind == StreamRecordKind::End) {
|
||
if (record.kind != StreamRecordKind::VideoEnd && p.sound && !p.audioEnded) stream_playback_audio_finish(p.sound);
|
||
{
|
||
std::lock_guard<std::mutex> guard(p.lock);
|
||
if (record.kind != StreamRecordKind::AudioEnd) p.videoInputEnded = true;
|
||
if (record.kind != StreamRecordKind::VideoEnd) p.audioEnded = true;
|
||
if (record.kind == StreamRecordKind::End) p.ended = true;
|
||
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop track end id=%llu track=%s video_input_end=%d audio_input_end=%d pending=%zu",
|
||
(unsigned long long)p.id, record.kind == StreamRecordKind::VideoEnd ? "video"
|
||
: record.kind == StreamRecordKind::AudioEnd ? "audio" : "all", p.videoInputEnded, p.audioEnded, records.size());
|
||
}
|
||
p.notify(); continue;
|
||
}
|
||
observe();
|
||
if (record.pts > INT64_MAX) { fail(text("Invalid stream timestamp")); break; }
|
||
if (record.kind == StreamRecordKind::Audio) {
|
||
++audioPackets; int64_t at = nowUs();
|
||
int result = p.sound ? stream_playback_audio_feed(p.sound, (int64_t)record.pts,
|
||
reinterpret_cast<const uint8_t*>(record.data.constData()), (size_t)record.data.size()) : 0;
|
||
feedMax = std::max(feedMax, nowUs() - at);
|
||
if (result == -2) reset(true);
|
||
else if (result < 0) { fail(text("Stream audio decoder failed")); break; }
|
||
else if (p.sound && result == 0) {
|
||
std::lock_guard<std::mutex> guard(p.lock); p.audioFedAt = nowUs(); p.audioFedPts = (int64_t)record.pts;
|
||
}
|
||
} else {
|
||
++videoPackets;
|
||
{ std::lock_guard<std::mutex> guard(p.lock); p.videoInput.push_back(std::move(record)); }
|
||
p.notify();
|
||
}
|
||
workMax = std::max(workMax, nowUs() - began);
|
||
}
|
||
#endif
|
||
gui_stream_stop(p.id); p.notify();
|
||
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop audio worker released id=%llu", (unsigned long long)p.id);
|
||
}
|
||
|
||
/* Единственный владелец FFmpeg. Изображения/входы ограничены; generation отменяет результаты старого decode. */
|
||
void StreamPlayerWindow::decodeVideo() {
|
||
#ifdef HAVE_FFMPEG
|
||
auto& p = *m_playback;
|
||
AVCodecContext* decoder = nullptr; AVFrame* frame = av_frame_alloc(); AVPacket* packet = av_packet_alloc();
|
||
SwsContext* converter = nullptr;
|
||
stream_media_format format{};
|
||
uint64_t generation = 0, decoded = 0, discarded = 0;
|
||
uint32_t revision = 0;
|
||
bool eosSent = false;
|
||
int64_t reportAt = nowUs(), codecTotal = 0, codecMax = 0, convertTotal = 0, convertMax = 0, progressAt = nowUs();
|
||
auto fail = [&](const QString& reason) {
|
||
p.failed = p.complete = true;
|
||
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "desktop video worker failed id=%llu: %s", (unsigned long long)p.id, qPrintable(reason));
|
||
emit statusChanged(reason); p.notify();
|
||
};
|
||
auto codecTime = [&](int64_t at) {
|
||
int64_t elapsed = nowUs() - at; codecTotal += elapsed; codecMax = std::max(codecMax, elapsed);
|
||
};
|
||
if (!frame || !packet) fail(text("Stream decoder allocation failed"));
|
||
while (p.running && !p.failed) {
|
||
uint64_t version;
|
||
{ std::lock_guard<std::mutex> guard(p.wakeLock); version = p.wakeVersion; }
|
||
uint64_t current; bool space, videoDone;
|
||
{
|
||
std::lock_guard<std::mutex> guard(p.lock);
|
||
current = p.videoGeneration; space = p.videos.size() < 2; videoDone = p.videoEnded;
|
||
}
|
||
if (current != generation) {
|
||
avcodec_free_context(&decoder); av_packet_unref(packet); revision = 0; eosSent = false;
|
||
generation = current; progressAt = nowUs();
|
||
DEBUG_TRACE(DEBUG_CATEGORY_VIDEO, "desktop video generation id=%llu generation=%llu",
|
||
(unsigned long long)p.id, (unsigned long long)generation);
|
||
}
|
||
if (videoDone) {
|
||
std::unique_lock<std::mutex> guard(p.wakeLock);
|
||
p.wake.wait_for(guard, std::chrono::milliseconds(50), [&] { return !p.running || p.failed || p.wakeVersion != version; });
|
||
continue;
|
||
}
|
||
bool worked = false;
|
||
if (decoder && space) {
|
||
int64_t at = nowUs(); int result = avcodec_receive_frame(decoder, frame); codecTime(at);
|
||
{ std::lock_guard<std::mutex> guard(p.lock); if (generation != p.videoGeneration) continue; }
|
||
if (result == AVERROR_EOF) {
|
||
{ std::lock_guard<std::mutex> guard(p.lock);
|
||
if (generation != p.videoGeneration) continue;
|
||
p.videoEnded = true; p.videoBusy = false; }
|
||
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop decoder drained id=%llu decoded=%llu discarded=%llu",
|
||
(unsigned long long)p.id, (unsigned long long)decoded, (unsigned long long)discarded);
|
||
p.notify(); continue;
|
||
}
|
||
if (result < 0 && result != AVERROR(EAGAIN)) { fail(text("Stream video decoder failed")); break; }
|
||
if (result == 0) {
|
||
++decoded; progressAt = nowUs(); worked = true;
|
||
if (frame->width <= 0 || frame->width > 4096 || frame->height <= 0 || frame->height > 4096) {
|
||
fail(text("Invalid stream video dimensions")); break;
|
||
}
|
||
int64_t pts;
|
||
{
|
||
std::lock_guard<std::mutex> guard(p.lock);
|
||
if (generation != p.videoGeneration) continue;
|
||
pts = frame->best_effort_timestamp == AV_NOPTS_VALUE ? p.lastInputPts : frame->best_effort_timestamp;
|
||
p.decoderLead = std::max(p.decoderLead, p.lastInputPts - pts); p.lastDecodedPts = pts; p.videoReady = true;
|
||
if (!p.presenting) {
|
||
p.presenting = true; p.clockAt = nowUs();
|
||
DEBUG_INFO(DEBUG_CATEGORY_VIDEO,
|
||
"desktop first decoded frame id=%llu pts=%lld elapsed_us=%lld decode_us=%lld",
|
||
(unsigned long long)p.id, (long long)pts, (long long)(nowUs() - p.createdAt),
|
||
(long long)(nowUs() - p.firstInputAt));
|
||
}
|
||
if (pts < p.clock(nowUs()) - 100000) { ++discarded; ++p.discarded; continue; }
|
||
}
|
||
int64_t convertAt = nowUs();
|
||
converter = sws_getCachedContext(converter, frame->width, frame->height, (AVPixelFormat)frame->format,
|
||
frame->width, frame->height, AV_PIX_FMT_RGB24, SWS_BILINEAR, nullptr, nullptr, nullptr);
|
||
QImage image = video_frame_image(frame->width, frame->height);
|
||
if (!converter || image.isNull()) { fail(text("Stream video conversion failed")); break; }
|
||
uint8_t* destination[] = {image.bits(), nullptr, nullptr, nullptr};
|
||
int strides[] = {(int)image.bytesPerLine(), 0, 0, 0};
|
||
if (sws_scale(converter, frame->data, frame->linesize, 0, frame->height, destination, strides) != frame->height) {
|
||
fail(text("Stream video conversion failed")); break;
|
||
}
|
||
if (format.rotation) image = image.transformed(QTransform().rotate(format.rotation));
|
||
int64_t elapsed = nowUs() - convertAt; convertTotal += elapsed; convertMax = std::max(convertMax, elapsed);
|
||
{
|
||
std::lock_guard<std::mutex> guard(p.lock);
|
||
if (generation != p.videoGeneration) continue;
|
||
p.videoBytes += (size_t)image.sizeInBytes(); p.videos.push_back({std::move(image), pts});
|
||
}
|
||
p.notify(); continue;
|
||
}
|
||
}
|
||
StreamRecord input;
|
||
bool haveInput = false, ending;
|
||
{
|
||
std::lock_guard<std::mutex> guard(p.lock);
|
||
if (generation != p.videoGeneration) continue;
|
||
space = p.videos.size() < 2; ending = p.videoInputEnded && p.videoInput.empty();
|
||
if (!packet->size && space && !p.videoInput.empty()) {
|
||
input = std::move(p.videoInput.front()); p.videoInput.pop_front(); p.videoBusy = true; haveInput = true;
|
||
}
|
||
}
|
||
if (haveInput) {
|
||
worked = true;
|
||
if (revision != input.format.revision) {
|
||
avcodec_free_context(&decoder); revision = input.format.revision; eosSent = false; progressAt = nowUs();
|
||
std::lock_guard<std::mutex> guard(p.lock);
|
||
if (generation != p.videoGeneration) continue;
|
||
p.videos.clear(); p.videoBytes = 0; p.videoReady = false;
|
||
p.decoderLead = 0; p.framePeriod = 33333; p.lastInputPts = p.lastDecodedPts = -1;
|
||
}
|
||
format = input.format;
|
||
const uint8_t* data = reinterpret_cast<const uint8_t*>(input.data.constData());
|
||
const uint8_t* configuration = nullptr; size_t size = (size_t)input.data.size(), configurationSize = 0;
|
||
if ((input.flags & STREAM_MEDIA_KEY) &&
|
||
stream_media_key_parse(data, size, &configuration, &configurationSize, &data, &size) != STREAM_OK) {
|
||
fail(text("Stream video decoder failed")); break;
|
||
}
|
||
if (!decoder) {
|
||
if (!(input.flags & STREAM_MEDIA_KEY)) {
|
||
DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "desktop video waiting KEY id=%llu revision=%u pts=%llu",
|
||
(unsigned long long)p.id, revision, (unsigned long long)input.pts);
|
||
{ std::lock_guard<std::mutex> guard(p.lock); p.videoBusy = false; }
|
||
p.notify(); continue;
|
||
}
|
||
int64_t openingAt = nowUs();
|
||
AVCodecID codecId = format.video_codec == STREAM_MEDIA_H264 ? AV_CODEC_ID_H264
|
||
: format.video_codec == STREAM_MEDIA_H265 ? AV_CODEC_ID_HEVC
|
||
: format.video_codec == STREAM_MEDIA_VP9 ? AV_CODEC_ID_VP9
|
||
: format.video_codec == STREAM_MEDIA_AV1 ? AV_CODEC_ID_AV1 : AV_CODEC_ID_NONE;
|
||
if (format.video_codec == STREAM_MEDIA_H266) {
|
||
const AVCodecDescriptor* descriptor = avcodec_descriptor_get_by_name("vvc");
|
||
if (descriptor) codecId = descriptor->id;
|
||
}
|
||
const AVCodec* codec = avcodec_find_decoder(codecId);
|
||
decoder = codec ? avcodec_alloc_context3(codec) : nullptr;
|
||
if (decoder) {
|
||
decoder->thread_count = 4; decoder->thread_type = FF_THREAD_SLICE; decoder->flags |= AV_CODEC_FLAG_LOW_DELAY;
|
||
decoder->pkt_timebase = AVRational{1, 1000000};
|
||
if (configurationSize) {
|
||
decoder->extradata = static_cast<uint8_t*>(av_mallocz(configurationSize + AV_INPUT_BUFFER_PADDING_SIZE));
|
||
if (!decoder->extradata) { fail(text("Stream decoder allocation failed")); break; }
|
||
std::memcpy(decoder->extradata, configuration, configurationSize); decoder->extradata_size = (int)configurationSize;
|
||
}
|
||
}
|
||
int result = !codec ? AVERROR_DECODER_NOT_FOUND : !decoder ? AVERROR(ENOMEM) : avcodec_open2(decoder, codec, nullptr);
|
||
if (result < 0) {
|
||
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop decoder open id=%llu codec=%u result=%d",
|
||
(unsigned long long)p.id, format.video_codec, result);
|
||
fail(text("Stream video decoder failed")); break;
|
||
}
|
||
p.firstInputAt = nowUs();
|
||
DEBUG_INFO(DEBUG_CATEGORY_STREAM,
|
||
"desktop decoder id=%llu codec=%u name=%s threads=%d thread_type=%d config=%zu open_us=%lld key_pts=%llu",
|
||
(unsigned long long)p.id, format.video_codec, codec->name, decoder->thread_count, decoder->active_thread_type,
|
||
configurationSize, (long long)(nowUs() - openingAt), (unsigned long long)input.pts);
|
||
}
|
||
if (av_new_packet(packet, (int)size) < 0) { fail(text("Stream decoder allocation failed")); break; }
|
||
std::memcpy(packet->data, data, size); packet->pts = packet->dts = (int64_t)input.pts;
|
||
}
|
||
if (space && decoder && (packet->size || (ending && !eosSent))) {
|
||
int64_t at = nowUs(); int result = avcodec_send_packet(decoder, packet->size ? packet : nullptr); codecTime(at);
|
||
{ std::lock_guard<std::mutex> guard(p.lock); if (generation != p.videoGeneration) continue; }
|
||
if (result == 0) {
|
||
worked = true;
|
||
{
|
||
std::lock_guard<std::mutex> guard(p.lock);
|
||
if (generation != p.videoGeneration) continue;
|
||
if (packet->size) {
|
||
if (p.lastInputPts >= 0) p.framePeriod = std::clamp<int64_t>(packet->pts - p.lastInputPts, 1, 1000000);
|
||
p.lastInputPts = packet->pts;
|
||
} else eosSent = true;
|
||
p.videoBusy = false;
|
||
}
|
||
av_packet_unref(packet); p.notify();
|
||
} else if (result != AVERROR(EAGAIN)) {
|
||
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop decoder input id=%llu result=%d bytes=%d",
|
||
(unsigned long long)p.id, result, packet->size);
|
||
fail(text("Stream video decoder failed")); break;
|
||
}
|
||
}
|
||
if (ending && !decoder && !packet->size) {
|
||
{ std::lock_guard<std::mutex> guard(p.lock); if (generation != p.videoGeneration) continue; p.videoEnded = true; }
|
||
p.notify(); continue;
|
||
}
|
||
int64_t now = nowUs();
|
||
if (now - reportAt >= 1000000) {
|
||
DEBUG_TRACE(DEBUG_CATEGORY_VIDEO,
|
||
"desktop video worker id=%llu interval_us=%lld decoded=%llu discarded=%llu codec_us=%lld/%lld convert_us=%lld/%lld",
|
||
(unsigned long long)p.id, (long long)(now - reportAt), (unsigned long long)decoded,
|
||
(unsigned long long)discarded, (long long)codecTotal, (long long)codecMax, (long long)convertTotal, (long long)convertMax);
|
||
reportAt = now; codecTotal = codecMax = convertTotal = convertMax = 0;
|
||
}
|
||
if (space && decoder && (packet->size || ending) && now - progressAt > 10000000) {
|
||
fail(text("Stream video decoder failed")); break;
|
||
}
|
||
if (worked) continue;
|
||
std::unique_lock<std::mutex> guard(p.wakeLock);
|
||
p.wake.wait_for(guard, std::chrono::milliseconds(50), [&] { return !p.running || p.failed || p.wakeVersion != version; });
|
||
}
|
||
avcodec_free_context(&decoder); av_frame_free(&frame); av_packet_free(&packet);
|
||
if (converter) sws_freeContext(converter);
|
||
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop video worker released id=%llu", (unsigned long long)p.id);
|
||
#endif
|
||
}
|