Keep per-stream playback clocks on the shared audio output

This commit is contained in:
evgeny committed 2026-10-20 16:29:06 +02:00
1 parent a0b6dd7ef5
commit 103e0b6865
3 files changed
+52 -43

No files matched your search

+29 -35
View File
@@ -51,6 +51,8 @@ struct StreamPlayerWindow::Playback {
uint64_t id = ++nextId, group = 0, source = 0, epoch = 0;
std::atomic<bool> running{true}, audioAvailable{false};
std::atomic<float> volume{1.0f};
float outputVolume = 0.0f;
uint64_t audioGeneration = 0;
std::thread worker;
std::mutex lock;
std::deque<Video> videos;
@@ -100,17 +102,7 @@ StreamPlayerWindow::StreamPlayerWindow(uint64_t group, uint64_t source, uint64_t
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();
attachAudio();
connect(&m_timer, &QTimer::timeout, this, &StreamPlayerWindow::tick); m_timer.start(15);
m_playback->worker = std::thread(&StreamPlayerWindow::decode, this);
}
@@ -118,21 +110,20 @@ StreamPlayerWindow::StreamPlayerWindow(uint64_t group, uint64_t source, uint64_t
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);
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);
int64_t now = nowUs();
p.deviceLead = std::max<int64_t>(0, m_audio.callbackTimeUs() - 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 = m_audio.callbackTimeUs();
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.ended ? INT64_MAX : p.ready);
if (count < 0) { p.resync = true; count = 0; }
if ((unsigned)count < frames) {
@@ -146,25 +137,24 @@ void StreamPlayerWindow::openAudio() {
}
}
float volume = p.volume.load();
for (int i = 0; i < count * 2; i++) pcm[i] = static_cast<int16_t>(pcm[i] * volume);
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.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;
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;
}
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();
}
if (!SoundManager::instance()->outputAvailable()) p.audioAvailable = false;
QImage image; bool finished = false, first = false;
{
std::lock_guard<std::mutex> guard(p.lock);
@@ -183,13 +173,17 @@ void StreamPlayerWindow::tick() {
(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(); }
if (finished) {
m_status->setText(text("Stream ended"));
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_timer.stop(); gui_stream_stop(m_playback->id); m_audio.close(); m_playback->audioAvailable = false;
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();
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);