Browse Source

Play live stereo on desktop and verify channel separation

master
evgeny 1 week ago
parent
commit
214a98efb1
  1. 4
      tests/test_android_stream.c
  2. 12
      tools/chatgui/src/streamplayer_window.cpp
  3. 28
      tools/chatgui/tests/test_stream_view.cpp

4
tests/test_android_stream.c

@ -18,7 +18,7 @@
#define CHECK(x) do { if (!(x)) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "Android stream test line=%d: %s", __LINE__, #x); abort(); } } while (0)
static uint8_t video[9000];
static struct stream_media_format current_format = {1280, 720, 90, STREAM_MEDIA_H264, STREAM_MEDIA_OPUS, 1};
static struct stream_media_format current_format = {1280, 720, 90, STREAM_MEDIA_H264, STREAM_MEDIA_OPUS_STEREO, 1};
static int feed(uint64_t id, int is_video, uint64_t pts, int key, const uint8_t *data, size_t length) {
return android_stream_feed(id, is_video, pts, key, data, length, is_video ? &current_format : NULL);
}
@ -101,7 +101,7 @@ int main(void) {
/* Commit новой revision удаляет старое видео, сохраняет аудио/epoch и не требует нового owner. */
CHECK(feed(1, 1, 200000, 0, video, sizeof(video)) == 1);
CHECK(feed(1, 0, 120000, 0, (const uint8_t *)"ops", 3) == 1);
struct stream_media_format next_format = {1920, 1080, 270, STREAM_MEDIA_H264, STREAM_MEDIA_OPUS, 2};
struct stream_media_format next_format = {1920, 1080, 270, STREAM_MEDIA_H264, STREAM_MEDIA_OPUS_STEREO, 2};
CHECK(android_stream_feed(1, 1, 300000, 0, video, sizeof(video), &next_format) == 0);
CHECK(android_stream_feed(1, 1, 300000, 1, video, sizeof(video), &next_format) == 1);
CHECK(feed(1, 1, 350000, 1, video, sizeof(video)) == 0);

12
tools/chatgui/src/streamplayer_window.cpp

@ -39,7 +39,7 @@ QString text(const char* value) { return QCoreApplication::translate("ChatGui",
struct StreamPlayerWindow::Playback {
struct Video { QImage image; int64_t pts; };
struct Audio { std::array<int16_t, 960> pcm; int64_t pts; unsigned offset = 0; };
struct Audio { std::array<int16_t, 1920> pcm; int64_t pts; unsigned offset = 0; };
uint64_t id = ++nextId, group = 0, source = 0, epoch = 0;
std::atomic<bool> running{true}, audioAvailable{false};
std::atomic<float> volume{1.0f};
@ -96,11 +96,11 @@ void StreamPlayerWindow::openAudio() {
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 = 1; config.playback.format = ma_format_s16;
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, 0);
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();
if (!p.prefillUntil || now < p.prefillUntil) return;
@ -109,7 +109,7 @@ void StreamPlayerWindow::openAudio() {
auto& packet = p.audios.front();
if (firstPts < 0) firstPts = packet.pts + packet.offset * 1000000LL / 48000 - written * 1000000LL / 48000;
unsigned count = std::min(frames - written, 960 - packet.offset);
for (unsigned i = 0; i < count; i++) pcm[written + i] = static_cast<int16_t>(packet.pcm[packet.offset + i] * volume);
for (unsigned i = 0; i < count * 2; i++) pcm[written * 2 + i] = static_cast<int16_t>(packet.pcm[packet.offset * 2 + i] * volume);
written += count; packet.offset += count;
if (packet.offset == 960) p.audios.pop_front();
}
@ -124,7 +124,7 @@ void StreamPlayerWindow::openAudio() {
m_audio.close(); m_playback->audioAvailable = false; return;
}
m_playback->audioAvailable = true;
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop stream audio output ready id=%llu", (unsigned long long)m_playback->id);
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop stream audio output ready id=%llu rate=48000 channels=2", (unsigned long long)m_playback->id);
}
/* GUI забирает последний готовый кадр по аппаратным часам, не накапливая Qt-сигналы с QImage. */
@ -216,7 +216,7 @@ void StreamPlayerWindow::decode() {
if (record.pts > INT64_MAX) { fail(text("Invalid stream timestamp")); break; }
if (record.kind == StreamRecordKind::Audio) {
if (!p.audioAvailable) continue;
if (!opus) opus = opus_codec_decoder_create(48000, 1);
if (!opus) opus = opus_codec_decoder_create(48000, 2);
if (!opus) { fail(text("Stream audio decoder failed")); break; }
if (expectedAudio >= 0 && (static_cast<int64_t>(record.pts) < expectedAudio - 10000
|| static_cast<int64_t>(record.pts) > expectedAudio + 100000)) {

28
tools/chatgui/tests/test_stream_view.cpp

@ -30,6 +30,7 @@ static QList<QByteArray> snapshots;
#ifdef TEST_NULL_AUDIO
static std::atomic<ma_device_data_proc> originalAudioCallback{nullptr};
static std::atomic<uint64_t> nonzeroSamples{0};
static std::atomic<uint64_t> leftEnergy{0}, rightEnergy{0};
static std::atomic<int64_t> playbackDelayUs{0};
extern "C" int64_t __real_audio_stream_frame_time(ma_device*, unsigned int, int*);
extern "C" int64_t __wrap_audio_stream_frame_time(ma_device* device, unsigned int frames, int* valid) {
@ -46,11 +47,17 @@ 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);
for (unsigned i = 0; i < frames; i++) if (pcm[i]) nonzeroSamples.fetch_add(1);
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];
nonzero += pcm[2 * i] != 0; nonzero += pcm[2 * i + 1] != 0;
}
leftEnergy.fetch_add(left); rightEnergy.fetch_add(right); nonzeroSamples.fetch_add(nonzero);
}
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 == 1) {
if (copy.deviceType == ma_device_type_playback && copy.playback.format == ma_format_s16 && copy.playback.channels == 2) {
originalAudioCallback = copy.dataCallback; copy.dataCallback = observeAudio;
}
return __real_ma_device_init(context, &copy, device);
@ -81,7 +88,7 @@ class StreamViewTest : public QObject {
}
void publish(const QByteArray& video, const QByteArray& audio = {}, unsigned videoFrames = 1) {
stream_media_block media{}; media.format = {160, 90, rotation, STREAM_MEDIA_H264, STREAM_MEDIA_OPUS, revision};
stream_media_block media{}; media.format = {160, 90, rotation, STREAM_MEDIA_H264, STREAM_MEDIA_OPUS_STEREO, revision};
mediaPts += 100000;
media.first_frame_offset = 0; media.video_count = videoFrames;
for (unsigned i = 0; i < videoFrames; i++) media.video[i] = {++frameId, mediaPts + i * 100000 / videoFrames,
@ -172,11 +179,17 @@ private slots:
QByteArray audio;
#ifdef TEST_NULL_AUDIO
QVERIFY(SoundManager::instance()->init());
auto* opus = opus_codec_encoder_create(48000, 1); QVERIFY(opus);
int16_t pcm[960]; for (unsigned i = 0; i < 960; i++) pcm[i] = static_cast<int16_t>(6000 * std::sin(i * 0.05));
auto* opus = opus_codec_encoder_create(48000, 2); QVERIFY(opus);
QCOMPARE(opus_codec_encoder_application_set(opus, OPUS_CODEC_APP_AUDIO), 0);
QCOMPARE(opus_codec_encoder_bitrate_set(opus, 128000), 0);
int16_t pcm[1920];
for (unsigned i = 0; i < 960; i++) {
pcm[2 * i] = static_cast<int16_t>(6000 * std::sin(i * 0.05));
pcm[2 * i + 1] = static_cast<int16_t>(1500 * std::sin(i * 0.13));
}
uint8_t encoded[1275]; int length = opus_codec_encode(opus, pcm, 960, encoded, sizeof(encoded)); QVERIFY(length > 0);
audio = QByteArray(reinterpret_cast<const char*>(encoded), length); opus_codec_encoder_destroy(opus);
nonzeroSamples = 0; playbackDelayUs = 200000;
nonzeroSamples = 0; leftEnergy = rightEnergy = 0; playbackDelayUs = 200000;
#endif
StreamPlayerWindow window(group, inst->node_id, epoch, "Source");
QSignalSpy shown(&window, &StreamPlayerWindow::framePresented); window.show();
@ -190,6 +203,9 @@ private slots:
QCOMPARE(switched.descriptor.source_epoch, epoch); QCOMPARE(switched.owners, 2u);
#ifdef TEST_NULL_AUDIO
QTRY_VERIFY_WITH_TIMEOUT(nonzeroSamples.load() > 0, 5000);
QVERIFY(rightEnergy.load() > 0);
QVERIFY2(leftEnergy.load() > rightEnergy.load() * 8 && leftEnergy.load() < rightEnergy.load() * 25,
"Independent stereo channels must reach the playback device without downmix");
auto* volume = window.findChild<QSlider*>(); QVERIFY(volume); volume->setValue(0);
QTest::qWait(50); uint64_t played = nonzeroSamples.load(); QTest::qWait(100); QCOMPARE(nonzeroSamples.load(), played);
#endif

Loading…
Cancel
Save