|
|
|
|
@ -40,6 +40,13 @@ QString text(const char* value) { return QCoreApplication::translate("ChatGui",
|
|
|
|
|
|
|
|
|
|
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}; |
|
|
|
|
@ -56,6 +63,7 @@ struct StreamPlayerWindow::Playback {
|
|
|
|
|
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 { |
|
|
|
|
@ -128,7 +136,13 @@ void StreamPlayerWindow::openAudio() {
|
|
|
|
|
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++; |
|
|
|
|
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); |
|
|
|
|
@ -200,6 +214,8 @@ void StreamPlayerWindow::decode() {
|
|
|
|
|
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; |
|
|
|
|
@ -210,6 +226,7 @@ void StreamPlayerWindow::decode() {
|
|
|
|
|
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…")); |
|
|
|
|
@ -226,7 +243,7 @@ void StreamPlayerWindow::decode() {
|
|
|
|
|
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); |
|
|
|
|
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); |
|
|
|
|
@ -283,7 +300,18 @@ void StreamPlayerWindow::decode() {
|
|
|
|
|
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; |
|
|
|
|
{ std::lock_guard<std::mutex> guard(p.lock); resync = p.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_DEBUG, |
|
|
|
|
"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(); |
|
|
|
|
@ -359,10 +387,15 @@ void StreamPlayerWindow::decode() {
|
|
|
|
|
observe(); |
|
|
|
|
if (record.pts > INT64_MAX) { fail(text("Invalid stream timestamp")); break; } |
|
|
|
|
if (record.kind == StreamRecordKind::Audio) { |
|
|
|
|
int result = 0; |
|
|
|
|
int result = 0, wanted = 0, stillWanted = 0; |
|
|
|
|
{ std::lock_guard<std::mutex> guard(p.lock); |
|
|
|
|
if (p.sound) wanted = stream_playback_audio_needs_input(p.sound); |
|
|
|
|
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()); } |
|
|
|
|
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 (p.sound) stillWanted = stream_playback_audio_needs_input(p.sound); } |
|
|
|
|
DEBUG_TRACE(DEBUG_CATEGORY_DEBUG, "desktop audio feed id=%llu pts=%llu result=%d needs_input=%d->%d", |
|
|
|
|
(unsigned long long)p.id, (unsigned long long)record.pts, result, wanted, stillWanted); |
|
|
|
|
if (result == -2) reset(true); |
|
|
|
|
else if (result < 0) { fail(text("Stream audio decoder failed")); break; } |
|
|
|
|
continue; |
|
|
|
|
|