diff options
Diffstat (limited to 'examples/multimedia/player')
18 files changed, 2760 insertions, 0 deletions
diff --git a/examples/multimedia/player/CMakeLists.txt b/examples/multimedia/player/CMakeLists.txt new file mode 100644 index 000000000..bd6631899 --- /dev/null +++ b/examples/multimedia/player/CMakeLists.txt @@ -0,0 +1,41 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +cmake_minimum_required(VERSION 3.16) +project(player LANGUAGES CXX) + +set(CMAKE_AUTOMOC ON) + +if(NOT DEFINED INSTALL_EXAMPLESDIR) + set(INSTALL_EXAMPLESDIR "examples") +endif() + +set(INSTALL_EXAMPLEDIR "${INSTALL_EXAMPLESDIR}/multimedia/player") + +find_package(Qt6 REQUIRED COMPONENTS MultimediaWidgets Network) + +qt_add_executable(player + main.cpp + player.cpp player.h + playercontrols.cpp playercontrols.h + playlistmodel.cpp playlistmodel.h + videowidget.cpp videowidget.h + qmediaplaylist.cpp qmediaplaylist.h qmediaplaylist_p.h + qplaylistfileparser.cpp qplaylistfileparser_p.h +) + +set_target_properties(player PROPERTIES + WIN32_EXECUTABLE TRUE + MACOSX_BUNDLE TRUE +) + +target_link_libraries(player PUBLIC + Qt::MultimediaWidgets + Qt::Network +) + +install(TARGETS player + RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}" + BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}" + LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}" +) diff --git a/examples/multimedia/player/doc/images/mediaplayerex.jpg b/examples/multimedia/player/doc/images/mediaplayerex.jpg Binary files differnew file mode 100644 index 000000000..e875bd134 --- /dev/null +++ b/examples/multimedia/player/doc/images/mediaplayerex.jpg diff --git a/examples/multimedia/player/doc/src/player.qdoc b/examples/multimedia/player/doc/src/player.qdoc new file mode 100644 index 000000000..d63b99ae3 --- /dev/null +++ b/examples/multimedia/player/doc/src/player.qdoc @@ -0,0 +1,48 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only + +/*! + \example player + \title Media Player Example + \ingroup multimedia_examples + \ingroup video_examples + \brief Playing audio and video. + \meta {tag} {widgets} + + \image mediaplayerex.jpg + + \e{Media Player} demonstrates a simple multimedia player that can play + audio and or video files using various codecs. + + \include examples-run.qdocinc + + The example uses a QMediaPlayer object passed into a QVideoWidget to + control the video output. To give the application playlist capability + we also use a QPlayList object. + + To activate the various functions such as play and stop on the dialog, + the button clicked events emit the play() and stop() signals, which + are connected to the play() and stop() slots of QMediaPlayer. + + \code + connect(controls, SIGNAL(play()), player, SLOT(play())); + connect(controls, SIGNAL(pause()), player, SLOT(pause())); + connect(controls, SIGNAL(stop()), player, SLOT(stop())); + \endcode + + We can get the volume (and set our user interface representation) + + \code + controls->setVolume(player->volume()); + \endcode + + and we can make widget 'volume' changes change the volume + + \code + connect(controls, SIGNAL(changeVolume(int)), player, SLOT(setVolume(int))); + \endcode + + The example also allows us to change video properties by means + of the QVideoWidget object. We can go to Full Screen mode with a single + button click, and back again. +*/ diff --git a/examples/multimedia/player/main.cpp b/examples/multimedia/player/main.cpp new file mode 100644 index 000000000..befe1d561 --- /dev/null +++ b/examples/multimedia/player/main.cpp @@ -0,0 +1,37 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "player.h" + +#include <QApplication> +#include <QCommandLineParser> +#include <QCommandLineOption> +#include <QDir> +#include <QUrl> + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + QCoreApplication::setApplicationName("Player Example"); + QCoreApplication::setOrganizationName("QtProject"); + QCoreApplication::setApplicationVersion(QT_VERSION_STR); + QCommandLineParser parser; + parser.setApplicationDescription("Qt MultiMedia Player Example"); + parser.addHelpOption(); + parser.addVersionOption(); + parser.addPositionalArgument("url", "The URL(s) to open."); + parser.process(app); + + Player player; + + if (!parser.positionalArguments().isEmpty() && player.isPlayerAvailable()) { + QList<QUrl> urls; + for (auto &a: parser.positionalArguments()) + urls.append(QUrl::fromUserInput(a, QDir::currentPath())); + player.addToPlaylist(urls); + } + + player.show(); + return app.exec(); +} diff --git a/examples/multimedia/player/player.cpp b/examples/multimedia/player/player.cpp new file mode 100644 index 000000000..22146d7e9 --- /dev/null +++ b/examples/multimedia/player/player.cpp @@ -0,0 +1,506 @@ +// 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 <QMediaMetaData> +#include <QMediaDevices> +#include <QAudioDevice> +#include <QAudioOutput> +#include <QMediaFormat> +#include <QtWidgets> + +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, QOverload<>::of(&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); + + // player layout + QBoxLayout *layout = new QVBoxLayout(this); + + // display + QBoxLayout *displayLayout = new QHBoxLayout; + displayLayout->addWidget(m_videoWidget, 2); +#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, &PlayerControls::setState); + 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); + +#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) + m_audioOutputCombo = new QComboBox(this); + m_audioOutputCombo->addItem(QString::fromUtf8("Default"), QVariant::fromValue(QAudioDevice())); + for (auto &deviceInfo: QMediaDevices::audioOutputs()) + m_audioOutputCombo->addItem(deviceInfo.description(), QVariant::fromValue(deviceInfo)); + connect(m_audioOutputCombo, QOverload<int>::of(&QComboBox::activated), this, + &Player::audioOutputChanged); + controlLayout->addWidget(m_audioOutputCombo); +#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::ThumbnailImage || key == QMediaMetaData::CoverArtImage) + m_metaDataFields[key] = new QLabel; + else + m_metaDataFields[key] = new QLineEdit; + 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 + + setLayout(layout); + + 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(QString("%1 - %2") + .arg(metaData.value(QMediaMetaData::AlbumArtist).toString()) + .arg(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 : metaData.keys()) { + int i = int(key); + if (key == QMediaMetaData::CoverArtImage) { + QVariant v = metaData.value(key); + if (QLabel *cover = qobject_cast<QLabel*>(m_metaDataFields[key])) { + QImage coverImage = v.value<QImage>(); + cover->setPixmap(QPixmap::fromImage(coverImage)); + } + } else if (key == QMediaMetaData::ThumbnailImage) { + QVariant v = metaData.value(key); + if (QLabel *thumbnail = qobject_cast<QLabel*>(m_metaDataFields[key])) { + QImage thumbnailImage = v.value<QImage>(); + thumbnail->setPixmap(QPixmap::fromImage(thumbnailImage)); + } + } 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); + } +#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 = QString("%1 - [%2]").arg(title).arg(QLocale::languageToString(lang)); + } + return name; +} + +void Player::tracksChanged() +{ + m_audioTracks->clear(); + m_videoTracks->clear(); + m_subtitleTracks->clear(); + + const auto audioTracks = m_player->audioTracks(); + m_audioTracks->addItem(QString::fromUtf8("No audio"), -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(QString::fromUtf8("No video"), -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(QString::fromUtf8("No subtitles"), -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(QString("%1 | %2").arg(m_trackInfo).arg(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(QString("%1 | %2").arg(m_trackInfo).arg(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 = "mm:ss"; + if (m_duration > 3600) + format = "hh:mm:ss"; + tStr = currentTime.toString(format) + " / " + totalTime.toString(format); + } + m_labelDuration->setText(tStr); +} + +void Player::audioOutputChanged(int index) +{ + auto device = m_audioOutputCombo->itemData(index).value<QAudioDevice>(); + m_player->audioOutput()->setDevice(device); +} diff --git a/examples/multimedia/player/player.h b/examples/multimedia/player/player.h new file mode 100644 index 000000000..1d328d307 --- /dev/null +++ b/examples/multimedia/player/player.h @@ -0,0 +1,99 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef PLAYER_H +#define PLAYER_H + +#include "qmediaplaylist.h" + +#include <QWidget> +#include <QMediaPlayer> +#include <QMediaMetaData> + +QT_BEGIN_NAMESPACE +class QAbstractItemView; +class QLabel; +class QMediaPlayer; +class QModelIndex; +class QPushButton; +class QComboBox; +class QSlider; +class QStatusBar; +class QVideoWidget; +QT_END_NAMESPACE + +class PlaylistModel; + +class Player : public QWidget +{ + Q_OBJECT + +public: + explicit Player(QWidget *parent = nullptr); + ~Player() = default; + + bool isPlayerAvailable() const; + + void addToPlaylist(const QList<QUrl> &urls); + +signals: + void fullScreenChanged(bool fullScreen); + +private slots: + void open(); + void durationChanged(qint64 duration); + void positionChanged(qint64 progress); + void metaDataChanged(); + void tracksChanged(); + + void previousClicked(); + + void seek(int mseconds); + void jump(const QModelIndex &index); + void playlistPositionChanged(int); + + void statusChanged(QMediaPlayer::MediaStatus status); + void bufferingProgress(float progress); + void videoAvailableChanged(bool available); + + void selectAudioStream(); + void selectVideoStream(); + void selectSubtitleStream(); + + void displayErrorMessage(); + + void audioOutputChanged(int); + +private: + void setTrackInfo(const QString &info); + void setStatusInfo(const QString &info); + void handleCursor(QMediaPlayer::MediaStatus status); + void updateDurationInfo(qint64 currentInfo); + QString trackName(const QMediaMetaData &metaData, int index); + + QMediaPlayer *m_player = nullptr; + QAudioOutput *m_audioOutput = nullptr; + QMediaPlaylist *m_playlist = nullptr; + QVideoWidget *m_videoWidget = nullptr; + QSlider *m_slider = nullptr; + QLabel *m_labelDuration = nullptr; + QPushButton *m_fullScreenButton = nullptr; + QComboBox *m_audioOutputCombo = nullptr; + QLabel *m_statusLabel = nullptr; + QStatusBar *m_statusBar = nullptr; + + QComboBox *m_audioTracks = nullptr; + QComboBox *m_videoTracks = nullptr; + QComboBox *m_subtitleTracks = nullptr; + + PlaylistModel *m_playlistModel = nullptr; + QAbstractItemView *m_playlistView = nullptr; + QString m_trackInfo; + QString m_statusInfo; + qint64 m_duration; + + QWidget *m_metaDataFields[QMediaMetaData::NumMetaData] = {}; + QLabel *m_metaDataLabels[QMediaMetaData::NumMetaData] = {}; +}; + +#endif // PLAYER_H diff --git a/examples/multimedia/player/player.pro b/examples/multimedia/player/player.pro new file mode 100644 index 000000000..703942441 --- /dev/null +++ b/examples/multimedia/player/player.pro @@ -0,0 +1,27 @@ +TEMPLATE = app +TARGET = player + +QT += network \ + multimedia \ + multimediawidgets \ + widgets + +HEADERS = \ + player.h \ + playercontrols.h \ + playlistmodel.h \ + videowidget.h \ + qmediaplaylist.h \ + qmediaplaylist_p.h \ + qplaylistfileparser_p.h + +SOURCES = main.cpp \ + player.cpp \ + playercontrols.cpp \ + playlistmodel.cpp \ + videowidget.cpp \ + qmediaplaylist.cpp \ + qplaylistfileparser.cpp + +target.path = $$[QT_INSTALL_EXAMPLES]/multimedia/player +INSTALLS += target diff --git a/examples/multimedia/player/playercontrols.cpp b/examples/multimedia/player/playercontrols.cpp new file mode 100644 index 000000000..0a6827326 --- /dev/null +++ b/examples/multimedia/player/playercontrols.cpp @@ -0,0 +1,172 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "playercontrols.h" + +#include <QBoxLayout> +#include <QSlider> +#include <QStyle> +#include <QToolButton> +#include <QComboBox> +#include <QAudio> + +PlayerControls::PlayerControls(QWidget *parent) + : QWidget(parent) +{ + m_playButton = new QToolButton(this); + m_playButton->setIcon(style()->standardIcon(QStyle::SP_MediaPlay)); + + connect(m_playButton, &QAbstractButton::clicked, this, &PlayerControls::playClicked); + + m_stopButton = new QToolButton(this); + m_stopButton->setIcon(style()->standardIcon(QStyle::SP_MediaStop)); + m_stopButton->setEnabled(false); + + connect(m_stopButton, &QAbstractButton::clicked, this, &PlayerControls::stop); + + m_nextButton = new QToolButton(this); + m_nextButton->setIcon(style()->standardIcon(QStyle::SP_MediaSkipForward)); + + connect(m_nextButton, &QAbstractButton::clicked, this, &PlayerControls::next); + + m_previousButton = new QToolButton(this); + m_previousButton->setIcon(style()->standardIcon(QStyle::SP_MediaSkipBackward)); + + connect(m_previousButton, &QAbstractButton::clicked, this, &PlayerControls::previous); + + m_muteButton = new QToolButton(this); + m_muteButton->setIcon(style()->standardIcon(QStyle::SP_MediaVolume)); + + connect(m_muteButton, &QAbstractButton::clicked, this, &PlayerControls::muteClicked); + + m_volumeSlider = new QSlider(Qt::Horizontal, this); + m_volumeSlider->setRange(0, 100); + + connect(m_volumeSlider, &QSlider::valueChanged, this, &PlayerControls::onVolumeSliderValueChanged); + + m_rateBox = new QComboBox(this); + m_rateBox->addItem("0.5x", QVariant(0.5)); + m_rateBox->addItem("1.0x", QVariant(1.0)); + m_rateBox->addItem("2.0x", QVariant(2.0)); + m_rateBox->setCurrentIndex(1); + + connect(m_rateBox, QOverload<int>::of(&QComboBox::activated), this, &PlayerControls::updateRate); + + QBoxLayout *layout = new QHBoxLayout; + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(m_stopButton); + layout->addWidget(m_previousButton); + layout->addWidget(m_playButton); + layout->addWidget(m_nextButton); + layout->addWidget(m_muteButton); + layout->addWidget(m_volumeSlider); + layout->addWidget(m_rateBox); + setLayout(layout); +} + +QMediaPlayer::PlaybackState PlayerControls::state() const +{ + return m_playerState; +} + +void PlayerControls::setState(QMediaPlayer::PlaybackState state) +{ + if (state != m_playerState) { + m_playerState = state; + + switch (state) { + case QMediaPlayer::StoppedState: + m_stopButton->setEnabled(false); + m_playButton->setIcon(style()->standardIcon(QStyle::SP_MediaPlay)); + break; + case QMediaPlayer::PlayingState: + m_stopButton->setEnabled(true); + m_playButton->setIcon(style()->standardIcon(QStyle::SP_MediaPause)); + break; + case QMediaPlayer::PausedState: + m_stopButton->setEnabled(true); + m_playButton->setIcon(style()->standardIcon(QStyle::SP_MediaPlay)); + break; + } + } +} + +float PlayerControls::volume() const +{ + qreal linearVolume = QAudio::convertVolume(m_volumeSlider->value() / qreal(100), + QAudio::LogarithmicVolumeScale, + QAudio::LinearVolumeScale); + + return linearVolume; +} + +void PlayerControls::setVolume(float volume) +{ + qreal logarithmicVolume = QAudio::convertVolume(volume, + QAudio::LinearVolumeScale, + QAudio::LogarithmicVolumeScale); + + m_volumeSlider->setValue(qRound(logarithmicVolume * 100)); +} + +bool PlayerControls::isMuted() const +{ + return m_playerMuted; +} + +void PlayerControls::setMuted(bool muted) +{ + if (muted != m_playerMuted) { + m_playerMuted = muted; + + m_muteButton->setIcon(style()->standardIcon(muted + ? QStyle::SP_MediaVolumeMuted + : QStyle::SP_MediaVolume)); + } +} + +void PlayerControls::playClicked() +{ + switch (m_playerState) { + case QMediaPlayer::StoppedState: + case QMediaPlayer::PausedState: + emit play(); + break; + case QMediaPlayer::PlayingState: + emit pause(); + break; + } +} + +void PlayerControls::muteClicked() +{ + emit changeMuting(!m_playerMuted); +} + +qreal PlayerControls::playbackRate() const +{ + return m_rateBox->itemData(m_rateBox->currentIndex()).toDouble(); +} + +void PlayerControls::setPlaybackRate(float rate) +{ + for (int i = 0; i < m_rateBox->count(); ++i) { + if (qFuzzyCompare(rate, float(m_rateBox->itemData(i).toDouble()))) { + m_rateBox->setCurrentIndex(i); + return; + } + } + + m_rateBox->addItem(QString("%1x").arg(rate), QVariant(rate)); + m_rateBox->setCurrentIndex(m_rateBox->count() - 1); +} + +void PlayerControls::updateRate() +{ + emit changeRate(playbackRate()); +} + +void PlayerControls::onVolumeSliderValueChanged() +{ + emit changeVolume(volume()); +} diff --git a/examples/multimedia/player/playercontrols.h b/examples/multimedia/player/playercontrols.h new file mode 100644 index 000000000..72dddd68f --- /dev/null +++ b/examples/multimedia/player/playercontrols.h @@ -0,0 +1,62 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef PLAYERCONTROLS_H +#define PLAYERCONTROLS_H + +#include <QMediaPlayer> +#include <QWidget> + +QT_BEGIN_NAMESPACE +class QAbstractButton; +class QAbstractSlider; +class QComboBox; +QT_END_NAMESPACE + +class PlayerControls : public QWidget +{ + Q_OBJECT + +public: + explicit PlayerControls(QWidget *parent = nullptr); + + QMediaPlayer::PlaybackState state() const; + float volume() const; + bool isMuted() const; + qreal playbackRate() const; + +public slots: + void setState(QMediaPlayer::PlaybackState state); + void setVolume(float volume); + void setMuted(bool muted); + void setPlaybackRate(float rate); + +signals: + void play(); + void pause(); + void stop(); + void next(); + void previous(); + void changeVolume(float volume); + void changeMuting(bool muting); + void changeRate(qreal rate); + +private slots: + void playClicked(); + void muteClicked(); + void updateRate(); + void onVolumeSliderValueChanged(); + +private: + QMediaPlayer::PlaybackState m_playerState = QMediaPlayer::StoppedState; + bool m_playerMuted = false; + QAbstractButton *m_playButton = nullptr; + QAbstractButton *m_stopButton = nullptr; + QAbstractButton *m_nextButton = nullptr; + QAbstractButton *m_previousButton = nullptr; + QAbstractButton *m_muteButton = nullptr; + QAbstractSlider *m_volumeSlider = nullptr; + QComboBox *m_rateBox = nullptr; +}; + +#endif // PLAYERCONTROLS_H diff --git a/examples/multimedia/player/playlistmodel.cpp b/examples/multimedia/player/playlistmodel.cpp new file mode 100644 index 000000000..871aed0b8 --- /dev/null +++ b/examples/multimedia/player/playlistmodel.cpp @@ -0,0 +1,102 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "playlistmodel.h" +#include "qmediaplaylist.h" + +#include <QFileInfo> +#include <QUrl> + +PlaylistModel::PlaylistModel(QObject *parent) + : QAbstractItemModel(parent) +{ + m_playlist.reset(new QMediaPlaylist); + connect(m_playlist.data(), &QMediaPlaylist::mediaAboutToBeInserted, this, &PlaylistModel::beginInsertItems); + connect(m_playlist.data(), &QMediaPlaylist::mediaInserted, this, &PlaylistModel::endInsertItems); + connect(m_playlist.data(), &QMediaPlaylist::mediaAboutToBeRemoved, this, &PlaylistModel::beginRemoveItems); + connect(m_playlist.data(), &QMediaPlaylist::mediaRemoved, this, &PlaylistModel::endRemoveItems); + connect(m_playlist.data(), &QMediaPlaylist::mediaChanged, this, &PlaylistModel::changeItems); +} + +PlaylistModel::~PlaylistModel() = default; + +int PlaylistModel::rowCount(const QModelIndex &parent) const +{ + return m_playlist && !parent.isValid() ? m_playlist->mediaCount() : 0; +} + +int PlaylistModel::columnCount(const QModelIndex &parent) const +{ + return !parent.isValid() ? ColumnCount : 0; +} + +QModelIndex PlaylistModel::index(int row, int column, const QModelIndex &parent) const +{ + return m_playlist && !parent.isValid() + && row >= 0 && row < m_playlist->mediaCount() + && column >= 0 && column < ColumnCount + ? createIndex(row, column) + : QModelIndex(); +} + +QModelIndex PlaylistModel::parent(const QModelIndex &child) const +{ + Q_UNUSED(child); + + return QModelIndex(); +} + +QVariant PlaylistModel::data(const QModelIndex &index, int role) const +{ + if (index.isValid() && role == Qt::DisplayRole) { + QVariant value = m_data[index]; + if (!value.isValid() && index.column() == Title) { + QUrl location = m_playlist->media(index.row()); + return QFileInfo(location.path()).fileName(); + } + + return value; + } + return QVariant(); +} + +QMediaPlaylist *PlaylistModel::playlist() const +{ + return m_playlist.data(); +} + +bool PlaylistModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + Q_UNUSED(role); + m_data[index] = value; + emit dataChanged(index, index); + return true; +} + +void PlaylistModel::beginInsertItems(int start, int end) +{ + m_data.clear(); + beginInsertRows(QModelIndex(), start, end); +} + +void PlaylistModel::endInsertItems() +{ + endInsertRows(); +} + +void PlaylistModel::beginRemoveItems(int start, int end) +{ + m_data.clear(); + beginRemoveRows(QModelIndex(), start, end); +} + +void PlaylistModel::endRemoveItems() +{ + endInsertRows(); +} + +void PlaylistModel::changeItems(int start, int end) +{ + m_data.clear(); + emit dataChanged(index(start,0), index(end,ColumnCount)); +} diff --git a/examples/multimedia/player/playlistmodel.h b/examples/multimedia/player/playlistmodel.h new file mode 100644 index 000000000..6c20cc1d6 --- /dev/null +++ b/examples/multimedia/player/playlistmodel.h @@ -0,0 +1,52 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef PLAYLISTMODEL_H +#define PLAYLISTMODEL_H + +#include <QAbstractItemModel> +#include <QScopedPointer> + +QT_BEGIN_NAMESPACE +class QMediaPlaylist; +QT_END_NAMESPACE + +class PlaylistModel : public QAbstractItemModel +{ + Q_OBJECT + +public: + enum Column + { + Title = 0, + ColumnCount + }; + + explicit PlaylistModel(QObject *parent = nullptr); + ~PlaylistModel(); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &child) const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + QMediaPlaylist *playlist() const; + + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::DisplayRole) override; + +private slots: + void beginInsertItems(int start, int end); + void endInsertItems(); + void beginRemoveItems(int start, int end); + void endRemoveItems(); + void changeItems(int start, int end); + +private: + QScopedPointer<QMediaPlaylist> m_playlist; + QMap<QModelIndex, QVariant> m_data; +}; + +#endif // PLAYLISTMODEL_H diff --git a/examples/multimedia/player/qmediaplaylist.cpp b/examples/multimedia/player/qmediaplaylist.cpp new file mode 100644 index 000000000..529720808 --- /dev/null +++ b/examples/multimedia/player/qmediaplaylist.cpp @@ -0,0 +1,653 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qmediaplaylist.h" +#include "qmediaplaylist_p.h" +#include "qplaylistfileparser_p.h" + +#include <QtCore/qlist.h> +#include <QtCore/qfile.h> +#include <QtCore/qurl.h> +#include <QtCore/qcoreevent.h> +#include <QtCore/qcoreapplication.h> +#include <QRandomGenerator> + +QT_BEGIN_NAMESPACE + +class QM3uPlaylistWriter +{ +public: + QM3uPlaylistWriter(QIODevice *device) + :m_device(device), m_textStream(new QTextStream(m_device)) + { + } + + ~QM3uPlaylistWriter() + { + delete m_textStream; + } + + bool writeItem(const QUrl& item) + { + *m_textStream << item.toString() << Qt::endl; + return true; + } + +private: + QIODevice *m_device; + QTextStream *m_textStream; +}; + + +int QMediaPlaylistPrivate::nextPosition(int steps) const +{ + if (playlist.count() == 0) + return -1; + + int next = currentPos + steps; + + switch (playbackMode) { + case QMediaPlaylist::CurrentItemOnce: + return steps != 0 ? -1 : currentPos; + case QMediaPlaylist::CurrentItemInLoop: + return currentPos; + case QMediaPlaylist::Sequential: + if (next >= playlist.size()) + next = -1; + break; + case QMediaPlaylist::Loop: + next %= playlist.count(); + break; + } + + return next; +} + +int QMediaPlaylistPrivate::prevPosition(int steps) const +{ + if (playlist.count() == 0) + return -1; + + int next = currentPos; + if (next < 0) + next = playlist.size(); + next -= steps; + + switch (playbackMode) { + case QMediaPlaylist::CurrentItemOnce: + return steps != 0 ? -1 : currentPos; + case QMediaPlaylist::CurrentItemInLoop: + return currentPos; + case QMediaPlaylist::Sequential: + if (next < 0) + next = -1; + break; + case QMediaPlaylist::Loop: + next %= playlist.size(); + if (next < 0) + next += playlist.size(); + break; + } + + return next; +} + +/*! + \class QMediaPlaylist + \inmodule QtMultimedia + \ingroup multimedia + \ingroup multimedia_playback + + + \brief The QMediaPlaylist class provides a list of media content to play. + + QMediaPlaylist is intended to be used with other media objects, + like QMediaPlayer. + + QMediaPlaylist allows to access the service intrinsic playlist functionality + if available, otherwise it provides the local memory playlist implementation. + + \snippet multimedia-snippets/media.cpp Movie playlist + + Depending on playlist source implementation, most of the playlist mutating + operations can be asynchronous. + + QMediaPlayList currently supports M3U playlists (file extension .m3u and .m3u8). + + \sa QUrl +*/ + + +/*! + \enum QMediaPlaylist::PlaybackMode + + The QMediaPlaylist::PlaybackMode describes the order items in playlist are played. + + \value CurrentItemOnce The current item is played only once. + + \value CurrentItemInLoop The current item is played repeatedly in a loop. + + \value Sequential Playback starts from the current and moves through each successive item until the last is reached and then stops. + The next item is a null item when the last one is currently playing. + + \value Loop Playback restarts at the first item after the last has finished playing. + + \value Random Play items in random order. +*/ + + + +/*! + Create a new playlist object with the given \a parent. +*/ + +QMediaPlaylist::QMediaPlaylist(QObject *parent) + : QObject(parent) + , d_ptr(new QMediaPlaylistPrivate) +{ + Q_D(QMediaPlaylist); + + d->q_ptr = this; +} + +/*! + Destroys the playlist. + */ + +QMediaPlaylist::~QMediaPlaylist() +{ + delete d_ptr; +} + +/*! + \property QMediaPlaylist::playbackMode + + This property defines the order that items in the playlist are played. + + \sa QMediaPlaylist::PlaybackMode +*/ + +QMediaPlaylist::PlaybackMode QMediaPlaylist::playbackMode() const +{ + return d_func()->playbackMode; +} + +void QMediaPlaylist::setPlaybackMode(QMediaPlaylist::PlaybackMode mode) +{ + Q_D(QMediaPlaylist); + + if (mode == d->playbackMode) + return; + + d->playbackMode = mode; + + emit playbackModeChanged(mode); +} + +/*! + Returns position of the current media content in the playlist. +*/ +int QMediaPlaylist::currentIndex() const +{ + return d_func()->currentPos; +} + +/*! + Returns the current media content. +*/ + +QUrl QMediaPlaylist::currentMedia() const +{ + Q_D(const QMediaPlaylist); + if (d->currentPos < 0 || d->currentPos >= d->playlist.size()) + return QUrl(); + return d_func()->playlist.at(d_func()->currentPos); +} + +/*! + Returns the index of the item, which would be current after calling next() + \a steps times. + + Returned value depends on the size of playlist, current position + and playback mode. + + \sa QMediaPlaylist::playbackMode(), previousIndex() +*/ +int QMediaPlaylist::nextIndex(int steps) const +{ + return d_func()->nextPosition(steps); +} + +/*! + Returns the index of the item, which would be current after calling previous() + \a steps times. + + \sa QMediaPlaylist::playbackMode(), nextIndex() +*/ + +int QMediaPlaylist::previousIndex(int steps) const +{ + return d_func()->prevPosition(steps); +} + + +/*! + Returns the number of items in the playlist. + + \sa isEmpty() + */ +int QMediaPlaylist::mediaCount() const +{ + return d_func()->playlist.count(); +} + +/*! + Returns true if the playlist contains no items, otherwise returns false. + + \sa mediaCount() + */ +bool QMediaPlaylist::isEmpty() const +{ + return mediaCount() == 0; +} + +/*! + Returns the media content at \a index in the playlist. +*/ + +QUrl QMediaPlaylist::media(int index) const +{ + Q_D(const QMediaPlaylist); + if (index < 0 || index >= d->playlist.size()) + return QUrl(); + return d->playlist.at(index); +} + +/*! + Append the media \a content to the playlist. + + Returns true if the operation is successful, otherwise returns false. + */ +void QMediaPlaylist::addMedia(const QUrl &content) +{ + Q_D(QMediaPlaylist); + int pos = d->playlist.size(); + emit mediaAboutToBeInserted(pos, pos); + d->playlist.append(content); + emit mediaInserted(pos, pos); +} + +/*! + Append multiple media content \a items to the playlist. + + Returns true if the operation is successful, otherwise returns false. + */ +void QMediaPlaylist::addMedia(const QList<QUrl> &items) +{ + if (!items.size()) + return; + + Q_D(QMediaPlaylist); + int first = d->playlist.size(); + int last = first + items.size() - 1; + emit mediaAboutToBeInserted(first, last); + d_func()->playlist.append(items); + emit mediaInserted(first, last); +} + +/*! + Insert the media \a content to the playlist at position \a pos. + + Returns true if the operation is successful, otherwise returns false. +*/ + +bool QMediaPlaylist::insertMedia(int pos, const QUrl &content) +{ + Q_D(QMediaPlaylist); + pos = qBound(0, pos, d->playlist.size()); + emit mediaAboutToBeInserted(pos, pos); + d->playlist.insert(pos, content); + emit mediaInserted(pos, pos); + return true; +} + +/*! + Insert multiple media content \a items to the playlist at position \a pos. + + Returns true if the operation is successful, otherwise returns false. +*/ + +bool QMediaPlaylist::insertMedia(int pos, const QList<QUrl> &items) +{ + if (!items.size()) + return true; + + Q_D(QMediaPlaylist); + pos = qBound(0, pos, d->playlist.size()); + int last = pos + items.size() - 1; + emit mediaAboutToBeInserted(pos, last); + auto newList = d->playlist.mid(0, pos); + newList += items; + newList += d->playlist.mid(pos); + d->playlist = newList; + emit mediaInserted(pos, last); + return true; +} + +/*! + Move the item from position \a from to position \a to. + + Returns true if the operation is successful, otherwise false. + + \since 5.7 +*/ +bool QMediaPlaylist::moveMedia(int from, int to) +{ + Q_D(QMediaPlaylist); + if (from < 0 || from > d->playlist.count() || + to < 0 || to > d->playlist.count()) + return false; + + d->playlist.move(from, to); + emit mediaChanged(from, to); + return true; +} + +/*! + Remove the item from the playlist at position \a pos. + + Returns true if the operation is successful, otherwise return false. + */ +bool QMediaPlaylist::removeMedia(int pos) +{ + return removeMedia(pos, pos); +} + +/*! + Remove items in the playlist from \a start to \a end inclusive. + + Returns true if the operation is successful, otherwise return false. + */ +bool QMediaPlaylist::removeMedia(int start, int end) +{ + Q_D(QMediaPlaylist); + if (end < start || end < 0 || start >= d->playlist.count()) + return false; + start = qBound(0, start, d->playlist.size() - 1); + end = qBound(0, end, d->playlist.size() - 1); + + emit mediaAboutToBeRemoved(start, end); + d->playlist.remove(start, end - start + 1); + emit mediaRemoved(start, end); + return true; +} + +/*! + Remove all the items from the playlist. + + Returns true if the operation is successful, otherwise return false. + */ +void QMediaPlaylist::clear() +{ + Q_D(QMediaPlaylist); + int size = d->playlist.size(); + emit mediaAboutToBeRemoved(0, size - 1); + d->playlist.clear(); + emit mediaRemoved(0, size - 1); +} + +/*! + Load playlist from \a location. If \a format is specified, it is used, + otherwise format is guessed from location name and data. + + New items are appended to playlist. + + QMediaPlaylist::loaded() signal is emitted if playlist was loaded successfully, + otherwise the playlist emits loadFailed(). +*/ + +void QMediaPlaylist::load(const QUrl &location, const char *format) +{ + Q_D(QMediaPlaylist); + + d->error = NoError; + d->errorString.clear(); + + d->ensureParser(); + d->parser->start(location, QString::fromUtf8(format)); +} + +/*! + Load playlist from QIODevice \a device. If \a format is specified, it is used, + otherwise format is guessed from device data. + + New items are appended to playlist. + + QMediaPlaylist::loaded() signal is emitted if playlist was loaded successfully, + otherwise the playlist emits loadFailed(). +*/ +void QMediaPlaylist::load(QIODevice *device, const char *format) +{ + Q_D(QMediaPlaylist); + + d->error = NoError; + d->errorString.clear(); + + d->ensureParser(); + d->parser->start(device, QString::fromUtf8(format)); +} + +/*! + Save playlist to \a location. If \a format is specified, it is used, + otherwise format is guessed from location name. + + Returns true if playlist was saved successfully, otherwise returns false. + */ +bool QMediaPlaylist::save(const QUrl &location, const char *format) const +{ + Q_D(const QMediaPlaylist); + + d->error = NoError; + d->errorString.clear(); + + if (!d->checkFormat(format)) + return false; + + QFile file(location.toLocalFile()); + + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + d->error = AccessDeniedError; + d->errorString = tr("The file could not be accessed."); + return false; + } + + return save(&file, format); +} + +/*! + Save playlist to QIODevice \a device using format \a format. + + Returns true if playlist was saved successfully, otherwise returns false. +*/ +bool QMediaPlaylist::save(QIODevice *device, const char *format) const +{ + Q_D(const QMediaPlaylist); + + d->error = NoError; + d->errorString.clear(); + + if (!d->checkFormat(format)) + return false; + + QM3uPlaylistWriter writer(device); + for (const auto &entry : d->playlist) + writer.writeItem(entry); + return true; +} + +/*! + Returns the last error condition. +*/ +QMediaPlaylist::Error QMediaPlaylist::error() const +{ + return d_func()->error; +} + +/*! + Returns the string describing the last error condition. +*/ +QString QMediaPlaylist::errorString() const +{ + return d_func()->errorString; +} + +/*! + Shuffle items in the playlist. +*/ +void QMediaPlaylist::shuffle() +{ + Q_D(QMediaPlaylist); + QList<QUrl> playlist; + + // keep the current item when shuffling + QUrl current; + if (d->currentPos != -1) + current = d->playlist.takeAt(d->currentPos); + + while (!d->playlist.isEmpty()) + playlist.append(d->playlist.takeAt(QRandomGenerator::global()->bounded(int(d->playlist.size())))); + + if (d->currentPos != -1) + playlist.insert(d->currentPos, current); + d->playlist = playlist; + emit mediaChanged(0, d->playlist.count()); +} + + +/*! + Advance to the next media content in playlist. +*/ +void QMediaPlaylist::next() +{ + Q_D(QMediaPlaylist); + d->currentPos = d->nextPosition(1); + + emit currentIndexChanged(d->currentPos); + emit currentMediaChanged(currentMedia()); +} + +/*! + Return to the previous media content in playlist. +*/ +void QMediaPlaylist::previous() +{ + Q_D(QMediaPlaylist); + d->currentPos = d->prevPosition(1); + + emit currentIndexChanged(d->currentPos); + emit currentMediaChanged(currentMedia()); +} + +/*! + Activate media content from playlist at position \a playlistPosition. +*/ + +void QMediaPlaylist::setCurrentIndex(int playlistPosition) +{ + Q_D(QMediaPlaylist); + if (playlistPosition < 0 || playlistPosition >= d->playlist.size()) + playlistPosition = -1; + d->currentPos = playlistPosition; + + emit currentIndexChanged(d->currentPos); + emit currentMediaChanged(currentMedia()); +} + +/*! + \fn void QMediaPlaylist::mediaInserted(int start, int end) + + This signal is emitted after media has been inserted into the playlist. + The new items are those between \a start and \a end inclusive. + */ + +/*! + \fn void QMediaPlaylist::mediaRemoved(int start, int end) + + This signal is emitted after media has been removed from the playlist. + The removed items are those between \a start and \a end inclusive. + */ + +/*! + \fn void QMediaPlaylist::mediaChanged(int start, int end) + + This signal is emitted after media has been changed in the playlist + between \a start and \a end positions inclusive. + */ + +/*! + \fn void QMediaPlaylist::currentIndexChanged(int position) + + Signal emitted when playlist position changed to \a position. +*/ + +/*! + \fn void QMediaPlaylist::playbackModeChanged(QMediaPlaylist::PlaybackMode mode) + + Signal emitted when playback mode changed to \a mode. +*/ + +/*! + \fn void QMediaPlaylist::mediaAboutToBeInserted(int start, int end) + + Signal emitted when items are to be inserted at \a start and ending at \a end. +*/ + +/*! + \fn void QMediaPlaylist::mediaAboutToBeRemoved(int start, int end) + + Signal emitted when item are to be deleted at \a start and ending at \a end. +*/ + +/*! + \fn void QMediaPlaylist::currentMediaChanged(const QUrl &content) + + Signal emitted when current media changes to \a content. +*/ + +/*! + \property QMediaPlaylist::currentIndex + \brief Current position. +*/ + +/*! + \property QMediaPlaylist::currentMedia + \brief Current media content. +*/ + +/*! + \fn QMediaPlaylist::loaded() + + Signal emitted when playlist finished loading. +*/ + +/*! + \fn QMediaPlaylist::loadFailed() + + Signal emitted if failed to load playlist. +*/ + +/*! + \enum QMediaPlaylist::Error + + This enum describes the QMediaPlaylist error codes. + + \value NoError No errors. + \value FormatError Format error. + \value FormatNotSupportedError Format not supported. + \value NetworkError Network error. + \value AccessDeniedError Access denied error. +*/ + +QT_END_NAMESPACE + +#include "moc_qmediaplaylist.cpp" diff --git a/examples/multimedia/player/qmediaplaylist.h b/examples/multimedia/player/qmediaplaylist.h new file mode 100644 index 000000000..94846d9b7 --- /dev/null +++ b/examples/multimedia/player/qmediaplaylist.h @@ -0,0 +1,96 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QMEDIAPLAYLIST_H +#define QMEDIAPLAYLIST_H + +#include <QtCore/qobject.h> + +#include <QtMultimedia/qtmultimediaglobal.h> +#include <QtMultimedia/qmediaenumdebug.h> + + +QT_BEGIN_NAMESPACE + +class QMediaPlaylistPrivate; +class QMediaPlaylist : public QObject +{ + Q_OBJECT + Q_PROPERTY(QMediaPlaylist::PlaybackMode playbackMode READ playbackMode WRITE setPlaybackMode NOTIFY playbackModeChanged) + Q_PROPERTY(QUrl currentMedia READ currentMedia NOTIFY currentMediaChanged) + Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) + +public: + enum PlaybackMode { CurrentItemOnce, CurrentItemInLoop, Sequential, Loop }; + Q_ENUM(PlaybackMode) + enum Error { NoError, FormatError, FormatNotSupportedError, NetworkError, AccessDeniedError }; + Q_ENUM(Error) + + explicit QMediaPlaylist(QObject *parent = nullptr); + virtual ~QMediaPlaylist(); + + PlaybackMode playbackMode() const; + void setPlaybackMode(PlaybackMode mode); + + int currentIndex() const; + QUrl currentMedia() const; + + int nextIndex(int steps = 1) const; + int previousIndex(int steps = 1) const; + + QUrl media(int index) const; + + int mediaCount() const; + bool isEmpty() const; + + void addMedia(const QUrl &content); + void addMedia(const QList<QUrl> &items); + bool insertMedia(int index, const QUrl &content); + bool insertMedia(int index, const QList<QUrl> &items); + bool moveMedia(int from, int to); + bool removeMedia(int pos); + bool removeMedia(int start, int end); + void clear(); + + void load(const QUrl &location, const char *format = nullptr); + void load(QIODevice *device, const char *format = nullptr); + + bool save(const QUrl &location, const char *format = nullptr) const; + bool save(QIODevice *device, const char *format) const; + + Error error() const; + QString errorString() const; + +public Q_SLOTS: + void shuffle(); + + void next(); + void previous(); + + void setCurrentIndex(int index); + +Q_SIGNALS: + void currentIndexChanged(int index); + void playbackModeChanged(QMediaPlaylist::PlaybackMode mode); + void currentMediaChanged(const QUrl&); + + void mediaAboutToBeInserted(int start, int end); + void mediaInserted(int start, int end); + void mediaAboutToBeRemoved(int start, int end); + void mediaRemoved(int start, int end); + void mediaChanged(int start, int end); + + void loaded(); + void loadFailed(); + +private: + QMediaPlaylistPrivate *d_ptr; + Q_DECLARE_PRIVATE(QMediaPlaylist) +}; + +QT_END_NAMESPACE + +Q_MEDIA_ENUM_DEBUG(QMediaPlaylist, PlaybackMode) +Q_MEDIA_ENUM_DEBUG(QMediaPlaylist, Error) + +#endif // QMEDIAPLAYLIST_H diff --git a/examples/multimedia/player/qmediaplaylist_p.h b/examples/multimedia/player/qmediaplaylist_p.h new file mode 100644 index 000000000..b0a6609c7 --- /dev/null +++ b/examples/multimedia/player/qmediaplaylist_p.h @@ -0,0 +1,112 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QMEDIAPLAYLIST_P_H +#define QMEDIAPLAYLIST_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qmediaplaylist.h" +#include "qplaylistfileparser_p.h" + +#include <QtCore/qdebug.h> + +#ifdef Q_MOC_RUN +# pragma Q_MOC_EXPAND_MACROS +#endif + +QT_BEGIN_NAMESPACE + + +class QMediaPlaylistControl; + +class QMediaPlaylistPrivate +{ + Q_DECLARE_PUBLIC(QMediaPlaylist) +public: + QMediaPlaylistPrivate() + : error(QMediaPlaylist::NoError) + { + } + + virtual ~QMediaPlaylistPrivate() + { + if (parser) + delete parser; + } + + void loadFailed(QMediaPlaylist::Error error, const QString &errorString) + { + this->error = error; + this->errorString = errorString; + + emit q_ptr->loadFailed(); + } + + void loadFinished() + { + q_ptr->addMedia(parser->playlist); + + emit q_ptr->loaded(); + } + + bool checkFormat(const char *format) const + { + QLatin1String f(format); + QPlaylistFileParser::FileType type = format ? QPlaylistFileParser::UNKNOWN : QPlaylistFileParser::M3U8; + if (format) { + if (f == QLatin1String("m3u") || f == QLatin1String("text/uri-list") || + f == QLatin1String("audio/x-mpegurl") || f == QLatin1String("audio/mpegurl")) + type = QPlaylistFileParser::M3U; + else if (f == QLatin1String("m3u8") || f == QLatin1String("application/x-mpegURL") || + f == QLatin1String("application/vnd.apple.mpegurl")) + type = QPlaylistFileParser::M3U8; + } + + if (type == QPlaylistFileParser::UNKNOWN || type == QPlaylistFileParser::PLS) { + error = QMediaPlaylist::FormatNotSupportedError; + errorString = QMediaPlaylist::tr("This file format is not supported."); + return false; + } + return true; + } + + void ensureParser() + { + if (parser) + return; + + parser = new QPlaylistFileParser(q_ptr); + QObject::connect(parser, &QPlaylistFileParser::finished, [this]() { loadFinished(); }); + QObject::connect(parser, &QPlaylistFileParser::error, + [this](QMediaPlaylist::Error err, const QString& errorMsg) { loadFailed(err, errorMsg); }); + } + + int nextPosition(int steps) const; + int prevPosition(int steps) const; + + QList<QUrl> playlist; + + int currentPos = -1; + QMediaPlaylist::PlaybackMode playbackMode = QMediaPlaylist::Sequential; + + QPlaylistFileParser *parser = nullptr; + mutable QMediaPlaylist::Error error; + mutable QString errorString; + + QMediaPlaylist *q_ptr; +}; + +QT_END_NAMESPACE + + +#endif // QMEDIAPLAYLIST_P_H diff --git a/examples/multimedia/player/qplaylistfileparser.cpp b/examples/multimedia/player/qplaylistfileparser.cpp new file mode 100644 index 000000000..698f81ddc --- /dev/null +++ b/examples/multimedia/player/qplaylistfileparser.cpp @@ -0,0 +1,605 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qplaylistfileparser_p.h" +#include <qfileinfo.h> +#include <QtCore/QDebug> +#include <QtCore/qiodevice.h> +#include <QtCore/qpointer.h> +#include <QtNetwork/QNetworkReply> +#include <QtNetwork/QNetworkRequest> +#include "qmediaplayer.h" +#include "qmediametadata.h" + +QT_BEGIN_NAMESPACE + +namespace { + +class ParserBase +{ +public: + explicit ParserBase(QPlaylistFileParser *parent) + : m_parent(parent) + , m_aborted(false) + { + Q_ASSERT(m_parent); + } + + bool parseLine(int lineIndex, const QString& line, const QUrl& root) + { + if (m_aborted) + return false; + + const bool ok = parseLineImpl(lineIndex, line, root); + return ok && !m_aborted; + } + + virtual void abort() { m_aborted = true; } + virtual ~ParserBase() = default; + +protected: + virtual bool parseLineImpl(int lineIndex, const QString& line, const QUrl& root) = 0; + + static QUrl expandToFullPath(const QUrl &root, const QString &line) + { + // On Linux, backslashes are not converted to forward slashes :/ + if (line.startsWith(QLatin1String("//")) || line.startsWith(QLatin1String("\\\\"))) { + // Network share paths are not resolved + return QUrl::fromLocalFile(line); + } + + QUrl url(line); + if (url.scheme().isEmpty()) { + // Resolve it relative to root + if (root.isLocalFile()) + return QUrl::fromUserInput(line, root.adjusted(QUrl::RemoveFilename).toLocalFile(), QUrl::AssumeLocalFile); + return root.resolved(url); + } + if (url.scheme().length() == 1) + // Assume it's a drive letter for a Windows path + url = QUrl::fromLocalFile(line); + + return url; + } + + void newItemFound(const QVariant& content) { Q_EMIT m_parent->newItem(content); } + + + QPlaylistFileParser *m_parent; + bool m_aborted; +}; + +class M3UParser : public ParserBase +{ +public: + explicit M3UParser(QPlaylistFileParser *q) + : ParserBase(q) + , m_extendedFormat(false) + { + } + + /* + * + Extended M3U directives + + #EXTM3U - header - must be first line of file + #EXTINF - extra info - length (seconds), title + #EXTINF - extra info - length (seconds), artist '-' title + + Example + + #EXTM3U + #EXTINF:123, Sample artist - Sample title + C:\Documents and Settings\I\My Music\Sample.mp3 + #EXTINF:321,Example Artist - Example title + C:\Documents and Settings\I\My Music\Greatest Hits\Example.ogg + + */ + bool parseLineImpl(int lineIndex, const QString& line, const QUrl& root) override + { + if (line[0] == u'#' ) { + if (m_extendedFormat) { + if (line.startsWith(QLatin1String("#EXTINF:"))) { + m_extraInfo.clear(); + int artistStart = line.indexOf(QLatin1String(","), 8); + bool ok = false; + QStringView lineView { line }; + int length = lineView.mid(8, artistStart < 8 ? -1 : artistStart - 8).trimmed().toInt(&ok); + if (ok && length > 0) { + //convert from second to milisecond + m_extraInfo[QMediaMetaData::Duration] = QVariant(length * 1000); + } + if (artistStart > 0) { + int titleStart = getSplitIndex(line, artistStart); + if (titleStart > artistStart) { + m_extraInfo[QMediaMetaData::Author] = lineView.mid(artistStart + 1, + titleStart - artistStart - 1).trimmed().toString(). + replace(QLatin1String("--"), QLatin1String("-")); + m_extraInfo[QMediaMetaData::Title] = lineView.mid(titleStart + 1).trimmed().toString(). + replace(QLatin1String("--"), QLatin1String("-")); + } else { + m_extraInfo[QMediaMetaData::Title] = lineView.mid(artistStart + 1).trimmed().toString(). + replace(QLatin1String("--"), QLatin1String("-")); + } + } + } + } else if (lineIndex == 0 && line.startsWith(QLatin1String("#EXTM3U"))) { + m_extendedFormat = true; + } + } else { + QUrl url = expandToFullPath(root, line); + m_extraInfo[QMediaMetaData::Url] = url; + m_parent->playlist.append(url); + newItemFound(QVariant::fromValue(m_extraInfo)); + m_extraInfo.clear(); + } + + return true; + } + + int getSplitIndex(const QString& line, int startPos) + { + if (startPos < 0) + startPos = 0; + const QChar* buf = line.data(); + for (int i = startPos; i < line.length(); ++i) { + if (buf[i] == u'-') { + if (i == line.length() - 1) + return i; + ++i; + if (buf[i] != u'-') + return i - 1; + } + } + return -1; + } + +private: + QMediaMetaData m_extraInfo; + bool m_extendedFormat; +}; + +class PLSParser : public ParserBase +{ +public: + explicit PLSParser(QPlaylistFileParser *q) + : ParserBase(q) + { + } + +/* + * +The format is essentially that of an INI file structured as follows: + +Header + + * [playlist] : This tag indicates that it is a Playlist File + +Track Entry +Assuming track entry #X + + * FileX : Variable defining location of stream. + * TitleX : Defines track title. + * LengthX : Length in seconds of track. Value of -1 indicates indefinite. + +Footer + + * NumberOfEntries : This variable indicates the number of tracks. + * Version : Playlist version. Currently only a value of 2 is valid. + +[playlist] + +File1=Alternative\everclear - SMFTA.mp3 + +Title1=Everclear - So Much For The Afterglow + +Length1=233 + +File2=http://www.site.com:8000/listen.pls + +Title2=My Cool Stream + +Length5=-1 + +NumberOfEntries=2 + +Version=2 +*/ + bool parseLineImpl(int, const QString &line, const QUrl &root) override + { + // We ignore everything but 'File' entries, since that's the only thing we care about. + if (!line.startsWith(QLatin1String("File"))) + return true; + + QString value = getValue(line); + if (value.isEmpty()) + return true; + + QUrl path = expandToFullPath(root, value); + m_parent->playlist.append(path); + newItemFound(path); + + return true; + } + + QString getValue(QStringView line) { + int start = line.indexOf(u'='); + if (start < 0) + return QString(); + return line.mid(start + 1).trimmed().toString(); + } +}; +} + +///////////////////////////////////////////////////////////////////////////////////////////////// + +class QPlaylistFileParserPrivate +{ + Q_DECLARE_PUBLIC(QPlaylistFileParser) +public: + QPlaylistFileParserPrivate(QPlaylistFileParser *q) + : q_ptr(q) + , m_stream(nullptr) + , m_type(QPlaylistFileParser::UNKNOWN) + , m_scanIndex(0) + , m_lineIndex(-1) + , m_utf8(false) + , m_aborted(false) + { + } + + void handleData(); + void handleParserFinished(); + void abort(); + void reset(); + + QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> m_source; + QScopedPointer<ParserBase> m_currentParser; + QByteArray m_buffer; + QUrl m_root; + QNetworkAccessManager m_mgr; + QString m_mimeType; + QPlaylistFileParser *q_ptr; + QPointer<QIODevice> m_stream; + QPlaylistFileParser::FileType m_type; + struct ParserJob + { + QIODevice *m_stream; + QUrl m_media; + QString m_mimeType; + [[nodiscard]] bool isValid() const { return m_stream || !m_media.isEmpty(); } + void reset() { m_stream = nullptr; m_media = QUrl(); m_mimeType = QString(); } + } m_pendingJob; + int m_scanIndex; + int m_lineIndex; + bool m_utf8; + bool m_aborted; + +private: + bool processLine(int startIndex, int length); +}; + +#define LINE_LIMIT 4096 +#define READ_LIMIT 64 + +bool QPlaylistFileParserPrivate::processLine(int startIndex, int length) +{ + Q_Q(QPlaylistFileParser); + m_lineIndex++; + + if (!m_currentParser) { + const QString urlString = m_root.toString(); + const QString &suffix = !urlString.isEmpty() ? QFileInfo(urlString).suffix() : urlString; + QString mimeType; + if (m_source) + mimeType = m_source->header(QNetworkRequest::ContentTypeHeader).toString(); + m_type = QPlaylistFileParser::findPlaylistType(suffix, !mimeType.isEmpty() ? mimeType : m_mimeType, m_buffer.constData(), quint32(m_buffer.size())); + + switch (m_type) { + case QPlaylistFileParser::UNKNOWN: + emit q->error(QMediaPlaylist::FormatError, + QMediaPlaylist::tr("%1 playlist type is unknown").arg(m_root.toString())); + q->abort(); + return false; + case QPlaylistFileParser::M3U: + m_currentParser.reset(new M3UParser(q)); + break; + case QPlaylistFileParser::M3U8: + m_currentParser.reset(new M3UParser(q)); + m_utf8 = true; + break; + case QPlaylistFileParser::PLS: + m_currentParser.reset(new PLSParser(q)); + break; + } + + Q_ASSERT(!m_currentParser.isNull()); + } + + QString line; + + if (m_utf8) { + line = QString::fromUtf8(m_buffer.constData() + startIndex, length).trimmed(); + } else { + line = QString::fromLatin1(m_buffer.constData() + startIndex, length).trimmed(); + } + if (line.isEmpty()) + return true; + + Q_ASSERT(m_currentParser); + return m_currentParser->parseLine(m_lineIndex, line, m_root); +} + +void QPlaylistFileParserPrivate::handleData() +{ + Q_Q(QPlaylistFileParser); + while (m_stream->bytesAvailable() && !m_aborted) { + int expectedBytes = qMin(READ_LIMIT, int(qMin(m_stream->bytesAvailable(), + qint64(LINE_LIMIT - m_buffer.size())))); + m_buffer.push_back(m_stream->read(expectedBytes)); + int processedBytes = 0; + while (m_scanIndex < m_buffer.length() && !m_aborted) { + char s = m_buffer[m_scanIndex]; + if (s == '\r' || s == '\n') { + int l = m_scanIndex - processedBytes; + if (l > 0) { + if (!processLine(processedBytes, l)) + break; + } + processedBytes = m_scanIndex + 1; + if (!m_stream) { + //some error happened, so exit parsing + return; + } + } + m_scanIndex++; + } + + if (m_aborted) + break; + + if (m_buffer.length() - processedBytes >= LINE_LIMIT) { + emit q->error(QMediaPlaylist::FormatError, QMediaPlaylist::tr("invalid line in playlist file")); + q->abort(); + break; + } + + if (!m_stream->bytesAvailable() && (!m_source || !m_source->isFinished())) { + //last line + processLine(processedBytes, -1); + break; + } + + Q_ASSERT(m_buffer.length() == m_scanIndex); + if (processedBytes == 0) + continue; + + int copyLength = m_buffer.length() - processedBytes; + if (copyLength > 0) { + Q_ASSERT(copyLength <= READ_LIMIT); + m_buffer = m_buffer.right(copyLength); + } else { + m_buffer.clear(); + } + m_scanIndex = 0; + } + + handleParserFinished(); +} + +QPlaylistFileParser::QPlaylistFileParser(QObject *parent) + : QObject(parent) + , d_ptr(new QPlaylistFileParserPrivate(this)) +{ + +} + +QPlaylistFileParser::~QPlaylistFileParser() = default; + +QPlaylistFileParser::FileType QPlaylistFileParser::findByMimeType(const QString &mime) +{ + if (mime == QLatin1String("text/uri-list") || mime == QLatin1String("audio/x-mpegurl") || mime == QLatin1String("audio/mpegurl")) + return QPlaylistFileParser::M3U; + + if (mime == QLatin1String("application/x-mpegURL") || mime == QLatin1String("application/vnd.apple.mpegurl")) + return QPlaylistFileParser::M3U8; + + if (mime == QLatin1String("audio/x-scpls")) + return QPlaylistFileParser::PLS; + + return QPlaylistFileParser::UNKNOWN; +} + +QPlaylistFileParser::FileType QPlaylistFileParser::findBySuffixType(const QString &suffix) +{ + const QString &s = suffix.toLower(); + + if (s == QLatin1String("m3u")) + return QPlaylistFileParser::M3U; + + if (s == QLatin1String("m3u8")) + return QPlaylistFileParser::M3U8; + + if (s == QLatin1String("pls")) + return QPlaylistFileParser::PLS; + + return QPlaylistFileParser::UNKNOWN; +} + +QPlaylistFileParser::FileType QPlaylistFileParser::findByDataHeader(const char *data, quint32 size) +{ + if (!data || size == 0) + return QPlaylistFileParser::UNKNOWN; + + if (size >= 7 && strncmp(data, "#EXTM3U", 7) == 0) + return QPlaylistFileParser::M3U; + + if (size >= 10 && strncmp(data, "[playlist]", 10) == 0) + return QPlaylistFileParser::PLS; + + return QPlaylistFileParser::UNKNOWN; +} + +QPlaylistFileParser::FileType QPlaylistFileParser::findPlaylistType(const QString& suffix, + const QString& mime, + const char *data, + quint32 size) +{ + + FileType dataHeaderType = findByDataHeader(data, size); + if (dataHeaderType != UNKNOWN) + return dataHeaderType; + + FileType mimeType = findByMimeType(mime); + if (mimeType != UNKNOWN) + return mimeType; + + mimeType = findBySuffixType(mime); + if (mimeType != UNKNOWN) + return mimeType; + + FileType suffixType = findBySuffixType(suffix); + if (suffixType != UNKNOWN) + return suffixType; + + return UNKNOWN; +} + +/* + * Delegating + */ +void QPlaylistFileParser::start(const QUrl &media, QIODevice *stream, const QString &mimeType) +{ + if (stream) + start(stream, mimeType); + else + start(media, mimeType); +} + +void QPlaylistFileParser::start(QIODevice *stream, const QString &mimeType) +{ + Q_D(QPlaylistFileParser); + const bool validStream = stream ? (stream->isOpen() && stream->isReadable()) : false; + + if (!validStream) { + Q_EMIT error(QMediaPlaylist::AccessDeniedError, QMediaPlaylist::tr("Invalid stream")); + return; + } + + if (!d->m_currentParser.isNull()) { + abort(); + d->m_pendingJob = { stream, QUrl(), mimeType }; + return; + } + + playlist.clear(); + d->reset(); + d->m_mimeType = mimeType; + d->m_stream = stream; + connect(d->m_stream, SIGNAL(readyRead()), this, SLOT(handleData())); + d->handleData(); +} + +void QPlaylistFileParser::start(const QUrl& request, const QString &mimeType) +{ + Q_D(QPlaylistFileParser); + const QUrl &url = request.url(); + + if (url.isLocalFile() && !QFile::exists(url.toLocalFile())) { + emit error(QMediaPlaylist::AccessDeniedError, QString(QMediaPlaylist::tr("%1 does not exist")).arg(url.toString())); + return; + } + + if (!d->m_currentParser.isNull()) { + abort(); + d->m_pendingJob = { nullptr, request, mimeType }; + return; + } + + d->reset(); + d->m_root = url; + d->m_mimeType = mimeType; + d->m_source.reset(d->m_mgr.get(QNetworkRequest(request))); + d->m_stream = d->m_source.get(); + connect(d->m_source.data(), SIGNAL(readyRead()), this, SLOT(handleData())); + connect(d->m_source.data(), SIGNAL(finished()), this, SLOT(handleData())); + connect(d->m_source.data(), SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this, SLOT(handleError())); + + if (url.isLocalFile()) + d->handleData(); +} + +void QPlaylistFileParser::abort() +{ + Q_D(QPlaylistFileParser); + d->abort(); + + if (d->m_source) + d->m_source->disconnect(); + + if (d->m_stream) + disconnect(d->m_stream, SIGNAL(readyRead()), this, SLOT(handleData())); + + playlist.clear(); +} + +void QPlaylistFileParser::handleData() +{ + Q_D(QPlaylistFileParser); + d->handleData(); +} + +void QPlaylistFileParserPrivate::handleParserFinished() +{ + Q_Q(QPlaylistFileParser); + const bool isParserValid = !m_currentParser.isNull(); + if (!isParserValid && !m_aborted) + emit q->error(QMediaPlaylist::FormatNotSupportedError, QMediaPlaylist::tr("Empty file provided")); + + if (isParserValid && !m_aborted) { + m_currentParser.reset(); + emit q->finished(); + } + + if (!m_aborted) + q->abort(); + + if (!m_source.isNull()) + m_source.reset(); + + if (m_pendingJob.isValid()) + q->start(m_pendingJob.m_media, m_pendingJob.m_stream, m_pendingJob.m_mimeType); +} + +void QPlaylistFileParserPrivate::abort() +{ + m_aborted = true; + if (!m_currentParser.isNull()) + m_currentParser->abort(); +} + +void QPlaylistFileParserPrivate::reset() +{ + Q_ASSERT(m_currentParser.isNull()); + Q_ASSERT(m_source.isNull()); + m_buffer.clear(); + m_root.clear(); + m_mimeType.clear(); + m_stream = nullptr; + m_type = QPlaylistFileParser::UNKNOWN; + m_scanIndex = 0; + m_lineIndex = -1; + m_utf8 = false; + m_aborted = false; + m_pendingJob.reset(); +} + +void QPlaylistFileParser::handleError() +{ + Q_D(QPlaylistFileParser); + const QString &errorString = d->m_source->errorString(); + Q_EMIT error(QMediaPlaylist::NetworkError, errorString); + abort(); +} + +QT_END_NAMESPACE diff --git a/examples/multimedia/player/qplaylistfileparser_p.h b/examples/multimedia/player/qplaylistfileparser_p.h new file mode 100644 index 000000000..3d2016736 --- /dev/null +++ b/examples/multimedia/player/qplaylistfileparser_p.h @@ -0,0 +1,80 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef PLAYLISTFILEPARSER_P_H +#define PLAYLISTFILEPARSER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qtmultimediaglobal.h" +#include "qmediaplaylist.h" +#include <QtCore/qobject.h> + +QT_BEGIN_NAMESPACE + +class QIODevice; +class QUrl; +class QNetworkRequest; + +class QPlaylistFileParserPrivate; + +class QPlaylistFileParser : public QObject +{ + Q_OBJECT +public: + QPlaylistFileParser(QObject *parent = nullptr); + ~QPlaylistFileParser(); + + enum FileType + { + UNKNOWN, + M3U, + M3U8, // UTF-8 version of M3U + PLS + }; + + void start(const QUrl &media, QIODevice *stream = nullptr, const QString &mimeType = QString()); + void start(const QUrl &request, const QString &mimeType = QString()); + void start(QIODevice *stream, const QString &mimeType = QString()); + void abort(); + + QList<QUrl> playlist; + +Q_SIGNALS: + void newItem(const QVariant& content); + void finished(); + void error(QMediaPlaylist::Error err, const QString& errorMsg); + +private Q_SLOTS: + void handleData(); + void handleError(); + +private: + + static FileType findByMimeType(const QString &mime); + static FileType findBySuffixType(const QString &suffix); + static FileType findByDataHeader(const char *data, quint32 size); + static FileType findPlaylistType(QIODevice *device, + const QString& mime); + static FileType findPlaylistType(const QString &suffix, + const QString& mime, + const char *data = nullptr, + quint32 size = 0); + + Q_DISABLE_COPY(QPlaylistFileParser) + Q_DECLARE_PRIVATE(QPlaylistFileParser) + QScopedPointer<QPlaylistFileParserPrivate> d_ptr; +}; + +QT_END_NAMESPACE + +#endif // PLAYLISTFILEPARSER_P_H diff --git a/examples/multimedia/player/videowidget.cpp b/examples/multimedia/player/videowidget.cpp new file mode 100644 index 000000000..e7f906e38 --- /dev/null +++ b/examples/multimedia/player/videowidget.cpp @@ -0,0 +1,46 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "videowidget.h" + +#include <QKeyEvent> +#include <QMouseEvent> + +VideoWidget::VideoWidget(QWidget *parent) + : QVideoWidget(parent) +{ + setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); + + QPalette p = palette(); + p.setColor(QPalette::Window, Qt::black); + setPalette(p); + +#ifndef Q_OS_ANDROID // QTBUG-95723 + setAttribute(Qt::WA_OpaquePaintEvent); +#endif +} + +void VideoWidget::keyPressEvent(QKeyEvent *event) +{ + if ((event->key() == Qt::Key_Escape || event->key() == Qt::Key_Back) && isFullScreen()) { + setFullScreen(false); + event->accept(); + } else if (event->key() == Qt::Key_Enter && event->modifiers() & Qt::Key_Alt) { + setFullScreen(!isFullScreen()); + event->accept(); + } else { + QVideoWidget::keyPressEvent(event); + } +} + +void VideoWidget::mouseDoubleClickEvent(QMouseEvent *event) +{ + setFullScreen(!isFullScreen()); + event->accept(); +} + +void VideoWidget::mousePressEvent(QMouseEvent *event) +{ + QVideoWidget::mousePressEvent(event); +} + diff --git a/examples/multimedia/player/videowidget.h b/examples/multimedia/player/videowidget.h new file mode 100644 index 000000000..3505a3fb8 --- /dev/null +++ b/examples/multimedia/player/videowidget.h @@ -0,0 +1,22 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef VIDEOWIDGET_H +#define VIDEOWIDGET_H + +#include <QVideoWidget> + +class VideoWidget : public QVideoWidget +{ + Q_OBJECT + +public: + explicit VideoWidget(QWidget *parent = nullptr); + +protected: + void keyPressEvent(QKeyEvent *event) override; + void mouseDoubleClickEvent(QMouseEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; +}; + +#endif // VIDEOWIDGET_H |