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);
+3 -4
View File
@@ -1,5 +1,5 @@
/* Просмотр одной live-трансляции: запас остаётся в RS-кольце, worker готовит ближайшие FFmpeg/Opus кадры.
* GUI владеет output device и окном; audio callback публикует часы физического воспроизведения.
* GUI владеет окном; общий output смешивает PCM, callback сохраняет отдельные часы этой трансляции.
* Закрытие останавливает worker и снимает reader. Публикация и общий транспорт не закрываются. */
#pragma once
@@ -7,7 +7,6 @@
#include <QTimer>
#include <memory>
#include <cstdint>
#include "audio_device.h"
class VideoRenderWidget;
class QLabel;
@@ -29,11 +28,11 @@ protected:
private:
struct Playback;
std::unique_ptr<Playback> m_playback;
AudioDevice m_audio;
bool m_audioAttached = false;
QTimer m_timer;
VideoRenderWidget* m_render = nullptr;
QLabel* m_status = nullptr;
void openAudio();
void attachAudio();
void tick();
void decode();
};
+20 -4
View File
@@ -127,11 +127,11 @@ extern "C" ma_result __wrap_ma_context_init(const ma_backend*, ma_uint32, const
}
static void observeAudio(ma_device* device, void* out, const void* in, ma_uint32 frames) {
originalAudioCallback.load()(device, out, in, frames);
const auto* pcm = static_cast<const int16_t*>(out);
const auto* pcm = static_cast<const float*>(out);
uint64_t left = 0, right = 0, nonzero = 0;
for (unsigned i = 0; i < frames; i++) {
left += static_cast<int64_t>(pcm[2 * i]) * pcm[2 * i];
right += static_cast<int64_t>(pcm[2 * i + 1]) * pcm[2 * i + 1];
int64_t l = std::lround(pcm[2 * i] * 32768), r = std::lround(pcm[2 * i + 1] * 32768);
left += l * l; right += r * r;
nonzero += pcm[2 * i] != 0; nonzero += pcm[2 * i + 1] != 0;
if (pcm[2 * i] || pcm[2 * i + 1]) silentRun = 0;
else if (++silentRun == 48) silenceGaps.fetch_add(1);
@@ -140,7 +140,7 @@ static void observeAudio(ma_device* device, void* out, const void* in, ma_uint32
}
extern "C" ma_result __wrap_ma_device_init(ma_context* context, const ma_device_config* config, ma_device* device) {
ma_device_config copy = *config;
if (copy.deviceType == ma_device_type_playback && copy.playback.format == ma_format_s16 && copy.playback.channels == 2) {
if (copy.deviceType == ma_device_type_playback && copy.playback.format == ma_format_f32 && copy.playback.channels == 2) {
originalAudioCallback = copy.dataCallback; copy.dataCallback = observeAudio;
}
return __real_ma_device_init(context, &copy, device);
@@ -370,6 +370,22 @@ private slots:
QCOMPARE(stream_get_info(publisher, &info), STREAM_OK); QCOMPARE(info.owners, 1u);
}
void independentCatalogueOwners() {
int before = snapshots.size();
QVERIFY(gui_stream_watch(group, 77));
QTRY_VERIFY(snapshots.size() > before);
QVERIFY(gui_stream_watch(0)); QTest::qWait(20); // Основной чат перестал наблюдать, окно осталось.
before = snapshots.size();
QCOMPARE(stream_publish_finish(publisher), STREAM_OK);
QTRY_VERIFY(snapshots.size() > before);
QCOMPARE(snapshots.last().mid(9), QByteArray("[]")); QCOMPARE(snapshots.last().at(8), char(1));
stream_close(publisher); publisher = nullptr;
QCOMPARE(stream_publish_open(inst, group, nullptr, &publisher), STREAM_OK);
stream_info info{}; QCOMPARE(stream_get_info(publisher, &info), STREAM_OK); epoch = info.descriptor.source_epoch;
QTRY_VERIFY(snapshots.last().mid(9) != QByteArray("[]"));
QVERIFY(gui_stream_watch(0, 77)); QVERIFY(gui_stream_watch(group)); QTest::qWait(20);
}
void realH264RenderingAndClose() {
#ifdef HAVE_FFMPEG
/* Фикстура протокола выше содержит фиктивный H264; реальный decoder получает отдельную эпоху. */