Revision control

++ // Copyright (C) 2017 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
++
++ #include "player.h"
++ #include "playercontrols.h"
++ #include "playlistmodel.h"
++ #include "qmediaplaylist.h"
++ #include "videowidget.h"
++ #include "audiolevelmeter.h"
++
++ #include <QApplication>
++ #include <QAudioDevice>
++ #include <QAudioOutput>
++ #include <QBoxLayout>
++ #include <QComboBox>
++ #include <QDir>
++ #include <QFileDialog>
++ #include <QHBoxLayout>
++ #include <QLabel>
++ #include <QLineEdit>
++ #include <QListView>
++ #include <QMediaDevices>
++ #include <QMediaFormat>
++ #include <QMediaMetaData>
++ #include <QMessageBox>
++ #include <QPushButton>
++ #include <QSlider>
++ #include <QStandardPaths>
++ #include <QStatusBar>
++ #include <QVBoxLayout>
++ #include <QAudioBufferOutput>
++
++ using namespace Qt::Literals;
++ using namespace Qt::StringLiterals;
++
++ Player::Player(QWidget *parent) : QWidget(parent)
++ {
++ //! [create-objs]
++ m_player = new QMediaPlayer(this);
++ m_audioOutput = new QAudioOutput(this);
++ m_player->setAudioOutput(m_audioOutput);
++ //! [create-objs]
++ connect(m_player, &QMediaPlayer::durationChanged, this, &Player::durationChanged);
++ connect(m_player, &QMediaPlayer::positionChanged, this, &Player::positionChanged);
++ connect(m_player, &QMediaPlayer::metaDataChanged, this, &Player::metaDataChanged);
++ connect(m_player, &QMediaPlayer::mediaStatusChanged, this, &Player::statusChanged);
++ connect(m_player, &QMediaPlayer::bufferProgressChanged, this, &Player::bufferingProgress);
++ connect(m_player, &QMediaPlayer::hasVideoChanged, this, &Player::videoAvailableChanged);
++ connect(m_player, &QMediaPlayer::errorChanged, this, &Player::displayErrorMessage);
++ connect(m_player, &QMediaPlayer::tracksChanged, this, &Player::tracksChanged);
++
++ //! [2]
++ m_videoWidget = new VideoWidget(this);
++ m_videoWidget->resize(1280, 720);
++ m_player->setVideoOutput(m_videoWidget);
++
++ m_playlistModel = new PlaylistModel(this);
++ m_playlist = m_playlistModel->playlist();
++ //! [2]
++ connect(m_playlist, &QMediaPlaylist::currentIndexChanged, this,
++ &Player::playlistPositionChanged);
++
++ // audio level meter
++ m_audioBufferOutput = new QAudioBufferOutput(this);
++ m_player->setAudioBufferOutput(m_audioBufferOutput);
++ m_audioLevelMeter = new AudioLevelMeter(this);
++ connect(m_audioBufferOutput, &QAudioBufferOutput::audioBufferReceived,
++ m_audioLevelMeter, &AudioLevelMeter::onAudioBufferReceived);
++ connect(m_player, &QMediaPlayer::playingChanged,
++ m_audioLevelMeter, &AudioLevelMeter::deactivate);
++
++ // player layout
++ QBoxLayout *layout = new QVBoxLayout(this);
++
++ // display
++ QBoxLayout *displayLayout = new QHBoxLayout;
++ displayLayout->addWidget(m_videoWidget, 2);
++ displayLayout->addWidget(m_audioLevelMeter, 3);
++ #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
++ m_playlistView = new QListView();
++ m_playlistView->setModel(m_playlistModel);
++ m_playlistView->setCurrentIndex(m_playlistModel->index(m_playlist->currentIndex(), 0));
++ connect(m_playlistView, &QAbstractItemView::activated, this, &Player::jump);
++ displayLayout->addWidget(m_playlistView);
++ #endif
++ layout->addLayout(displayLayout);
++
++ // duration slider and label
++ QHBoxLayout *hLayout = new QHBoxLayout;
++
++ m_slider = new QSlider(Qt::Horizontal, this);
++ m_slider->setRange(0, m_player->duration());
++ connect(m_slider, &QSlider::sliderMoved, this, &Player::seek);
++ hLayout->addWidget(m_slider);
++
++ m_labelDuration = new QLabel();
++ m_labelDuration->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
++ hLayout->addWidget(m_labelDuration);
++ layout->addLayout(hLayout);
++
++ // controls
++ QBoxLayout *controlLayout = new QHBoxLayout;
++ controlLayout->setContentsMargins(0, 0, 0, 0);
++
++ QPushButton *openButton = new QPushButton(tr("Open"), this);
++ connect(openButton, &QPushButton::clicked, this, &Player::open);
++ controlLayout->addWidget(openButton);
++ controlLayout->addStretch(1);
++
++ PlayerControls *controls = new PlayerControls();
++ controls->setState(m_player->playbackState());
++ controls->setVolume(m_audioOutput->volume());
++ controls->setMuted(controls->isMuted());
++
++ connect(controls, &PlayerControls::play, m_player, &QMediaPlayer::play);
++ connect(controls, &PlayerControls::pause, m_player, &QMediaPlayer::pause);
++ connect(controls, &PlayerControls::stop, m_player, &QMediaPlayer::stop);
++ connect(controls, &PlayerControls::next, m_playlist, &QMediaPlaylist::next);
++ connect(controls, &PlayerControls::previous, this, &Player::previousClicked);
++ connect(controls, &PlayerControls::changeVolume, m_audioOutput, &QAudioOutput::setVolume);
++ connect(controls, &PlayerControls::changeMuting, m_audioOutput, &QAudioOutput::setMuted);
++ connect(controls, &PlayerControls::changeRate, m_player, &QMediaPlayer::setPlaybackRate);
++ connect(controls, &PlayerControls::stop, m_videoWidget, QOverload<>::of(&QVideoWidget::update));
++
++ connect(m_player, &QMediaPlayer::playbackStateChanged, controls,
++ [controls](QMediaPlayer::PlaybackState arg) {
++ controls->setState(arg);
++ });
++ connect(m_audioOutput, &QAudioOutput::volumeChanged, controls, &PlayerControls::setVolume);
++ connect(m_audioOutput, &QAudioOutput::mutedChanged, controls, &PlayerControls::setMuted);
++
++ controlLayout->addWidget(controls);
++ controlLayout->addStretch(1);
++
++ m_fullScreenButton = new QPushButton(tr("FullScreen"), this);
++ m_fullScreenButton->setCheckable(true);
++ controlLayout->addWidget(m_fullScreenButton);
++
++ m_pitchCompensationButton = new QPushButton(tr("Pitch compensation"), this);
++ m_pitchCompensationButton->setCheckable(true);
++ switch (m_player->pitchCompensationAvailability()) {
++ case QMediaPlayer::PitchCompensationAvailability::AlwaysOn:
++ m_pitchCompensationButton->setEnabled(false);
++ m_pitchCompensationButton->setChecked(true);
++ m_pitchCompensationButton->setToolTip(
++ u"Pitch compensation always enabled on this backend"_s);
++ break;
++ case QMediaPlayer::PitchCompensationAvailability::Unavailable:
++ m_pitchCompensationButton->setEnabled(false);
++ m_pitchCompensationButton->setChecked(false);
++ m_pitchCompensationButton->setToolTip(u"Pitch compensation unavailable on this backend"_s);
++ break;
++ case QMediaPlayer::PitchCompensationAvailability::Available:
++ m_pitchCompensationButton->setEnabled(true);
++ m_pitchCompensationButton->setChecked(m_player->pitchCompensation());
++ break;
++ default:
++ Q_UNREACHABLE();
++ }
++
++ controlLayout->addWidget(m_pitchCompensationButton);
++ connect(m_player, &QMediaPlayer::pitchCompensationChanged, this, [this] {
++ m_pitchCompensationButton->setChecked(m_player->pitchCompensation());
++ });
++ m_pitchCompensationButton->setChecked(m_player->pitchCompensation());
++ connect(m_pitchCompensationButton, &QPushButton::toggled, m_player,
++ &QMediaPlayer::setPitchCompensation);
++
++ #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
++ m_audioOutputCombo = new QComboBox(this);
++ controlLayout->addWidget(m_audioOutputCombo);
++
++ updateAudioDevices();
++
++ connect(m_audioOutputCombo, QOverload<int>::of(&QComboBox::activated), this,
++ &Player::audioOutputChanged);
++
++ QObject::connect(&m_mediaDevices, &QMediaDevices::audioOutputsChanged, this, [this] {
++ updateAudioDevices();
++ });
++ #endif
++
++ layout->addLayout(controlLayout);
++
++ // tracks
++ QGridLayout *tracksLayout = new QGridLayout;
++
++ m_audioTracks = new QComboBox(this);
++ connect(m_audioTracks, &QComboBox::activated, this, &Player::selectAudioStream);
++ tracksLayout->addWidget(new QLabel(tr("Audio Tracks:")), 0, 0);
++ tracksLayout->addWidget(m_audioTracks, 0, 1);
++
++ m_videoTracks = new QComboBox(this);
++ connect(m_videoTracks, &QComboBox::activated, this, &Player::selectVideoStream);
++ tracksLayout->addWidget(new QLabel(tr("Video Tracks:")), 1, 0);
++ tracksLayout->addWidget(m_videoTracks, 1, 1);
++
++ m_subtitleTracks = new QComboBox(this);
++ connect(m_subtitleTracks, &QComboBox::activated, this, &Player::selectSubtitleStream);
++ tracksLayout->addWidget(new QLabel(tr("Subtitle Tracks:")), 2, 0);
++ tracksLayout->addWidget(m_subtitleTracks, 2, 1);
++
++ layout->addLayout(tracksLayout);
++
++ #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
++ // metadata
++
++ QLabel *metaDataLabel = new QLabel(tr("Metadata for file:"));
++ layout->addWidget(metaDataLabel);
++
++ QGridLayout *metaDataLayout = new QGridLayout;
++ int key = QMediaMetaData::Title;
++ for (int i = 0; i < (QMediaMetaData::NumMetaData + 2) / 3; i++) {
++ for (int j = 0; j < 6; j += 2) {
++ m_metaDataLabels[key] = new QLabel(
++ QMediaMetaData::metaDataKeyToString(static_cast<QMediaMetaData::Key>(key)));
++ if (key == QMediaMetaData::CoverArtImage) {
++ m_metaDataFields[key] = new QLabel;
++ } else {
++ auto lineEdit = new QLineEdit;
++ lineEdit->setReadOnly(true);
++ m_metaDataFields[key] = lineEdit;
++ }
++ m_metaDataLabels[key]->setDisabled(true);
++ m_metaDataFields[key]->setDisabled(true);
++ metaDataLayout->addWidget(m_metaDataLabels[key], i, j);
++ metaDataLayout->addWidget(m_metaDataFields[key], i, j + 1);
++ key++;
++ if (key == QMediaMetaData::NumMetaData)
++ break;
++ }
++ }
++
++ layout->addLayout(metaDataLayout);
++ #endif
++
++ #if defined(Q_OS_QNX)
++ // On QNX, the main window doesn't have a title bar (or any other decorations).
++ // Create a status bar for the status information instead.
++ m_statusLabel = new QLabel;
++ m_statusBar = new QStatusBar;
++ m_statusBar->addPermanentWidget(m_statusLabel);
++ m_statusBar->setSizeGripEnabled(false); // Without mouse grabbing, it doesn't work very well.
++ layout->addWidget(m_statusBar);
++ #endif
++
++ if (!isPlayerAvailable()) {
++ QMessageBox::warning(this, tr("Service not available"),
++ tr("The QMediaPlayer object does not have a valid service.\n"
++ "Please check the media service plugins are installed."));
++
++ controls->setEnabled(false);
++ if (m_playlistView)
++ m_playlistView->setEnabled(false);
++ openButton->setEnabled(false);
++ m_fullScreenButton->setEnabled(false);
++ }
++
++ metaDataChanged();
++ }
++
++ bool Player::isPlayerAvailable() const
++ {
++ return m_player->isAvailable();
++ }
++
++ void Player::open()
++ {
++ QFileDialog fileDialog(this);
++ fileDialog.setAcceptMode(QFileDialog::AcceptOpen);
++ fileDialog.setWindowTitle(tr("Open Files"));
++ fileDialog.setDirectory(QStandardPaths::standardLocations(QStandardPaths::MoviesLocation)
++ .value(0, QDir::homePath()));
++ if (fileDialog.exec() == QDialog::Accepted)
++ addToPlaylist(fileDialog.selectedUrls());
++ }
++
++ static bool isPlaylist(const QUrl &url) // Check for ".m3u" playlists.
++ {
++ if (!url.isLocalFile())
++ return false;
++ const QFileInfo fileInfo(url.toLocalFile());
++ return fileInfo.exists()
++ && !fileInfo.suffix().compare(QLatin1String("m3u"), Qt::CaseInsensitive);
++ }
++
++ void Player::addToPlaylist(const QList<QUrl> &urls)
++ {
++ const int previousMediaCount = m_playlist->mediaCount();
++ for (auto &url : urls) {
++ if (isPlaylist(url))
++ m_playlist->load(url);
++ else
++ m_playlist->addMedia(url);
++ }
++ if (m_playlist->mediaCount() > previousMediaCount) {
++ auto index = m_playlistModel->index(previousMediaCount, 0);
++ if (m_playlistView)
++ m_playlistView->setCurrentIndex(index);
++ jump(index);
++ }
++ }
++
++ void Player::durationChanged(qint64 duration)
++ {
++ m_duration = duration / 1000;
++ m_slider->setMaximum(duration);
++ }
++
++ void Player::positionChanged(qint64 progress)
++ {
++ if (!m_slider->isSliderDown())
++ m_slider->setValue(progress);
++
++ updateDurationInfo(progress / 1000);
++ }
++
++ void Player::metaDataChanged()
++ {
++ auto metaData = m_player->metaData();
++ setTrackInfo(metaData.value(QMediaMetaData::AlbumArtist).toString() + " - "_L1
++ + metaData.value(QMediaMetaData::Title).toString());
++
++ #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
++ for (int i = 0; i < QMediaMetaData::NumMetaData; i++) {
++ if (QLineEdit *field = qobject_cast<QLineEdit *>(m_metaDataFields[i]))
++ field->clear();
++ else if (QLabel *label = qobject_cast<QLabel *>(m_metaDataFields[i]))
++ label->clear();
++ m_metaDataFields[i]->setDisabled(true);
++ m_metaDataLabels[i]->setDisabled(true);
++ }
++
++ for (auto &&[key, value] : metaData.asKeyValueRange()) {
++ int i = int(key);
++ if (key == QMediaMetaData::CoverArtImage) {
++ if (QLabel *cover = qobject_cast<QLabel *>(m_metaDataFields[key])) {
++ QImage coverImage = value.value<QImage>();
++ cover->setPixmap(QPixmap::fromImage(coverImage));
++ }
++ } else if (QLineEdit *field = qobject_cast<QLineEdit *>(m_metaDataFields[key])) {
++ QString stringValue = metaData.stringValue(key);
++ field->setText(stringValue);
++ }
++ m_metaDataFields[i]->setDisabled(false);
++ m_metaDataLabels[i]->setDisabled(false);
++ }
++
++ const QList<QMediaMetaData> tracks = m_player->videoTracks();
++ const int currentVideoTrack = m_player->activeVideoTrack();
++ if (currentVideoTrack >= 0 && currentVideoTrack < tracks.size()) {
++ const QMediaMetaData track = tracks.value(currentVideoTrack);
++
++ const QList<QMediaMetaData::Key> trackKeys = track.keys();
++ for (const QMediaMetaData::Key &key : std::as_const(trackKeys)) {
++ if (QLineEdit *field = qobject_cast<QLineEdit *>(m_metaDataFields[key])) {
++ QString stringValue = track.stringValue(key);
++ field->setText(stringValue);
++ }
++ m_metaDataFields[key]->setDisabled(true);
++ m_metaDataLabels[key]->setDisabled(true);
++ }
++ }
++ #endif
++ }
++
++ QString Player::trackName(const QMediaMetaData &metaData, int index)
++ {
++ QString name;
++ QString title = metaData.stringValue(QMediaMetaData::Title);
++ QLocale::Language lang = metaData.value(QMediaMetaData::Language).value<QLocale::Language>();
++
++ if (title.isEmpty()) {
++ if (lang == QLocale::Language::AnyLanguage)
++ name = tr("Track %1").arg(index + 1);
++ else
++ name = QLocale::languageToString(lang);
++ } else {
++ if (lang == QLocale::Language::AnyLanguage)
++ name = title;
++ else
++ name = title + " - ["_L1 + QLocale::languageToString(lang) + u']';
++ }
++ return name;
++ }
++
++ void Player::tracksChanged()
++ {
++ m_audioTracks->clear();
++ m_videoTracks->clear();
++ m_subtitleTracks->clear();
++
++ const auto audioTracks = m_player->audioTracks();
++ m_audioTracks->addItem(u"No audio"_s, -1);
++ for (int i = 0; i < audioTracks.size(); ++i)
++ m_audioTracks->addItem(trackName(audioTracks.at(i), i), i);
++ m_audioTracks->setCurrentIndex(m_player->activeAudioTrack() + 1);
++
++ const auto videoTracks = m_player->videoTracks();
++ m_videoTracks->addItem(u"No video"_s, -1);
++ for (int i = 0; i < videoTracks.size(); ++i)
++ m_videoTracks->addItem(trackName(videoTracks.at(i), i), i);
++ m_videoTracks->setCurrentIndex(m_player->activeVideoTrack() + 1);
++
++ m_subtitleTracks->addItem(u"No subtitles"_s, -1);
++ const auto subtitleTracks = m_player->subtitleTracks();
++ for (int i = 0; i < subtitleTracks.size(); ++i)
++ m_subtitleTracks->addItem(trackName(subtitleTracks.at(i), i), i);
++ m_subtitleTracks->setCurrentIndex(m_player->activeSubtitleTrack() + 1);
++ }
++
++ void Player::previousClicked()
++ {
++ // Go to previous track if we are within the first 5 seconds of playback
++ // Otherwise, seek to the beginning.
++ if (m_player->position() <= 5000) {
++ m_playlist->previous();
++ } else {
++ m_player->setPosition(0);
++ }
++ }
++
++ void Player::jump(const QModelIndex &index)
++ {
++ if (index.isValid()) {
++ m_playlist->setCurrentIndex(index.row());
++ }
++ }
++
++ void Player::playlistPositionChanged(int currentItem)
++ {
++ if (m_playlistView)
++ m_playlistView->setCurrentIndex(m_playlistModel->index(currentItem, 0));
++ m_player->setSource(m_playlist->currentMedia());
++ }
++
++ void Player::seek(int mseconds)
++ {
++ m_player->setPosition(mseconds);
++ }
++
++ void Player::statusChanged(QMediaPlayer::MediaStatus status)
++ {
++ handleCursor(status);
++
++ // handle status message
++ switch (status) {
++ case QMediaPlayer::NoMedia:
++ case QMediaPlayer::LoadedMedia:
++ setStatusInfo(QString());
++ break;
++ case QMediaPlayer::LoadingMedia:
++ setStatusInfo(tr("Loading..."));
++ break;
++ case QMediaPlayer::BufferingMedia:
++ case QMediaPlayer::BufferedMedia:
++ setStatusInfo(tr("Buffering %1%").arg(qRound(m_player->bufferProgress() * 100.)));
++ break;
++ case QMediaPlayer::StalledMedia:
++ setStatusInfo(tr("Stalled %1%").arg(qRound(m_player->bufferProgress() * 100.)));
++ break;
++ case QMediaPlayer::EndOfMedia:
++ QApplication::alert(this);
++ m_playlist->next();
++ break;
++ case QMediaPlayer::InvalidMedia:
++ displayErrorMessage();
++ break;
++ }
++ }
++
++ void Player::handleCursor(QMediaPlayer::MediaStatus status)
++ {
++ #ifndef QT_NO_CURSOR
++ if (status == QMediaPlayer::LoadingMedia || status == QMediaPlayer::BufferingMedia
++ || status == QMediaPlayer::StalledMedia)
++ setCursor(QCursor(Qt::BusyCursor));
++ else
++ unsetCursor();
++ #endif
++ }
++
++ void Player::bufferingProgress(float progress)
++ {
++ if (m_player->mediaStatus() == QMediaPlayer::StalledMedia)
++ setStatusInfo(tr("Stalled %1%").arg(qRound(progress * 100.)));
++ else
++ setStatusInfo(tr("Buffering %1%").arg(qRound(progress * 100.)));
++ }
++
++ void Player::videoAvailableChanged(bool available)
++ {
++ if (!available) {
++ disconnect(m_fullScreenButton, &QPushButton::clicked, m_videoWidget,
++ &QVideoWidget::setFullScreen);
++ disconnect(m_videoWidget, &QVideoWidget::fullScreenChanged, m_fullScreenButton,
++ &QPushButton::setChecked);
++ m_videoWidget->setFullScreen(false);
++ } else {
++ connect(m_fullScreenButton, &QPushButton::clicked, m_videoWidget,
++ &QVideoWidget::setFullScreen);
++ connect(m_videoWidget, &QVideoWidget::fullScreenChanged, m_fullScreenButton,
++ &QPushButton::setChecked);
++
++ if (m_fullScreenButton->isChecked())
++ m_videoWidget->setFullScreen(true);
++ }
++ }
++
++ void Player::selectAudioStream()
++ {
++ int stream = m_audioTracks->currentData().toInt();
++ m_player->setActiveAudioTrack(stream);
++ }
++
++ void Player::selectVideoStream()
++ {
++ int stream = m_videoTracks->currentData().toInt();
++ m_player->setActiveVideoTrack(stream);
++ }
++
++ void Player::selectSubtitleStream()
++ {
++ int stream = m_subtitleTracks->currentData().toInt();
++ m_player->setActiveSubtitleTrack(stream);
++ }
++
++ void Player::setTrackInfo(const QString &info)
++ {
++ m_trackInfo = info;
++
++ if (m_statusBar) {
++ m_statusBar->showMessage(m_trackInfo);
++ m_statusLabel->setText(m_statusInfo);
++ } else {
++ if (!m_statusInfo.isEmpty())
++ setWindowTitle(m_trackInfo + " | "_L1 + m_statusInfo);
++ else
++ setWindowTitle(m_trackInfo);
++ }
++ }
++
++ void Player::setStatusInfo(const QString &info)
++ {
++ m_statusInfo = info;
++
++ if (m_statusBar) {
++ m_statusBar->showMessage(m_trackInfo);
++ m_statusLabel->setText(m_statusInfo);
++ } else {
++ if (!m_statusInfo.isEmpty())
++ setWindowTitle(m_trackInfo + " | "_L1 + m_statusInfo);
++ else
++ setWindowTitle(m_trackInfo);
++ }
++ }
++
++ void Player::displayErrorMessage()
++ {
++ if (m_player->error() == QMediaPlayer::NoError)
++ return;
++ setStatusInfo(m_player->errorString());
++ }
++
++ void Player::updateDurationInfo(qint64 currentInfo)
++ {
++ QString tStr;
++ if (currentInfo || m_duration) {
++ QTime currentTime((currentInfo / 3600) % 60, (currentInfo / 60) % 60, currentInfo % 60,
++ (currentInfo * 1000) % 1000);
++ QTime totalTime((m_duration / 3600) % 60, (m_duration / 60) % 60, m_duration % 60,
++ (m_duration * 1000) % 1000);
++ QString format = m_duration > 3600 ? u"hh:mm:ss"_s : u"mm:ss"_s;
++ tStr = currentTime.toString(format) + " / "_L1 + totalTime.toString(format);
++ }
++ m_labelDuration->setText(tStr);
++ }
++
++ void Player::updateAudioDevices()
++ {
++ m_audioOutputCombo->clear();
++
++ m_audioOutputCombo->addItem(u"Default"_s, QVariant::fromValue(QAudioDevice()));
++ for (auto &deviceInfo : QMediaDevices::audioOutputs())
++ m_audioOutputCombo->addItem(deviceInfo.description(), QVariant::fromValue(deviceInfo));
++ }
++
++ void Player::audioOutputChanged(int index)
++ {
++ auto device = m_audioOutputCombo->itemData(index).value<QAudioDevice>();
++ m_player->audioOutput()->setDevice(device);
++ }
++
++ #include "moc_player.cpp"