Введение
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.