468 lines
28 KiB
C++
468 lines
28 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 <QPushButton>
|
|
#include <QSlider>
|
|
#include <QCloseEvent>
|
|
#include <QKeyEvent>
|
|
#include <QTransform>
|
|
#include <atomic>
|
|
#include <thread>
|
|
#include <mutex>
|
|
#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); }
|
|
}
|
|
|
|
struct StreamPlayerWindow::Playback {
|
|
struct Video { QImage image; int64_t pts; };
|
|
struct AudioGap {
|
|
uint64_t count = 0, silentFrames = 0;
|
|
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;
|
|
std::atomic<bool> running{true}, audioAvailable{false};
|
|
std::atomic<float> volume{1.0f};
|
|
std::thread worker;
|
|
std::mutex lock;
|
|
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, underruns = 0, endFrames = UINT64_MAX;
|
|
bool started = false, presenting = false, videoReady = false, ended = false, videoEnded = false;
|
|
bool activeAudio = false, holdAudio = false, resync = false;
|
|
int64_t audioFedAt = 0, audioFedPts = -1, workerAt = 0;
|
|
~Playback() { stream_playback_audio_destroy(sound); }
|
|
|
|
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)
|
|
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));
|
|
}
|
|
};
|
|
|
|
StreamPlayerWindow::StreamPlayerWindow(uint64_t group, uint64_t source, uint64_t epoch, const QString& name, QWidget* parent)
|
|
: QWidget(parent), m_playback(std::make_unique<Playback>()) {
|
|
m_playback->group = group; m_playback->source = source; m_playback->epoch = epoch;
|
|
setWindowTitle(text("Live stream: %1").arg(name)); resize(800, 500);
|
|
auto* layout = new QVBoxLayout(this); layout->addWidget(new QLabel(name, this));
|
|
m_status = new QLabel(text("Connecting…"), this); layout->addWidget(m_status);
|
|
m_render = new VideoRenderWidget(this, true); layout->addWidget(m_render, 1);
|
|
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);
|
|
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);
|
|
connect(SoundManager::instance(), &SoundManager::contextAboutToReset, this, [this] {
|
|
m_audio.close(); m_playback->audioAvailable = false;
|
|
std::lock_guard<std::mutex> guard(m_playback->lock); m_playback->resync = true;
|
|
});
|
|
connect(SoundManager::instance(), &SoundManager::contextRestored, this, &StreamPlayerWindow::openAudio);
|
|
connect(SoundManager::instance(), &SoundManager::devicesChanged, this, [this] {
|
|
m_audio.close(); m_playback->audioAvailable = false;
|
|
{ std::lock_guard<std::mutex> guard(m_playback->lock); m_playback->resync = true; }
|
|
openAudio();
|
|
});
|
|
openAudio();
|
|
connect(&m_timer, &QTimer::timeout, this, &StreamPlayerWindow::tick); m_timer.start(15);
|
|
m_playback->worker = std::thread(&StreamPlayerWindow::decode, this);
|
|
}
|
|
|
|
StreamPlayerWindow::~StreamPlayerWindow() { stop(); }
|
|
|
|
/* Hardware callback заполняет только ближайшую порцию; source time тишины остаётся неподвижным. */
|
|
void StreamPlayerWindow::openAudio() {
|
|
if (!m_playback->running || m_audio.initialized()) return;
|
|
ma_context* context = SoundManager::instance()->context();
|
|
if (!context) { DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop stream audio unavailable: no context"); return; }
|
|
auto config = ma_device_config_init(ma_device_type_playback);
|
|
config.sampleRate = 48000; config.playback.channels = 2; config.playback.format = ma_format_s16;
|
|
ma_device_id device;
|
|
if (SoundManager::deviceId(SoundManager::instance()->playbackDevice(), &device)) config.playback.pDeviceID = &device;
|
|
bool initialized = m_audio.init(context, config, "stream", m_playback->id, [this](const void*, void* output, unsigned frames) {
|
|
auto* pcm = static_cast<int16_t*>(output); std::fill(pcm, pcm + frames * 2, 0);
|
|
auto& p = *m_playback; std::lock_guard<std::mutex> guard(p.lock);
|
|
int64_t now = nowUs();
|
|
p.deviceLead = std::max<int64_t>(0, m_audio.callbackTimeUs() - now);
|
|
if (!p.sound || !p.started || p.resync) return;
|
|
p.audioFrameStart = stream_playback_audio_written(p.sound); p.audioDacAt = m_audio.callbackTimeUs();
|
|
int count = !p.presenting || p.holdAudio ? 0 : stream_playback_audio_pull(p.sound, pcm, frames, p.ended ? INT64_MAX : p.ready);
|
|
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.ended) {
|
|
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.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;
|
|
}
|
|
}
|
|
float volume = p.volume.load();
|
|
for (int i = 0; i < count * 2; i++) pcm[i] = static_cast<int16_t>(pcm[i] * volume);
|
|
if (p.ended && p.endFrames == UINT64_MAX && stream_playback_audio_empty(p.sound))
|
|
p.endFrames = stream_playback_audio_written(p.sound);
|
|
});
|
|
if (!initialized || !m_audio.start()) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "desktop stream audio device failed id=%llu", (unsigned long long)m_playback->id);
|
|
m_audio.close(); m_playback->audioAvailable = false; return;
|
|
}
|
|
m_playback->audioAvailable = true;
|
|
}
|
|
|
|
/* GUI держит только ближайшие готовые изображения; накопленный поток остаётся в RS-кольце. */
|
|
void StreamPlayerWindow::tick() {
|
|
auto& p = *m_playback;
|
|
if (m_audio.needsRecovery()) {
|
|
m_audio.close(); p.audioAvailable = false;
|
|
{ std::lock_guard<std::mutex> guard(p.lock); p.resync = true; }
|
|
openAudio();
|
|
}
|
|
QImage image; bool finished = false, first = false;
|
|
{
|
|
std::lock_guard<std::mutex> guard(p.lock);
|
|
int64_t clock = p.clock(nowUs());
|
|
while (p.started && !p.videos.empty() && p.videos.front().pts <= clock) {
|
|
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;
|
|
bool audioDone = !p.activeAudio || !p.audioAvailable || (p.endFrames != UINT64_MAX && p.played(nowUs()) >= p.endFrames);
|
|
finished = p.ended && p.videoEnded && p.videos.empty() && audioDone;
|
|
}
|
|
if (!image.isNull()) {
|
|
m_render->setFrame(image); m_status->setText(text("Live"));
|
|
if (first) DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop first frame id=%llu %dx%d RGB_stride=%lld",
|
|
(unsigned long long)p.id, image.width(), image.height(), (long long)image.bytesPerLine());
|
|
emit framePresented();
|
|
}
|
|
if (finished) { m_status->setText(text("Stream ended")); m_audio.close(); m_timer.stop(); }
|
|
}
|
|
|
|
/* Callback устройства прекращается до освобождения DSP; worker снимает только свой reader. */
|
|
void StreamPlayerWindow::stop() {
|
|
if (!m_playback->running.exchange(false) && !m_playback->worker.joinable()) return;
|
|
m_timer.stop(); gui_stream_stop(m_playback->id); m_audio.close(); m_playback->audioAvailable = false;
|
|
if (m_playback->worker.joinable()) m_playback->worker.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);
|
|
}
|
|
|
|
/* Worker заполняет codec по готовности входа; GUI показывает только ближайшие декодированные кадры. */
|
|
void StreamPlayerWindow::decode() {
|
|
auto& p = *m_playback;
|
|
#ifndef HAVE_FFMPEG
|
|
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "desktop live decoder unavailable: FFmpeg disabled");
|
|
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{};
|
|
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;
|
|
auto reset = [&](bool request) {
|
|
avcodec_free_context(&decoder); av_packet_unref(packet); waitKey = true; finishing = eosSent = false;
|
|
decoderProgress = lastInputAt = -1;
|
|
{
|
|
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 = false;
|
|
p.audioDacAt = 0; p.audioFrameStart = 0; p.holdAudio = p.resync = false; p.endFrames = UINT64_MAX;
|
|
p.audioFedAt = 0; p.audioFedPts = -1;
|
|
}
|
|
if (request && !gui_stream_resync(p.id)) DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop playback resync refused");
|
|
deadline = nowUs() + 10000000; emit statusChanged(text("Waiting for a key frame…"));
|
|
};
|
|
auto fail = [&](const QString& reason) {
|
|
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;
|
|
stream_playback_clock_reset(&p.adaptation, p.id);
|
|
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop playback starts id=%llu pts=%lld fixed_prefill=0",
|
|
(unsigned long long)p.id, (long long)p.position);
|
|
}
|
|
p.ready = p.activeAudio ? status.ready_us : status.video_us;
|
|
};
|
|
/* Забираем выход прежде следующего входа. При FIN тот же проход дренирует codec до EOF. */
|
|
auto drainVideo = [&] {
|
|
if (!decoder) return;
|
|
for (unsigned work = 0; work < 8 && p.running; work++) {
|
|
{ std::lock_guard<std::mutex> guard(p.lock); if (p.videos.size() >= 2) return; }
|
|
int result = avcodec_receive_frame(decoder, frame);
|
|
if (result == AVERROR(EAGAIN)) return;
|
|
if (result == AVERROR_EOF) {
|
|
std::lock_guard<std::mutex> guard(p.lock); p.videoEnded = true;
|
|
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",
|
|
(unsigned long long)p.id, (long long)pts, (long long)p.decoderLead); }
|
|
if (pts < p.clock(nowUs()) - 100000) { discarded++; continue; } }
|
|
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));
|
|
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)) fail(text("Could not connect to the stream"));
|
|
else while (p.running) {
|
|
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) {
|
|
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",
|
|
(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();
|
|
}
|
|
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))) {
|
|
int sent = avcodec_send_packet(decoder, packet->size ? packet : nullptr);
|
|
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 (finishing && 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;
|
|
{
|
|
std::lock_guard<std::mutex> guard(p.lock);
|
|
int64_t now = nowUs(); position = p.started ? p.clock(now) : -1;
|
|
wantVideo = !packet->size && p.videos.size() < 2;
|
|
audioAhead = !p.activeAudio || !p.sound || stream_playback_audio_needs_input(p.sound) ? p.deviceLead + 80000 : -1;
|
|
if (p.presenting && p.ready >= 0) {
|
|
uint64_t played = p.played(now);
|
|
p.holdAudio = !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);
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
StreamRecord record;
|
|
if (finishing || !gui_stream_pull(p.id, record, position, wantVideo, audioAhead)) {
|
|
if (!gui_stream_ready()) { fail(text("Chat service is stopped")); break; }
|
|
if ((!opened || waitKey) && nowUs() > deadline) { fail(text("Stream connection timed out")); break; }
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(5)); continue;
|
|
}
|
|
if (record.kind == StreamRecordKind::Opened) { opened = true; 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)); break;
|
|
}
|
|
if (record.kind == StreamRecordKind::End) {
|
|
std::lock_guard<std::mutex> guard(p.lock); p.ended = true; p.holdAudio = false;
|
|
if (p.sound) stream_playback_audio_finish(p.sound);
|
|
finishing = true; p.videoEnded = !decoder; continue;
|
|
}
|
|
observe();
|
|
if (record.pts > INT64_MAX) { fail(text("Invalid stream timestamp")); break; }
|
|
if (record.kind == StreamRecordKind::Audio) {
|
|
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; } }
|
|
if (result == -2) reset(true);
|
|
else if (result < 0) { fail(text("Stream audio decoder failed")); break; }
|
|
continue;
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
if (!decoder) {
|
|
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;
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
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);
|
|
fail(text("Stream video decoder failed")); break;
|
|
}
|
|
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop decoder codec=%u name=%s config=%zu", record.format.video_codec, codec->name, configurationSize);
|
|
decoder->pkt_timebase = AVRational{1, 1000000}; waitKey = false;
|
|
}
|
|
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 (nowUs() - report >= 1000000) {
|
|
int64_t now = nowUs(); double seconds = (now - report) / 1000000.0; report = now;
|
|
std::lock_guard<std::mutex> guard(p.lock);
|
|
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",
|
|
(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);
|
|
reportedShown = p.shown;
|
|
}
|
|
}
|
|
avcodec_free_context(&decoder); av_frame_free(&frame); av_packet_free(&packet);
|
|
if (converter) sws_freeContext(converter);
|
|
#endif
|
|
gui_stream_stop(p.id);
|
|
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop decoder released id=%llu", (unsigned long long)p.id);
|
|
}
|