Мультимедиа

Воспроизведение аудио: QMediaPlayer, плейлисты, форматы

Введение

Статья 467 показала минимальный пример воспроизведения — в этой статье углубимся в практические детали: управление состоянием воспроизведения, реализацию плейлиста (поскольку начиная с Qt6 встроенный класс плейлиста был убран из самого API), и понимание, какие аудиоформаты реально поддерживаются.

Концепция

QMediaPlayer предоставляет сигналы для отслеживания состояния воспроизведения (playbackStateChanged, positionChanged, durationChanged), необходимые для построения пользовательского интерфейса плеера (прогресс-бар, кнопки play/pause, отображение текущего времени). Плейлист в Qt6 (в отличие от удалённого класса QMediaPlaylist ранних версий Qt5) реализуется на уровне кода приложения — простой список источников (QUrl) с собственной логикой переключения на следующий трек при сигнале о завершении текущего. Поддержка конкретных аудиоформатов (MP3, FLAC, OGG) зависит от используемого на конкретной платформе мультимедийного бэкенда, и не гарантирована универсально для всех форматов на всех платформах одинаково.

Пример кода

// Плейлист как явная, написанная в коде приложения логика — собственный класс, а не встроенный API
class SimplePlaylist : public QObject
{
    Q_OBJECT
public:
    SimplePlaylist(QMediaPlayer *player, QObject *parent = nullptr)
        : QObject(parent), m_player(player)
    {
        connect(m_player, &QMediaPlayer::mediaStatusChanged, this, &SimplePlaylist::onMediaStatusChanged);
    }

    void addTrack(const QUrl &url) { m_tracks.append(url); }

    void playFrom(int index)
    {
        if (index < 0 || index >= m_tracks.size()) return;
        m_currentIndex = index;
        m_player->setSource(m_tracks[m_currentIndex]);
        m_player->play();
    }

    void playNext() { playFrom(m_currentIndex + 1); }

private slots:
    void onMediaStatusChanged(QMediaPlayer::MediaStatus status)
    {
        if (status == QMediaPlayer::EndOfMedia) {
            playNext(); // автоматический переход к следующему треку по завершении текущего
        }
    }

private:
    QMediaPlayer *m_player;
    QList<QUrl> m_tracks;
    int m_currentIndex = -1;
};
// Отслеживание прогресса воспроизведения для построения UI (прогресс-бар, текущее время)
void connectProgressTracking(QMediaPlayer *player, QSlider *progressSlider, QLabel *timeLabel)
{
    QObject::connect(player, &QMediaPlayer::durationChanged, progressSlider, &QSlider::setMaximum);

    QObject::connect(player, &QMediaPlayer::positionChanged, [progressSlider, timeLabel](qint64 position) {
        progressSlider->setValue(static_cast<int>(position));
        timeLabel->setText(QTime::fromMSecsSinceStartOfDay(position).toString("mm:ss"));
    });

    QObject::connect(progressSlider, &QSlider::sliderMoved, player, &QMediaPlayer::setPosition);
    // ^ пользователь может перетаскивать слайдер для перемотки — setPosition() выполняет реальную перемотку
}

Пояснения к коду

SimplePlaylist показывает идиоматичный для Qt6 подход к реализации плейлиста — подписка на mediaStatusChanged и проверка статуса EndOfMedia дают точку, в которой код приложения сам решает, что делать дальше (переход к следующему треку, повтор, остановка), что даёт значительно больше гибкости по сравнению со встроенным, но негибким классом плейлиста более старых версий Qt. Связка сигналов durationChanged/positionChanged с QSlider/QLabel показывает стандартный паттерн построения интерфейса плеера — прогресс-бар автоматически обновляется по мере воспроизведения через сигнал positionChanged, а связь в обратном направлении (sliderMovedsetPosition) даёт пользователю возможность перематывать воспроизведение, перетаскивая тот же слайдер.

Подводные камни

  • Использование valueChanged вместо sliderMoved для связи слайдера с перемоткойvalueChanged срабатывает как от пользовательского взаимодействия, так и от программного изменения значения слайдера (включая обновление позиции из positionChanged самого плеера), что создаёт цикл обратной связи (плеер обновляет позицию слайдера → срабатывает valueChanged → код пытается переместить позицию плеера → и так далее) — sliderMoved срабатывает строго от реального действия пользователя, избегая этой проблемы.
  • Игнорирование того, что durationChanged может сработать не сразу при setSource(), а только после того, как плеер успеет проанализировать метаданные файла (особенно для сетевых источников, статья 474, где длительность может стать известна не сразу) — код, ожидающий немедленной доступности корректной длительности сразу после установки источника, может получить значение 0 или -1, и для надёжного отображения нужно дождаться именно сигнала durationChanged, а не пытаться прочитать duration() синхронно немедленно после setSource().
  • Предположение об универсальной поддержке всех популярных аудиоформатов на любой целевой платформе без явной проверки — поддержка конкретных кодеков (особенно лицензионно нагруженных, как MP3 в некоторых конфигурациях Linux-дистрибутивов) зависит от установленного на конкретной системе мультимедийного бэкенда, и приложение, рассчитывающее на воспроизведение конкретного формата, должно либо явно проверить эту поддержку, либо предоставить пользователю понятную диагностику при неудачной попытке воспроизведения неподдерживаемого формата (через сигнал errorOccurred).
  • Утечка соединений connect() при многократном пересоздании плейлиста или плеера без явного использования контекста удаления (передачи this как четвёртого аргумента lambda-варианта connect, или использования родительско-дочерних отношений Qt-объектов) — без этого соединения могут продолжать существовать и срабатывать даже после логического «удаления» связанного с ними объекта пользовательского интерфейса, что приводит к трудно диагностируемым ошибкам обращения к уже недействительным объектам.