Isolate live video decoding from audio preparation in ChatGUI

This commit is contained in:
evgeny committed 2026-10-21 01:00:31 +02:00
1 parent 6c619d5f2a
commit 1764603977
2 files changed
+314 -291

No files matched your search

+311 -289
View File
@@ -47,7 +47,6 @@ struct StreamPlayerWindow::Playback {
unsigned requested = 0, received = 0;
int64_t reserve = 0, feedAge = -1, workerAge = -1, audioPts = -1;
size_t videoQueue = 0;
bool held = false;
} audioGap;
uint64_t id = ++nextId, group = 0, source = 0, epoch = 0;
int64_t createdAt = nowUs(), firstInputAt = 0;
@@ -56,7 +55,10 @@ struct StreamPlayerWindow::Playback {
std::atomic<float> volume{1.0f};
float outputVolume = 0.0f;
uint64_t audioGeneration = 0;
std::thread worker;
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;
@@ -71,14 +73,14 @@ struct StreamPlayerWindow::Playback {
int64_t decoderLead = 0, framePeriod = 33333, lastInputPts = -1, lastDecodedPts = -1;
uint64_t audioFrameStart = 0, shown = 0, underruns = 0, endFrames = UINT64_MAX;
bool started = false, presenting = false, videoReady = false, ended = false, videoEnded = false;
bool activeAudio = false, audioEnded = false, holdAudio = false, resync = false;
bool activeAudio = false, audioEnded = false, resync = false;
int64_t audioFedAt = 0, audioFedPts = -1, workerAt = 0, guiAt = 0, guiGapMax = 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_one();
wake.notify_all();
}
uint64_t played(int64_t now) const {
@@ -92,7 +94,6 @@ struct StreamPlayerWindow::Playback {
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)));
if (holdAudio) return position;
int64_t proposed = position + (now - clockAt) * adaptation.tempo_milli / 1000;
return std::max(position, std::min(ready, proposed));
}
@@ -124,6 +125,7 @@ StreamPlayerWindow::StreamPlayerWindow(uint64_t group, uint64_t source, uint64_t
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);
}
@@ -152,13 +154,13 @@ void StreamPlayerWindow::attachAudio() {
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;
int count = !p.presenting || p.holdAudio ? 0 : stream_playback_audio_pull(p.sound, pcm, frames, p.audioEnded ? INT64_MAX : p.ready);
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.underruns++; p.audioGap.count++; p.audioGap.silentFrames += frames - (unsigned)count;
p.audioGap.requested = frames; p.audioGap.received = (unsigned)count; p.audioGap.held = p.holdAudio;
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;
@@ -228,6 +230,7 @@ void StreamPlayerWindow::stop() {
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);
}
@@ -238,239 +241,122 @@ void StreamPlayerWindow::keyPressEvent(QKeyEvent* event) {
else QWidget::keyPressEvent(event);
}
/* Worker заполняет codec по готовности входа; GUI показывает только ближайшие декодированные кадры. */
/* Один владелец 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"));
p.failed = p.complete = true; emit statusChanged(text("Live video playback requires FFmpeg"));
#else
AVCodecContext* decoder = nullptr; AVFrame* frame = av_frame_alloc(); AVPacket* packet = av_packet_alloc();
SwsContext* converter = nullptr;
bool waitKey = true, opened = false, finishing = false, eosSent = false;
stream_media_format videoFormat{};
std::deque<StreamRecord> records; // Только worker; порядок внутри каждого трека, GAP/CLOSED отменяют старые данные.
uint32_t revision = 0;
uint64_t decoded = 0, discarded = 0, reportedShown = 0;
int64_t deadline = nowUs() + 10000000, report = nowUs(), decoderProgress = -1, lastInputAt = -1;
uint64_t reportedGaps = 0;
int64_t gapReportAt = 0;
struct {
uint64_t loops = 0, video = 0, audio = 0, empty = 0, batches = 0, batchMax = 0;
int64_t codec = 0, codecMax = 0, convert = 0, convertMax = 0;
int64_t audioFeed = 0, audioFeedMax = 0;
int64_t pull = 0, pullMax = 0, wait = 0, waitMax = 0, loopMax = 0;
} work;
int64_t lastLoopAt = report;
std::deque<StreamRecord> records;
bool opened = false;
uint64_t reportedGaps = 0, observedGaps = 0;
int64_t deadline = nowUs() + 10000000, reportAt = nowUs(), gapReportAt = 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 = true; p.complete = true;
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();
avcodec_free_context(&decoder); av_packet_unref(packet); waitKey = true; finishing = eosSent = false;
decoderProgress = lastInputAt = -1;
stream_playback_audio* previous;
{
std::lock_guard<std::mutex> guard(p.lock);
stream_playback_audio_destroy(p.sound); p.sound = nullptr;
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 = -1;
p.ended = p.videoEnded = p.audioEnded = false;
p.audioDacAt = 0; p.audioFrameStart = 0; p.holdAudio = p.resync = false; p.endFrames = UINT64_MAX;
p.audioFedAt = 0; p.audioFedPts = -1;
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;
}
if (request && !gui_stream_resync(p.id)) DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop playback resync refused");
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);
deadline = nowUs() + 10000000; emit statusChanged(text("Waiting for a key frame…"));
};
auto fail = [&](const QString& reason, bool retry = true) {
p.failed = retry; p.complete = true;
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "desktop viewer failed id=%llu: %s", (unsigned long long)p.id, qPrintable(reason));
emit statusChanged(reason);
};
auto observe = [&] {
stream_media_status status{};
if (!gui_stream_status(p.id, status)) return;
std::lock_guard<std::mutex> guard(p.lock);
p.media = status;
if (!status.initialized) return;
if (!p.started) {
p.activeAudio = status.audio_enabled && p.audioAvailable;
if (p.activeAudio) p.sound = stream_playback_audio_create(status.start_us, 40);
if (p.activeAudio && !p.sound) { p.running = false; return; }
p.position = status.start_us; p.clockAt = nowUs(); p.started = true;
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 fixed_prefill=0 elapsed_us=%lld "
"ready_us=%lld video_us=%lld audio_us=%lld initial_reserve_us=%lld audio=%d",
(unsigned long long)p.id, (long long)p.position, (long long)(nowUs() - p.createdAt),
(long long)status.ready_us, (long long)status.video_us, (long long)status.audio_us,
(long long)((p.activeAudio ? status.ready_us : status.video_us) - p.position), p.activeAudio);
"desktop playback starts id=%llu pts=%lld fixed_prefill=0 elapsed_us=%lld audio=%d PCM_target_ms=250 separate_video=1",
(unsigned long long)p.id, (long long)p.position, (long long)(nowUs() - p.createdAt), audio);
}
p.ready = p.activeAudio ? status.ready_us : status.video_us;
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);
};
/* Забираем выход прежде следующего входа. При FIN тот же проход дренирует codec до EOF. */
auto drainVideo = [&] {
if (!decoder) return;
for (unsigned attempt = 0; attempt < 8 && p.running; attempt++) {
{ std::lock_guard<std::mutex> guard(p.lock); if (p.videoEnded || p.videos.size() >= 2) return; }
int64_t codecAt = nowUs();
int result = avcodec_receive_frame(decoder, frame);
int64_t codecUs = nowUs() - codecAt;
work.codec += codecUs; work.codecMax = std::max(work.codecMax, codecUs);
if (result == AVERROR(EAGAIN)) return;
if (result == AVERROR_EOF) {
std::lock_guard<std::mutex> guard(p.lock); p.videoEnded = true; p.holdAudio = false;
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop decoder drained id=%llu decoded=%llu", (unsigned long long)p.id,
(unsigned long long)decoded); return;
}
if (result < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop decoder output failed id=%llu codec=%u result=%d",
(unsigned long long)p.id, videoFormat.video_codec, result);
fail(text("Stream video decoder failed")); p.running = false; return;
}
decoded++; decoderProgress = nowUs();
if (frame->width <= 0 || frame->width > 4096 || frame->height <= 0 || frame->height > 4096) {
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop decoder invalid size=%dx%d", frame->width, frame->height);
fail(text("Invalid stream video dimensions")); p.running = false; return;
}
int64_t pts;
{ std::lock_guard<std::mutex> guard(p.lock);
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 decoder_lead=%lld elapsed_us=%lld decode_us=%lld ready_ahead_us=%lld",
(unsigned long long)p.id, (long long)pts, (long long)p.decoderLead, (long long)(nowUs() - p.createdAt),
(long long)(p.firstInputAt ? nowUs() - p.firstInputAt : -1), (long long)(p.ready - pts)); }
if (pts < p.clock(nowUs()) - 100000) { 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")); p.running = false; return; }
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")); p.running = false; return;
}
if (videoFormat.rotation) image = image.transformed(QTransform().rotate(videoFormat.rotation));
int64_t convertUs = nowUs() - convertAt;
work.convert += convertUs; work.convertMax = std::max(work.convertMax, convertUs);
std::lock_guard<std::mutex> guard(p.lock);
p.videoBytes += (size_t)image.sizeInBytes(); p.videos.push_back({std::move(image), pts});
}
};
if (!frame || !packet) fail(text("Stream decoder allocation failed"));
else if (!gui_stream_open(p.id, p.group, p.source, p.epoch,
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) {
uint64_t wakeVersion;
{ std::lock_guard<std::mutex> guard(p.wakeLock); wakeVersion = p.wakeVersion; }
int64_t loopAt = nowUs();
work.loops++; work.loopMax = std::max(work.loopMax, loopAt - lastLoopAt); lastLoopAt = loopAt;
if (loopAt - report >= 1000000) {
double seconds = (loopAt - report) / 1000000.0;
std::lock_guard<std::mutex> guard(p.lock);
uint64_t dataSignals;
{ std::lock_guard<std::mutex> wakeGuard(p.wakeLock); dataSignals = p.dataSignals; p.dataSignals = 0; }
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG,
"desktop viewer work id=%llu interval_us=%lld loops=%llu video=%llu audio=%llu empty=%llu "
"codec_us=%lld/%lld convert_us=%lld/%lld audio_feed_us=%lld/%lld pull_us=%lld/%lld wait_us=%lld/%lld "
"loop_max_us=%lld gui_gap_max_us=%lld gui_age_us=%lld decoded=%llu shown=%llu show_fps=%.1f "
"clock_us=%lld ready_us=%lld videoQ=%zu hold=%d underruns=%llu batches=%llu batch_max=%llu "
"pending_records=%zu data_signals=%llu presenting=%d audio_input=%d input_FIN=%d/%d/%d "
"drain=%d/%d packet_bytes=%d timing=total/max",
(unsigned long long)p.id, (long long)(loopAt - report), (unsigned long long)work.loops,
(unsigned long long)work.video, (unsigned long long)work.audio, (unsigned long long)work.empty,
(long long)work.codec, (long long)work.codecMax, (long long)work.convert, (long long)work.convertMax,
(long long)work.audioFeed, (long long)work.audioFeedMax,
(long long)work.pull, (long long)work.pullMax, (long long)work.wait, (long long)work.waitMax,
(long long)work.loopMax, (long long)p.guiGapMax, (long long)(p.guiAt ? nowUs() - p.guiAt : -1),
(unsigned long long)decoded, (unsigned long long)p.shown, (p.shown - reportedShown) / seconds,
(long long)p.position, (long long)p.ready, p.videos.size(), p.holdAudio, (unsigned long long)p.underruns,
(unsigned long long)work.batches, (unsigned long long)work.batchMax, records.size(), (unsigned long long)dataSignals,
p.presenting, p.sound ? stream_playback_audio_needs_input(p.sound) : -1,
p.media.finished, p.media.video_ended, p.media.audio_ended, finishing, eosSent, packet->size);
DEBUG_DEBUG(DEBUG_CATEGORY_STREAM,
"desktop viewer id=%llu decoded=%llu discarded=%llu shown=%llu showFps=%.1f videoQ=%zu "
"clock=%lld reserve_us=%lld minimum_us=%lld tempo=%d lead_us=%lld underruns=%llu "
"ready_us=%lld shown_pts=%lld display_lag_us=%lld decoder_lead_us=%lld hold=%d",
(unsigned long long)p.id, (unsigned long long)decoded, (unsigned long long)discarded,
(unsigned long long)p.shown, (p.shown - reportedShown) / seconds, p.videos.size(), (long long)p.position,
(long long)p.timing.reserve_us, (long long)p.timing.minimum_us, p.timing.tempo_milli,
(long long)p.deviceLead, (unsigned long long)p.underruns, (long long)p.ready,
(long long)p.lastShownPts, (long long)(p.lastShownPts >= 0 ? p.position - p.lastShownPts : -1),
(long long)p.decoderLead, p.holdAudio);
work = {}; report = loopAt; reportedShown = p.shown; p.guiGapMax = 0;
}
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 = nowUs(); gap = p.audioGap; }
/* Callback сохраняет снимок; печать выполняется worker без блокировки аудиоустройства. */
if (gap.count != reportedGaps && nowUs() - gapReportAt >= 1000000) {
{ std::lock_guard<std::mutex> guard(p.lock); resync = p.resync; p.workerAt = began; gap = p.audioGap; }
if (gap.count != reportedGaps && began - gapReportAt >= 1000000) {
DEBUG_WARN(DEBUG_CATEGORY_STREAM,
"desktop audio gap id=%llu count=%llu delta=%llu silence_frames=%llu cause=%s got=%u/%u "
"reserve_us=%lld feed_age_us=%lld worker_age_us=%lld audio_pts=%lld videoQ=%zu",
"desktop audio gap id=%llu count=%llu delta=%llu silence_frames=%llu got=%u/%u reserve_us=%lld "
"feed_age_us=%lld worker_age_us=%lld audio_pts=%lld videoQ=%zu",
(unsigned long long)p.id, (unsigned long long)gap.count, (unsigned long long)(gap.count - reportedGaps),
(unsigned long long)gap.silentFrames, gap.held ? "video_hold" : "pcm_short", gap.received, gap.requested,
(long long)gap.reserve, (long long)gap.feedAge, (long long)gap.workerAge, (long long)gap.audioPts, gap.videoQueue);
reportedGaps = gap.count; gapReportAt = nowUs();
(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);
reportedGaps = gap.count; gapReportAt = began;
}
if (resync) reset(true);
observe();
drainVideo();
if (!p.running) break;
bool space;
{ std::lock_guard<std::mutex> guard(p.lock); space = p.videos.size() < 2; }
if (space && decoder && (packet->size || (finishing && !eosSent))) {
int64_t codecAt = nowUs();
int sent = avcodec_send_packet(decoder, packet->size ? packet : nullptr);
int64_t codecUs = nowUs() - codecAt;
work.codec += codecUs; work.codecMax = std::max(work.codecMax, codecUs);
if (sent == 0) {
if (packet->size) {
std::lock_guard<std::mutex> guard(p.lock);
if (p.lastInputPts >= 0) p.framePeriod = std::clamp<int64_t>(packet->pts - p.lastInputPts, 1, 1000000);
p.lastInputPts = packet->pts; lastInputAt = nowUs();
} else eosSent = true;
av_packet_unref(packet); drainVideo();
} else if (sent != AVERROR(EAGAIN)) {
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop decoder input failed id=%llu codec=%u bytes=%d result=%d",
(unsigned long long)p.id, videoFormat.video_codec, packet->size, sent);
fail(text("Stream video decoder failed")); break;
}
}
{ std::lock_guard<std::mutex> guard(p.lock);
if (p.ended && p.videoEnded) break;
if (p.videos.empty() && decoderProgress >= 0 && nowUs() - decoderProgress > 10000000
&& (packet->size || finishing || (lastInputAt >= 0 && nowUs() - lastInputAt < 1000000))) {
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop decoder stalled id=%llu input_PTS=%lld output_PTS=%lld pending=%d FIN=%d",
(unsigned long long)p.id, (long long)p.lastInputPts, (long long)p.lastDecodedPts, packet->size, finishing);
fail(text("Stream video decoder failed")); break;
} }
int64_t position, audioAhead; bool wantVideo; unsigned videoBudget;
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;
wantVideo = !finishing && !packet->size && p.videos.size() < 2;
videoBudget = finishing ? 0 : static_cast<unsigned>(std::max(0, 2 - (int)p.videos.size() - (packet->size ? 1 : 0)));
audioAhead = !p.audioEnded && (!p.activeAudio || !p.sound || stream_playback_audio_needs_input(p.sound))
? p.deviceLead + 80000 : -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);
p.holdAudio = !p.videoEnded && (!p.videoReady || (p.videos.empty() && p.lastDecodedPts < position - p.framePeriod)
|| (!p.videos.empty() && p.videos.front().pts < position - 40000));
int virtualEmpty = !p.activeAudio && p.position + (now - p.clockAt) * p.adaptation.tempo_milli / 1000 > p.ready;
if (!p.activeAudio && p.holdAudio) position = p.position;
int starved = p.holdAudio || virtualEmpty || (p.sound && stream_playback_audio_starved(p.sound, played));
int64_t guardUs = std::max(p.decoderLead + p.framePeriod, p.activeAudio ? p.deviceLead + 10000 : p.framePeriod);
bool starved = p.underruns != observedGaps; observedGaps = p.underruns;
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;
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;
if (p.sound) stream_playback_audio_set_tempo(p.sound, p.timing.tempo_milli);
}
else { p.position = std::max(p.position, position); p.clockAt = now; tempo = p.timing.tempo_milli; }
}
if (began - reportAt >= 1000000) {
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG,
"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 (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;
});
@@ -485,27 +371,19 @@ void StreamPlayerWindow::decode() {
continue;
}
if (!video && !audio) { barrier = true; if (it == records.begin()) next = it; break; }
if (next == records.end() && ((video && wantVideo) || (audio && audioAhead >= 0))) next = it;
if (next == records.end() && ((video && videoBudget) || (audio && audioAhead >= 0))) next = it;
}
if (next == records.end()) {
size_t previous = records.size();
int64_t pullAt = nowUs();
// После обоих EOF нужен ещё общий END, даже когда больше media не требуется.
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);
int64_t pullUs = nowUs() - pullAt;
work.pull += pullUs; work.pullMax = std::max(work.pullMax, pullUs);
if (received) {
work.batches++; work.batchMax = std::max<uint64_t>(work.batchMax, records.size() - previous); continue;
}
work.empty++;
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; }
if ((!opened || waitKey) && nowUs() > deadline) { fail(text("Stream connection timed out")); break; }
int64_t waitAt = nowUs();
{ std::unique_lock<std::mutex> guard(p.wakeLock);
p.wake.wait_for(guard, std::chrono::milliseconds(50), [&] { return !p.running || p.wakeVersion != wakeVersion; }); }
int64_t waitUs = nowUs() - waitAt;
work.wait += waitUs; work.waitMax = std::max(work.waitMax, waitUs); continue;
if ((!opened || !p.started) && nowUs() > deadline) { fail(text("Stream connection timed out")); break; }
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) { opened = true; continue; }
@@ -518,93 +396,237 @@ void StreamPlayerWindow::decode() {
fail(text(reason), record.reason == STREAM_CLOSE_FAILED); break;
}
if (record.kind == StreamRecordKind::VideoEnd || record.kind == StreamRecordKind::AudioEnd || record.kind == StreamRecordKind::End) {
std::lock_guard<std::mutex> guard(p.lock);
if (record.kind != StreamRecordKind::AudioEnd && !finishing) { finishing = true; p.videoEnded = !decoder; }
if (record.kind != StreamRecordKind::VideoEnd && !p.audioEnded) {
p.audioEnded = true;
if (p.sound) stream_playback_audio_finish(p.sound);
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());
}
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", finishing, p.audioEnded, records.size());
continue;
p.notify(); continue;
}
observe();
if (record.pts > INT64_MAX) { fail(text("Invalid stream timestamp")); break; }
if (record.kind == StreamRecordKind::Audio) {
work.audio++;
int64_t audioAt = nowUs();
int result = 0;
{ std::lock_guard<std::mutex> guard(p.lock);
if (p.sound) result = stream_playback_audio_feed(p.sound, (int64_t)record.pts,
reinterpret_cast<const uint8_t*>(record.data.constData()), (size_t)record.data.size());
if (p.sound && result == 0) { p.audioFedAt = nowUs(); p.audioFedPts = (int64_t)record.pts; } }
int64_t audioUs = nowUs() - audioAt;
work.audioFeed += audioUs; work.audioFeedMax = std::max(work.audioFeedMax, audioUs);
++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; }
continue;
}
work.video++;
if (revision != record.format.revision) {
avcodec_free_context(&decoder); waitKey = true; revision = record.format.revision;
decoderProgress = lastInputAt = -1;
{ std::lock_guard<std::mutex> guard(p.lock); p.videos.clear(); p.videoBytes = 0; p.videoReady = false;
p.decoderLead = 0; p.framePeriod = 33333; p.lastInputPts = p.lastDecodedPts = -1; }
deadline = nowUs() + 10000000;
}
videoFormat = record.format;
if (waitKey && !(record.flags & STREAM_MEDIA_KEY)) continue;
const uint8_t* data = reinterpret_cast<const uint8_t*>(record.data.constData());
const uint8_t* configuration = nullptr; size_t size = (size_t)record.data.size(), configurationSize = 0;
if (record.flags & STREAM_MEDIA_KEY) {
if (stream_media_key_parse(data, size, &configuration, &configurationSize, &data, &size) != STREAM_OK) {
fail(text("Stream video 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();
}
if (!decoder) {
int64_t openingAt = nowUs();
AVCodecID codecId = record.format.video_codec == STREAM_MEDIA_H264 ? AV_CODEC_ID_H264
: record.format.video_codec == STREAM_MEDIA_H265 ? AV_CODEC_ID_HEVC
: record.format.video_codec == STREAM_MEDIA_VP9 ? AV_CODEC_ID_VP9
: record.format.video_codec == STREAM_MEDIA_AV1 ? AV_CODEC_ID_AV1 : AV_CODEC_ID_NONE;
if (record.format.video_codec == STREAM_MEDIA_H266) {
const AVCodecDescriptor* descriptor = avcodec_descriptor_get_by_name("vvc");
if (descriptor) codecId = descriptor->id;
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;
{
std::lock_guard<std::mutex> guard(p.lock);
current = p.videoGeneration; space = p.videos.size() < 2;
}
if (current != generation) {
avcodec_free_context(&decoder); av_packet_unref(packet); revision = 0; eosSent = false;
generation = current; progressAt = nowUs();
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "desktop video generation id=%llu generation=%llu",
(unsigned long long)p.id, (unsigned long long)generation);
}
bool worked = false;
if (decoder && space) {
int64_t at = nowUs(); int result = avcodec_receive_frame(decoder, frame); codecTime(at);
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(); break;
}
const AVCodec* codec = avcodec_find_decoder(codecId);
decoder = codec ? avcodec_alloc_context3(codec) : nullptr;
if (decoder) {
decoder->thread_count = 1; decoder->flags |= AV_CODEC_FLAG_LOW_DELAY;
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;
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; 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;
}
int openResult = !codec ? AVERROR_DECODER_NOT_FOUND : !decoder ? AVERROR(ENOMEM) : avcodec_open2(decoder, codec, nullptr);
if (openResult < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "desktop decoder open failed codec=%u name=%s config=%zu result=%d",
record.format.video_codec, codec ? codec->name : "unavailable", configurationSize, openResult);
}
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;
}
DEBUG_INFO(DEBUG_CATEGORY_STREAM,
"desktop decoder id=%llu codec=%u name=%s config=%zu elapsed_us=%lld open_us=%lld key_pts=%llu",
(unsigned long long)p.id, record.format.video_codec, codec->name, configurationSize,
(long long)(nowUs() - p.createdAt), (long long)(nowUs() - openingAt), (unsigned long long)record.pts);
decoder->pkt_timebase = AVRational{1, 1000000}; waitKey = false;
p.firstInputAt = nowUs();
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;
}
av_packet_unref(packet);
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)record.pts;
if (lastInputAt < 0 || nowUs() - lastInputAt > 1000000) decoderProgress = nowUs();
if (space && decoder && (packet->size || (ending && !eosSent))) {
int64_t at = nowUs(); int result = avcodec_send_packet(decoder, packet->size ? packet : nullptr); codecTime(at);
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(); break;
}
int64_t now = nowUs();
if (now - reportAt >= 1000000) {
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG,
"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
gui_stream_stop(p.id);
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop decoder released id=%llu", (unsigned long long)p.id);
}
+3 -2
View File
@@ -1,6 +1,6 @@
/* Просмотр одной live-трансляции: запас остаётся в RS-кольце, worker готовит ближайшие FFmpeg/Opus кадры.
/* Просмотр live: audio worker получает батчи и готовит PCM, отдельный worker владеет FFmpeg.
* GUI владеет окном; общий output смешивает PCM, callback сохраняет отдельные часы этой трансляции.
* Закрытие останавливает worker и снимает reader. Публикация и общий транспорт не закрываются. */
* Закрытие отсоединяет output, останавливает оба worker и снимает reader. */
#pragma once
#include <QWidget>
@@ -39,4 +39,5 @@ private:
void attachAudio();
void tick();
void decode();
void decodeVideo();
};