Мультимедиа

Плейлисты и управление очередью воспроизведения

Введение

QMediaPlayer сам по себе воспроизводит один источник за раз — для управления очередью из нескольких треков (плейлист с возможностью переключения, повтора, перемешивания) нужна дополнительная логика поверх базового плеера, поскольку начиная с Qt6 встроенный класс плейлиста (QMediaPlaylist, существовавший в Qt5) был удалён из модуля. В статье разберём, как реализовать собственный, простой менеджер плейлиста.

Концепция

Идея простого плейлист-менеджера — хранить список источников (QUrl) и текущий индекс воспроизведения, переключая source плеера на следующий/предыдущий элемент списка по запросу пользователя или автоматически при сигнале mediaStatusChanged == EndOfMedia. Дополнительные режимы (повтор одного трека, повтор всего плейлиста, случайный порядок) реализуются как варианты логики вычисления «следующего» индекса.

Пример кода: Плейлисты и управление

// playlistmanager.h — собственная реализация плейлиста поверх QMediaPlayer
#pragma once
#include <QMediaPlayer>
#include <QAudioOutput>
#include <QVector>
#include <QUrl>
#include <QRandomGenerator>

class PlaylistManager : public QObject
{
    Q_OBJECT
public:
    enum class RepeatMode { None, RepeatOne, RepeatAll };

    explicit PlaylistManager(QObject *parent = nullptr) : QObject(parent)
    {
        m_player.setAudioOutput(&m_audioOutput);

        connect(&m_player, &QMediaPlayer::mediaStatusChanged, this, [this](QMediaPlayer::MediaStatus status) {
            if (status == QMediaPlayer::EndOfMedia) {
                onTrackFinished();
            }
        });
    }

    void setTracks(const QVector<QUrl> &tracks)
    {
        m_tracks = tracks;
        m_currentIndex = m_tracks.isEmpty() ? -1 : 0;
        loadCurrentTrack();
    }

    void playNext()
    {
        if (m_tracks.isEmpty()) return;

        if (m_shuffleEnabled) {
            m_currentIndex = QRandomGenerator::global()->bounded(m_tracks.size());
        } else {
            m_currentIndex = (m_currentIndex + 1) % m_tracks.size();
        }
        loadCurrentTrack();
        m_player.play();
    }

    void playPrevious()
    {
        if (m_tracks.isEmpty()) return;
        m_currentIndex = (m_currentIndex - 1 + m_tracks.size()) % m_tracks.size();
        loadCurrentTrack();
        m_player.play();
    }

    void setRepeatMode(RepeatMode mode) { m_repeatMode = mode; }
    void setShuffleEnabled(bool enabled) { m_shuffleEnabled = enabled; }

    QMediaPlayer *player() { return &m_player; }

private slots:
    void onTrackFinished()
    {
        if (m_repeatMode == RepeatMode::RepeatOne) {
            m_player.setPosition(0);
            m_player.play();
            return;
        }

        const bool isLastTrack = (m_currentIndex == m_tracks.size() - 1);
        if (isLastTrack && m_repeatMode == RepeatMode::None && !m_shuffleEnabled) {
            emit playlistFinished(); // дошли до конца, повтор не включён — останавливаемся
            return;
        }

        playNext();
    }

private:
    void loadCurrentTrack()
    {
        if (m_currentIndex < 0 || m_currentIndex >= m_tracks.size()) return;
        m_player.setSource(m_tracks[m_currentIndex]);
        emit currentTrackChanged(m_currentIndex);
    }

signals:
    void currentTrackChanged(int index);
    void playlistFinished();

private:
    QMediaPlayer m_player;
    QAudioOutput m_audioOutput;
    QVector<QUrl> m_tracks;
    int m_currentIndex = -1;
    RepeatMode m_repeatMode = RepeatMode::None;
    bool m_shuffleEnabled = false;
};

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

PlaylistManager хранит список треков как QVector<QUrl> и текущий индекс — playNext()/playPrevious() вычисляют новый индекс с учётом цикличности списка (оператор % для перехода от последнего трека снова к первому) и режима перемешивания. onTrackFinished() — центральная логика автоматического перехода, вызываемая при обнаружении EndOfMedia: она различает три ситуации — повтор одного трека (просто сброс позиции на 0 без смены трека), достижение конца плейлиста без включённого повтора (испускание сигнала playlistFinished без дальнейшего автоматического воспроизведения), и обычный переход к следующему треку во всех остальных случаях.

Сигнал currentTrackChanged позволяет интерфейсу (например, списку треков в UI) подсвечивать текущий воспроизводимый трек, реагируя на изменение индекса, не дублируя логику отслеживания текущей позиции в плейлисте отдельно от самого PlaylistManager.

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

  • Race condition между ручным переключением трека пользователем и автоматическим переходом по EndOfMedia. Если пользователь нажимает «следующий трек» в момент, очень близкий к естественному завершению текущего трека, оба пути (явный вызов playNext() и обработчик onTrackFinished()) теоретически могут сработать почти одновременно — для надёжности стоит явно проверять состояние плеера перед переключением или использовать защитный флаг, предотвращающий двойную обработку.
  • Отсутствие предварительной загрузки (preloading) следующего трека для бесшовного перехода. В показанной реализации loadCurrentTrack() вызывает setSource() только в момент перехода — для треков с заметным временем инициализации (особенно сетевых источников) это означает короткую паузу между треками, в отличие от профессиональных аудиоплееров, которые заранее, в фоне, готовят следующий трек к воспроизведению.
  • Режим перемешивания (shuffle), реализованный через полностью случайный выбор индекса при каждом переходе, может с заметной вероятностью повторно выбрать недавно проигранный трек (или даже тот же самый) — более продуманная реализация шаффла обычно поддерживает отдельный, заранее перемешанный порядок воспроизведения без повторов, пока не закончится весь список, а не выбор случайного индекса при каждом отдельном переходе.
  • Отсутствие сохранения и восстановления состояния плейлиста (текущий трек, позиция, режимы повтора/перемешивания) между запусками приложения. Если пользователь ожидает, что приложение продолжит воспроизведение с того места, где он его оставил в прошлый раз, нужна явная логика сохранения этого состояния (например, через QSettings, основной цикл статей), не предоставляемая автоматически самим PlaylistManager.