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