Ladybird: Remove the Qt Multimedia audio plugin

Now that ladybird was moved moved to its own repository, we don't need
to keep unnecessary features like the Qt Multimedia audio backend.
This means that PulseAudio is now required to have working audio in the
Qt chrome.

Ladybird only still exists in the SerenityOS repo to make testing LibWeb
easier.
This commit is contained in:
Sönke Holz
2025-08-08 22:14:14 +02:00
parent 24a18a27fc
commit 82b46b7b26
9 changed files with 6 additions and 472 deletions

View File

@@ -7,12 +7,10 @@
Qt6 development packages and a C++23 capable compiler are required. g++-13 or clang-17 are required at a minimum for c++23 support.
NOTE: In all of the below lists of packages, the Qt6 multimedia package is not needed if your Linux system supports PulseAudio.
On Debian/Ubuntu required packages include, but are not limited to:
```
sudo apt install build-essential cmake libgl1-mesa-dev ninja-build qt6-base-dev qt6-tools-dev-tools qt6-multimedia-dev ccache
sudo apt install build-essential cmake libgl1-mesa-dev ninja-build qt6-base-dev qt6-tools-dev-tools ccache
```
For Ubuntu 20.04 and above, ensure that the Qt6 Wayland packages are available:
@@ -24,19 +22,19 @@ sudo apt install qt6-wayland
On Arch Linux/Manjaro:
```
sudo pacman -S --needed base-devel cmake libgl ninja qt6-base qt6-tools qt6-wayland qt6-multimedia ccache
sudo pacman -S --needed base-devel cmake libgl ninja qt6-base qt6-tools qt6-wayland ccache
```
On Fedora or derivatives:
```
sudo dnf install cmake libglvnd-devel ninja-build qt6-qtbase-devel qt6-qttools-devel qt6-qtwayland-devel qt6-qtmultimedia-devel ccache
sudo dnf install cmake libglvnd-devel ninja-build qt6-qtbase-devel qt6-qttools-devel qt6-qtwayland-devel ccache
```
On openSUSE:
```
sudo zypper install cmake libglvnd-devel ninja qt6-base-devel qt6-multimedia-devel qt6-tools-devel qt6-wayland-devel ccache
sudo zypper install cmake libglvnd-devel ninja qt6-base-devel qt6-tools-devel qt6-wayland-devel ccache
```
On NixOS or with Nix:
@@ -83,7 +81,7 @@ pfexec pkg install cmake ninja clang-17 libglvnd qt6
On Haiku:
```
pkgman install cmake ninja cmd:python3 qt6_base_devel qt6_multimedia_devel qt6_tools_devel openal_devel
pkgman install cmake ninja cmd:python3 qt6_base_devel qt6_tools_devel openal_devel
```
On Windows:

View File

@@ -1,67 +0,0 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "AudioCodecPluginQt.h"
#include "AudioThread.h"
#include <LibAudio/Loader.h>
namespace Ladybird {
ErrorOr<NonnullOwnPtr<AudioCodecPluginQt>> AudioCodecPluginQt::create(NonnullRefPtr<Audio::Loader> loader)
{
auto audio_thread = TRY(AudioThread::create(move(loader)));
audio_thread->start();
return adopt_nonnull_own_or_enomem(new (nothrow) AudioCodecPluginQt(move(audio_thread)));
}
AudioCodecPluginQt::AudioCodecPluginQt(NonnullOwnPtr<AudioThread> audio_thread)
: m_audio_thread(move(audio_thread))
{
connect(m_audio_thread, &AudioThread::playback_position_updated, this, [this](auto position) {
if (on_playback_position_updated)
on_playback_position_updated(position);
});
}
AudioCodecPluginQt::~AudioCodecPluginQt()
{
m_audio_thread->stop().release_value_but_fixme_should_propagate_errors();
}
void AudioCodecPluginQt::resume_playback()
{
m_audio_thread->queue_task({ AudioTask::Type::Play }).release_value_but_fixme_should_propagate_errors();
}
void AudioCodecPluginQt::pause_playback()
{
m_audio_thread->queue_task({ AudioTask::Type::Pause }).release_value_but_fixme_should_propagate_errors();
}
void AudioCodecPluginQt::set_volume(double volume)
{
AudioTask task { AudioTask::Type::Volume };
task.data = volume;
m_audio_thread->queue_task(move(task)).release_value_but_fixme_should_propagate_errors();
}
void AudioCodecPluginQt::seek(double position)
{
AudioTask task { AudioTask::Type::Seek };
task.data = position;
m_audio_thread->queue_task(move(task)).release_value_but_fixme_should_propagate_errors();
}
Duration AudioCodecPluginQt::duration()
{
return m_audio_thread->duration();
}
}

View File

@@ -1,42 +0,0 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Error.h>
#include <AK/NonnullOwnPtr.h>
#include <AK/NonnullRefPtr.h>
#include <LibAudio/Forward.h>
#include <LibWeb/Platform/AudioCodecPlugin.h>
#include <QObject>
namespace Ladybird {
class AudioThread;
class AudioCodecPluginQt final
: public QObject
, public Web::Platform::AudioCodecPlugin {
Q_OBJECT
public:
static ErrorOr<NonnullOwnPtr<AudioCodecPluginQt>> create(NonnullRefPtr<Audio::Loader>);
virtual ~AudioCodecPluginQt() override;
virtual void resume_playback() override;
virtual void pause_playback() override;
virtual void set_volume(double) override;
virtual void seek(double) override;
virtual Duration duration() override;
private:
explicit AudioCodecPluginQt(NonnullOwnPtr<AudioThread>);
NonnullOwnPtr<AudioThread> m_audio_thread;
};
}

View File

@@ -1,212 +0,0 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "AudioThread.h"
#include <LibWeb/Platform/AudioCodecPlugin.h>
namespace Ladybird {
struct AudioDevice {
static AudioDevice create(Audio::Loader const& loader)
{
auto const& device_info = QMediaDevices::defaultAudioOutput();
auto format = device_info.preferredFormat();
format.setSampleRate(static_cast<int>(loader.sample_rate()));
format.setChannelCount(2);
auto audio_output = make<QAudioSink>(device_info, format);
return AudioDevice { move(audio_output) };
}
AudioDevice(AudioDevice&&) = default;
AudioDevice& operator=(AudioDevice&& device)
{
if (audio_output) {
audio_output->stop();
io_device = nullptr;
}
swap(audio_output, device.audio_output);
swap(io_device, device.io_device);
return *this;
}
~AudioDevice()
{
if (audio_output)
audio_output->stop();
}
OwnPtr<QAudioSink> audio_output;
QIODevice* io_device { nullptr };
private:
explicit AudioDevice(NonnullOwnPtr<QAudioSink> output)
: audio_output(move(output))
{
io_device = audio_output->start();
}
};
ErrorOr<NonnullOwnPtr<AudioThread>> AudioThread::create(NonnullRefPtr<Audio::Loader> loader)
{
auto task_queue = TRY(AudioTaskQueue::create());
return adopt_nonnull_own_or_enomem(new (nothrow) AudioThread(move(loader), move(task_queue)));
}
ErrorOr<void> AudioThread::stop()
{
TRY(queue_task({ AudioTask::Type::Stop }));
wait();
return {};
}
ErrorOr<void> AudioThread::queue_task(AudioTask task)
{
return m_task_queue.blocking_enqueue(move(task), []() {
usleep(UPDATE_RATE_MS * 1000);
});
}
AudioThread::AudioThread(NonnullRefPtr<Audio::Loader> loader, AudioTaskQueue task_queue)
: m_loader(move(loader))
, m_task_queue(move(task_queue))
{
auto duration = static_cast<double>(m_loader->total_samples()) / static_cast<double>(m_loader->sample_rate());
m_duration = Duration::from_milliseconds(static_cast<i64>(duration * 1000.0));
}
void AudioThread::run()
{
auto devices = make<QMediaDevices>();
auto audio_device = AudioDevice::create(m_loader);
connect(devices, &QMediaDevices::audioOutputsChanged, this, [this]() {
queue_task({ AudioTask::Type::RecreateAudioDevice }).release_value_but_fixme_should_propagate_errors();
});
auto paused = Paused::Yes;
while (true) {
auto& audio_output = audio_device.audio_output;
auto* io_device = audio_device.io_device;
if (auto result = m_task_queue.dequeue(); result.is_error()) {
VERIFY(result.error() == AudioTaskQueue::QueueStatus::Empty);
} else {
auto task = result.release_value();
switch (task.type) {
case AudioTask::Type::Stop:
return;
case AudioTask::Type::Play:
audio_output->resume();
paused = Paused::No;
break;
case AudioTask::Type::Pause:
audio_output->suspend();
paused = Paused::Yes;
break;
case AudioTask::Type::Seek:
VERIFY(task.data.has_value());
m_position = Web::Platform::AudioCodecPlugin::set_loader_position(m_loader, *task.data, m_duration);
if (paused == Paused::Yes)
Q_EMIT playback_position_updated(m_position);
break;
case AudioTask::Type::Volume:
VERIFY(task.data.has_value());
audio_output->setVolume(*task.data);
break;
case AudioTask::Type::RecreateAudioDevice:
audio_device = AudioDevice::create(m_loader);
continue;
}
}
if (paused == Paused::No) {
if (auto result = play_next_samples(*audio_output, *io_device); result.is_error()) {
// FIXME: Propagate the error to the HTMLMediaElement.
} else {
Q_EMIT playback_position_updated(m_position);
paused = result.value();
}
}
usleep(UPDATE_RATE_MS * 1000);
}
}
ErrorOr<AudioThread::Paused> AudioThread::play_next_samples(QAudioSink& audio_output, QIODevice& io_device)
{
bool all_samples_loaded = m_loader->loaded_samples() >= m_loader->total_samples();
if (all_samples_loaded) {
audio_output.suspend();
(void)m_loader->reset();
m_position = m_duration;
return Paused::Yes;
}
auto bytes_available = audio_output.bytesFree();
auto bytes_per_sample = audio_output.format().bytesPerSample();
auto channel_count = audio_output.format().channelCount();
auto samples_to_load = bytes_available / bytes_per_sample / channel_count;
auto samples = TRY(Web::Platform::AudioCodecPlugin::read_samples_from_loader(*m_loader, samples_to_load));
enqueue_samples(audio_output, io_device, move(samples));
m_position = Web::Platform::AudioCodecPlugin::current_loader_position(m_loader);
return Paused::No;
}
void AudioThread::enqueue_samples(QAudioSink const& audio_output, QIODevice& io_device, FixedArray<Audio::Sample> samples)
{
auto buffer_size = samples.size() * audio_output.format().bytesPerSample() * audio_output.format().channelCount();
if (buffer_size > static_cast<size_t>(m_sample_buffer.size()))
m_sample_buffer.resize(buffer_size);
FixedMemoryStream stream { Bytes { m_sample_buffer.data(), buffer_size } };
for (auto const& sample : samples) {
switch (audio_output.format().sampleFormat()) {
case QAudioFormat::UInt8:
write_sample<u8>(stream, sample.left);
write_sample<u8>(stream, sample.right);
break;
case QAudioFormat::Int16:
write_sample<i16>(stream, sample.left);
write_sample<i16>(stream, sample.right);
break;
case QAudioFormat::Int32:
write_sample<i32>(stream, sample.left);
write_sample<i32>(stream, sample.right);
break;
case QAudioFormat::Float:
write_sample<float>(stream, sample.left);
write_sample<float>(stream, sample.right);
break;
default:
VERIFY_NOT_REACHED();
}
}
io_device.write(m_sample_buffer.data(), buffer_size);
}
}

View File

@@ -1,103 +0,0 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Endian.h>
#include <AK/MemoryStream.h>
#include <AK/Optional.h>
#include <AK/Types.h>
#include <LibAudio/Loader.h>
#include <LibAudio/Sample.h>
#include <LibCore/SharedCircularQueue.h>
#include <QAudioFormat>
#include <QAudioSink>
#include <QByteArray>
#include <QMediaDevices>
#include <QThread>
namespace Ladybird {
static constexpr u32 UPDATE_RATE_MS = 10;
struct AudioTask {
enum class Type {
Stop,
Play,
Pause,
Seek,
Volume,
RecreateAudioDevice,
};
Type type;
Optional<double> data {};
};
using AudioTaskQueue = Core::SharedSingleProducerCircularQueue<AudioTask>;
class AudioThread final : public QThread { // We have to use QThread, otherwise internal Qt media QTimer objects do not work.
Q_OBJECT
public:
static ErrorOr<NonnullOwnPtr<AudioThread>> create(NonnullRefPtr<Audio::Loader> loader);
ErrorOr<void> stop();
Duration duration() const { return m_duration; }
ErrorOr<void> queue_task(AudioTask task);
Q_SIGNALS:
void playback_position_updated(Duration);
private:
AudioThread(NonnullRefPtr<Audio::Loader> loader, AudioTaskQueue task_queue);
enum class Paused {
Yes,
No,
};
void run() override;
ErrorOr<Paused> play_next_samples(QAudioSink& audio_output, QIODevice& io_device);
void enqueue_samples(QAudioSink const& audio_output, QIODevice& io_device, FixedArray<Audio::Sample> samples);
template<typename T>
void write_sample(FixedMemoryStream& stream, float sample)
{
// The values that need to be written to the stream vary depending on the output channel format, and isn't
// particularly well documented. The value derivations performed below were adapted from a Qt example:
// https://code.qt.io/cgit/qt/qtmultimedia.git/tree/examples/multimedia/audiooutput/audiooutput.cpp?h=6.4.2#n46
LittleEndian<T> pcm;
if constexpr (IsSame<T, u8>)
pcm = static_cast<u8>((sample + 1.0f) / 2 * NumericLimits<u8>::max());
else if constexpr (IsSame<T, i16>)
pcm = static_cast<i16>(sample * NumericLimits<i16>::max());
else if constexpr (IsSame<T, i32>)
pcm = static_cast<i32>(sample * NumericLimits<i32>::max());
else if constexpr (IsSame<T, float>)
pcm = sample;
else
static_assert(DependentFalse<T>);
MUST(stream.write_value(pcm));
}
NonnullRefPtr<Audio::Loader> m_loader;
AudioTaskQueue m_task_queue;
QByteArray m_sample_buffer;
Duration m_duration;
Duration m_position;
};
}

View File

@@ -28,18 +28,6 @@ if (ENABLE_QT)
target_link_libraries(WebContent PRIVATE Qt::Core Qt::Network)
target_compile_definitions(WebContent PRIVATE HAVE_QT=1)
set_target_properties(WebContent PROPERTIES AUTOMOC ON)
if (NOT HAVE_PULSEAUDIO)
find_package(Qt6 REQUIRED COMPONENTS Multimedia)
target_sources(WebContent PRIVATE
../Qt/AudioCodecPluginQt.cpp
../Qt/AudioThread.cpp
)
target_link_libraries(WebContent PRIVATE Qt::Multimedia)
target_compile_definitions(WebContent PRIVATE HAVE_QT_MULTIMEDIA=1)
endif()
else()
set(LIB_TYPE STATIC)
if (IOS)

View File

@@ -39,10 +39,6 @@
# include <Ladybird/Qt/EventLoopImplementationQt.h>
# include <Ladybird/Qt/RequestManagerQt.h>
# include <QCoreApplication>
# if defined(HAVE_QT_MULTIMEDIA)
# include <Ladybird/Qt/AudioCodecPluginQt.h>
# endif
#endif
#if defined(AK_OS_MACOS)
@@ -82,9 +78,7 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
Web::Platform::ImageCodecPlugin::install(*new Ladybird::ImageCodecPlugin);
Web::Platform::AudioCodecPlugin::install_creation_hook([](auto loader) {
#if defined(HAVE_QT_MULTIMEDIA)
return Ladybird::AudioCodecPluginQt::create(move(loader));
#elif defined(AK_OS_MACOS) || defined(HAVE_PULSEAUDIO)
#if defined(AK_OS_MACOS) || defined(HAVE_PULSEAUDIO)
return Web::Platform::AudioCodecPluginAgnostic::create(move(loader));
#else
(void)loader;

View File

@@ -12,7 +12,6 @@ mkShell.override { stdenv = gcc13Stdenv; } {
pkg-config
qt6.qtbase
qt6.qtbase.dev
qt6.qtmultimedia
qt6.qttools
qt6.qtwayland
qt6.qtwayland.dev

View File

@@ -3,20 +3,11 @@ import("//Ladybird/link_qt.gni")
import("//Ladybird/moc_qt_objects.gni")
import("//Meta/gn/build/libs/pulse/enable.gni")
enable_qt_multimedia = !enable_pulseaudio && current_os != "mac"
moc_qt_objects("generate_moc") {
sources = [
"//Ladybird/Qt/EventLoopImplementationQtEventTarget.h",
"//Ladybird/Qt/RequestManagerQt.h",
]
if (enable_qt_multimedia) {
sources += [
"//Ladybird/Qt/AudioCodecPluginQt.cpp",
"//Ladybird/Qt/AudioThread.cpp",
]
}
}
link_qt("WebContent_qt") {
@@ -24,10 +15,6 @@ link_qt("WebContent_qt") {
"Core",
"Network",
]
if (enable_qt_multimedia) {
qt_components += [ "Multimedia" ]
}
}
executable("WebContent") {
@@ -83,14 +70,6 @@ executable("WebContent") {
"//Ladybird/Qt/WebSocketQt.cpp",
]
if (enable_qt_multimedia) {
defines += [ "HAVE_QT_MULTIMEDIA" ]
sources += [
"//Ladybird/Qt/AudioCodecPluginQt.cpp",
"//Ladybird/Qt/AudioThread.cpp",
]
}
sources += get_target_outputs(":generate_moc")
deps += [ ":generate_moc" ]
}