Pass direct Core::EventLoop references through media producer and playback plumbing that posts work back to the main thread. The browser process main loops stay alive for the process lifetime, so these paths no longer need weak event loop references. Update LibMedia tests to pass their stack event loop directly to the producer helpers.
210 lines
8 KiB
C++
210 lines
8 KiB
C++
/*
|
|
* Copyright (c) 2023-2026, Gregory Bertilson <gregory@ladybird.org>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <LibCore/EventLoop.h>
|
|
#include <LibCore/ThreadedPromise.h>
|
|
#include <LibThreading/Thread.h>
|
|
|
|
#include "PlaybackStreamPulseAudio.h"
|
|
|
|
namespace Audio {
|
|
|
|
#define TRY_OR_REJECT_AND_EXIT(expression) \
|
|
({ \
|
|
auto&& __temporary_result = (expression); \
|
|
if (__temporary_result.is_error()) [[unlikely]] { \
|
|
warnln("Failure in PulseAudio control thread: {}", __temporary_result.error().string_literal()); \
|
|
main_thread_event_loop.deferred_invoke([promise = move(promise), error = __temporary_result.release_error()] mutable { \
|
|
promise->reject(move(error)); \
|
|
}); \
|
|
internal_state->exit(); \
|
|
return 1; \
|
|
} \
|
|
__temporary_result.release_value(); \
|
|
})
|
|
|
|
NonnullRefPtr<PlaybackStream::CreatePromise> PlaybackStream::create(OutputState initial_output_state, u32 target_latency_ms, AudioDataRequestCallback&& data_request_callback)
|
|
{
|
|
return PlaybackStreamPulseAudio::create(initial_output_state, target_latency_ms, move(data_request_callback));
|
|
}
|
|
|
|
NonnullRefPtr<PlaybackStream::CreatePromise> PlaybackStreamPulseAudio::create(OutputState initial_state, u32 target_latency_ms, AudioDataRequestCallback&& data_request_callback)
|
|
{
|
|
VERIFY(data_request_callback);
|
|
|
|
auto promise = CreatePromise::construct();
|
|
|
|
// Create an internal state for the control thread to hold on to.
|
|
auto internal_state = MUST(adopt_nonnull_ref_or_enomem(new (nothrow) InternalState()));
|
|
auto playback_stream = MUST(adopt_nonnull_ref_or_enomem(new (nothrow) PlaybackStreamPulseAudio(internal_state)));
|
|
auto& main_thread_event_loop = Core::EventLoop::current();
|
|
|
|
// Create the control thread and start it.
|
|
auto thread = MUST(Threading::Thread::try_create("Audio Control"sv, [=, &main_thread_event_loop, data_request_callback = move(data_request_callback)]() mutable {
|
|
auto context = TRY_OR_REJECT_AND_EXIT(PulseAudioContext::the());
|
|
internal_state->set_stream(TRY_OR_REJECT_AND_EXIT(context->create_stream(initial_state, target_latency_ms, [data_request_callback = move(data_request_callback)](PulseAudioStream&, Span<float> buffer) {
|
|
return data_request_callback(buffer);
|
|
})));
|
|
|
|
// PulseAudio retains the last volume it sets for an application. We want to consistently
|
|
// start at 100% volume instead.
|
|
TRY_OR_REJECT_AND_EXIT(internal_state->stream()->set_volume(1.0));
|
|
|
|
{
|
|
main_thread_event_loop.deferred_invoke([promise = move(promise), playback_stream = move(playback_stream)] mutable {
|
|
promise->resolve(move(playback_stream));
|
|
});
|
|
}
|
|
|
|
internal_state->thread_loop();
|
|
return 0;
|
|
}));
|
|
|
|
thread->start();
|
|
thread->detach();
|
|
return promise;
|
|
}
|
|
|
|
PlaybackStreamPulseAudio::PlaybackStreamPulseAudio(NonnullRefPtr<InternalState> state)
|
|
: m_state(move(state))
|
|
{
|
|
}
|
|
|
|
SampleSpecification PlaybackStreamPulseAudio::sample_specification() const
|
|
{
|
|
return m_state->stream()->sample_specification();
|
|
}
|
|
|
|
PlaybackStreamPulseAudio::~PlaybackStreamPulseAudio()
|
|
{
|
|
m_state->exit();
|
|
}
|
|
|
|
#define TRY_OR_REJECT(expression, ...) \
|
|
({ \
|
|
auto&& __temporary_result = (expression); \
|
|
if (__temporary_result.is_error()) [[unlikely]] { \
|
|
promise->reject(__temporary_result.release_error()); \
|
|
return __VA_ARGS__; \
|
|
} \
|
|
__temporary_result.release_value(); \
|
|
})
|
|
|
|
void PlaybackStreamPulseAudio::set_underrun_callback(Function<void()> callback)
|
|
{
|
|
m_state->enqueue([&state = *m_state, callback = move(callback)]() mutable {
|
|
state.stream()->set_underrun_callback(move(callback));
|
|
});
|
|
}
|
|
|
|
NonnullRefPtr<Core::ThreadedPromise<AK::Duration>> PlaybackStreamPulseAudio::resume()
|
|
{
|
|
auto promise = Core::ThreadedPromise<AK::Duration>::create();
|
|
TRY_OR_REJECT(m_state->check_is_running(), promise);
|
|
m_state->enqueue([&state = *m_state, promise]() {
|
|
TRY_OR_REJECT(state.stream()->resume());
|
|
promise->resolve(state.stream()->total_time_played());
|
|
});
|
|
return promise;
|
|
}
|
|
|
|
NonnullRefPtr<Core::ThreadedPromise<void>> PlaybackStreamPulseAudio::drain_buffer_and_suspend()
|
|
{
|
|
auto promise = Core::ThreadedPromise<void>::create();
|
|
TRY_OR_REJECT(m_state->check_is_running(), promise);
|
|
m_state->enqueue([&state = *m_state, promise]() {
|
|
TRY_OR_REJECT(state.stream()->drain_and_suspend());
|
|
promise->resolve();
|
|
});
|
|
return promise;
|
|
}
|
|
|
|
NonnullRefPtr<Core::ThreadedPromise<void>> PlaybackStreamPulseAudio::discard_buffer_and_suspend()
|
|
{
|
|
auto promise = Core::ThreadedPromise<void>::create();
|
|
TRY_OR_REJECT(m_state->check_is_running(), promise);
|
|
m_state->enqueue([&state = *m_state, promise]() {
|
|
TRY_OR_REJECT(state.stream()->flush_and_suspend());
|
|
promise->resolve();
|
|
});
|
|
return promise;
|
|
}
|
|
|
|
void PlaybackStreamPulseAudio::notify_data_available()
|
|
{
|
|
if (m_state->stream() != nullptr)
|
|
m_state->stream()->notify_data_available();
|
|
}
|
|
|
|
AK::Duration PlaybackStreamPulseAudio::total_time_played() const
|
|
{
|
|
if (m_state->stream() != nullptr)
|
|
return m_state->stream()->total_time_played();
|
|
return AK::Duration::zero();
|
|
}
|
|
|
|
NonnullRefPtr<Core::ThreadedPromise<void>> PlaybackStreamPulseAudio::set_volume(double volume)
|
|
{
|
|
auto promise = Core::ThreadedPromise<void>::create();
|
|
TRY_OR_REJECT(m_state->check_is_running(), promise);
|
|
m_state->enqueue([&state = *m_state, promise, volume]() {
|
|
TRY_OR_REJECT(state.stream()->set_volume(volume));
|
|
promise->resolve();
|
|
});
|
|
return promise;
|
|
}
|
|
|
|
ErrorOr<void> PlaybackStreamPulseAudio::InternalState::check_is_running()
|
|
{
|
|
if (m_exit)
|
|
return Error::from_string_literal("PulseAudio control thread loop is not running");
|
|
return {};
|
|
}
|
|
|
|
void PlaybackStreamPulseAudio::InternalState::set_stream(NonnullRefPtr<PulseAudioStream>&& stream)
|
|
{
|
|
m_stream = move(stream);
|
|
}
|
|
|
|
RefPtr<PulseAudioStream> const& PlaybackStreamPulseAudio::InternalState::stream()
|
|
{
|
|
return m_stream;
|
|
}
|
|
|
|
void PlaybackStreamPulseAudio::InternalState::enqueue(Function<void()>&& task)
|
|
{
|
|
Sync::MutexLocker locker { m_mutex };
|
|
m_tasks.enqueue(forward<Function<void()>>(task));
|
|
m_wake_condition.signal();
|
|
}
|
|
|
|
void PlaybackStreamPulseAudio::InternalState::thread_loop()
|
|
{
|
|
while (true) {
|
|
auto task = [this]() -> Function<void()> {
|
|
Sync::MutexLocker locker { m_mutex };
|
|
|
|
while (m_tasks.is_empty() && !m_exit)
|
|
m_wake_condition.wait();
|
|
if (m_exit)
|
|
return nullptr;
|
|
return m_tasks.dequeue();
|
|
}();
|
|
if (!task) {
|
|
VERIFY(m_exit);
|
|
break;
|
|
}
|
|
task();
|
|
}
|
|
}
|
|
|
|
void PlaybackStreamPulseAudio::InternalState::exit()
|
|
{
|
|
m_exit = true;
|
|
m_wake_condition.signal();
|
|
}
|
|
|
|
}
|