Мультимедиа

QMediaPlayer: воспроизведение аудио и видео

Введение

QMediaPlayer — основной класс для воспроизведения медиафайлов и потоков в Qt: локальных видео/аудио файлов, сетевых потоков, ресурсов из Qt Resource System. В статье подробно разберём управление воспроизведением — старт, пауза, перемотка, контроль громкости, и реакцию на изменения состояния плеера через сигналы.

Концепция

QMediaPlayer асинхронен — установка источника через setSource() не означает немедленную готовность к воспроизведению, поскольку требуется время на открытие файла и его первичный разбор. Состояние плеера отражается через несколько связанных, но различных понятий: playbackState() (играет/на паузе/остановлен), mediaStatus() (загружается/буферизуется/готов/закончился) и error() для диагностики проблем. Управление позицией воспроизведения происходит через position()/setPosition() в миллисекундах, а громкость — через связанный объект QAudioOutput, а не сам плеер.

Пример кода: QMediaPlayer

#include <QMediaPlayer>
#include <QAudioOutput>
#include <QVideoWidget>
#include <QPushButton>
#include <QSlider>
#include <QVBoxLayout>
#include <QLabel>
#include <QWidget>

class VideoPlayerWidget : public QWidget
{
    Q_OBJECT
public:
    VideoPlayerWidget(QWidget *parent = nullptr) : QWidget(parent)
    {
        m_videoWidget = new QVideoWidget(this);
        m_player = new QMediaPlayer(this);
        m_audioOutput = new QAudioOutput(this);

        m_player->setVideoOutput(m_videoWidget);
        m_player->setAudioOutput(m_audioOutput);

        m_positionSlider = new QSlider(Qt::Horizontal, this);
        m_statusLabel = new QLabel("Готов к воспроизведению", this);
        auto *playButton = new QPushButton("Воспроизвести/Пауза", this);
        auto *volumeSlider = new QSlider(Qt::Horizontal, this);
        volumeSlider->setRange(0, 100);
        volumeSlider->setValue(80);

        auto *layout = new QVBoxLayout(this);
        layout->addWidget(m_videoWidget);
        layout->addWidget(m_positionSlider);
        layout->addWidget(playButton);
        layout->addWidget(volumeSlider);
        layout->addWidget(m_statusLabel);

        connect(playButton, &QPushButton::clicked, this, &VideoPlayerWidget::togglePlayPause);
        connect(volumeSlider, &QSlider::valueChanged, this, [this](int value) {
            m_audioOutput->setVolume(value / 100.0); // громкость в диапазоне 0.0–1.0
        });

        // Обновление слайдера позиции при воспроизведении
        connect(m_player, &QMediaPlayer::positionChanged, this, [this](qint64 position) {
            if (!m_positionSlider->isSliderDown()) { // не сбиваем пользователя, если он тащит слайдер сам
                m_positionSlider->setValue(static_cast<int>(position));
            }
        });

        connect(m_player, &QMediaPlayer::durationChanged, this, [this](qint64 duration) {
            m_positionSlider->setRange(0, static_cast<int>(duration));
        });

        connect(m_positionSlider, &QSlider::sliderMoved, this, [this](int value) {
            m_player->setPosition(value); // перемотка по перетаскиванию слайдера
        });

        // Реакция на изменение статуса медиа — важно для информирования пользователя
        connect(m_player, &QMediaPlayer::mediaStatusChanged, this, &VideoPlayerWidget::onMediaStatusChanged);
        connect(m_player, &QMediaPlayer::errorOccurred, this, &VideoPlayerWidget::onErrorOccurred);
    }

    void openFile(const QString &path)
    {
        m_player->setSource(QUrl::fromLocalFile(path));
    }

private slots:
    void togglePlayPause()
    {
        if (m_player->playbackState() == QMediaPlayer::PlayingState) {
            m_player->pause();
        } else {
            m_player->play();
        }
    }

    void onMediaStatusChanged(QMediaPlayer::MediaStatus status)
    {
        switch (status) {
        case QMediaPlayer::LoadingMedia:
            m_statusLabel->setText("Загрузка...");
            break;
        case QMediaPlayer::BufferingMedia:
            m_statusLabel->setText("Буферизация...");
            break;
        case QMediaPlayer::LoadedMedia:
            m_statusLabel->setText("Загружено, готово к воспроизведению");
            break;
        case QMediaPlayer::EndOfMedia:
            m_statusLabel->setText("Воспроизведение завершено");
            break;
        default:
            break;
        }
    }

    void onErrorOccurred(QMediaPlayer::Error error, const QString &errorString)
    {
        Q_UNUSED(error);
        m_statusLabel->setText("Ошибка: " + errorString);
    }

private:
    QMediaPlayer *m_player;
    QAudioOutput *m_audioOutput;
    QVideoWidget *m_videoWidget;
    QSlider *m_positionSlider;
    QLabel *m_statusLabel;
};

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

togglePlayPause() показывает корректный способ переключения воспроизведения/паузы — проверка playbackState() перед вызовом play()/pause(), а не слепое чередование, что важно, если пауза/воспроизведение может быть инициировано не только этой кнопкой (например, после автоматической остановки при ошибке). Связка слайдера позиции с positionChanged/durationChanged сигналами и обратная связь через sliderMoved — стандартный паттерн UI медиаплеера, где важна проверка isSliderDown() для избежания «дрожания» слайдера, когда пользователь сам тащит его, пока плеер одновременно пытается обновить позицию.

onMediaStatusChanged() обрабатывает именно mediaStatus, а не playbackState — это разные, не взаимоисключающие понятия: плеер может быть в состоянии «Playing» по playbackState, но одновременно находиться в BufferingMedia по mediaStatus, если сетевой поток временно не успевает подгружать данные. Обработка errorOccurred обязательна для информирования пользователя о реальных проблемах — без неё неудачная попытка открыть несуществующий или повреждённый файл может остаться незамеченной.

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

  • Вызов play() сразу после setSource() без ожидания готовности медиа. Хотя QMediaPlayer обычно сам справляется с очередью команд (воспроизведение начнётся, как только источник будет готов), для некоторых сетевых источников с заметной задержкой инициализации лучше дождаться LoadedMedia в mediaStatusChanged перед тем, как полагаться на немедленный старт воспроизведения, особенно если нужна точная синхронизация с другими событиями интерфейса.
  • Управление громкостью через QAudioOutput, а не через сам QMediaPlayer — в Qt6 громкость, в отличие от Qt5, перенесена с плеера на объект аудиовывода; код, скопированный из старых примеров для Qt5 (player->setVolume()), просто не скомпилируется в Qt6, и это частая причина путаницы при миграции старого кода.
  • Забытая проверка isSliderDown() при обновлении позиции слайдера во время воспроизведения — без этой проверки слайдер позиции будет «выпрыгивать» из рук пользователя, который пытается перетащить его для перемотки, поскольку positionChanged продолжает поступать и перезаписывать значение слайдера каждую долю секунды во время воспроизведения.
  • Несогласованность единиц измерения позиции/длительности. position()/duration() возвращают значения в миллисекундах (qint64) — при отображении пользователю их нужно преобразовывать в более понятный формат «минуты:секунды», и забытое преобразование (прямое отображение миллисекунд как «времени») создаёт нечитаемый для пользователя интерфейс.