diff options
Diffstat (limited to 'examples/multimedia')
57 files changed, 6242 insertions, 6 deletions
diff --git a/examples/multimedia/CMakeLists.txt b/examples/multimedia/CMakeLists.txt index 6123c7994..19a999541 100644 --- a/examples/multimedia/CMakeLists.txt +++ b/examples/multimedia/CMakeLists.txt @@ -3,13 +3,17 @@ if(NOT ANDROID AND NOT IOS) qt_internal_add_example(devices) endif() if(TARGET Qt::Widgets) - qt_internal_add_example(spectrum) - qt_internal_add_example(audiorecorder) if(NOT ANDROID AND NOT IOS) qt_internal_add_example(audiodevices) endif() - qt_internal_add_example(audiosource) qt_internal_add_example(audiooutput) + qt_internal_add_example(audiorecorder) + qt_internal_add_example(audiosource) + qt_internal_add_example(camera) + qt_internal_add_example(player) + qt_internal_add_example(spectrum) + qt_internal_add_example(videographicsitem) + qt_internal_add_example(videowidget) endif() if(TARGET Qt::Quick) qt_internal_add_example(declarative-camera) diff --git a/examples/multimedia/camera/CMakeLists.txt b/examples/multimedia/camera/CMakeLists.txt new file mode 100644 index 000000000..2c2c2a935 --- /dev/null +++ b/examples/multimedia/camera/CMakeLists.txt @@ -0,0 +1,81 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +cmake_minimum_required(VERSION 3.16) +project(camera LANGUAGES CXX) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) + +if(NOT DEFINED INSTALL_EXAMPLESDIR) + set(INSTALL_EXAMPLESDIR "examples") +endif() + +set(INSTALL_EXAMPLEDIR "${INSTALL_EXAMPLESDIR}/multimedia/camera") + +find_package(Qt6 REQUIRED COMPONENTS Core Gui Multimedia MultimediaWidgets Widgets) + +set(camera_form "") +set(videosettings_form "") +if(ANDROID OR IOS) + set(camera_form camera_mobile.ui) + set(videosettings_form videosettings_mobile.ui) +else() + set(camera_form camera.ui) + set(videosettings_form videosettings.ui) +endif() + +qt_add_executable(camera + MANUAL_FINALIZATION + camera.cpp camera.h ${camera_form} + imagesettings.cpp imagesettings.h imagesettings.ui + main.cpp + videosettings.cpp videosettings.h ${videosettings_form} + metadatadialog.cpp metadatadialog.h +) + +set_target_properties(camera PROPERTIES + WIN32_EXECUTABLE TRUE + MACOSX_BUNDLE TRUE +) + +if(APPLE AND NOT IOS) + set_target_properties(camera PROPERTIES + MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.plist.in" + ) +elseif(IOS) + set_target_properties(camera PROPERTIES + MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/ios/Info.plist.in" + ) +endif() + +set_property(TARGET camera APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR + ${CMAKE_CURRENT_SOURCE_DIR}/android) + +target_link_libraries(camera PUBLIC + Qt::Core + Qt::Gui + Qt::Multimedia + Qt::MultimediaWidgets + Qt::Widgets +) + +# Resources: +set(camera_resource_files + "images/shutter.svg" +) + +qt_add_resources(camera "camera" + PREFIX + "/" + FILES + ${camera_resource_files} +) + +qt_finalize_executable(camera) + +install(TARGETS camera + RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}" + BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}" + LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}" +) diff --git a/examples/multimedia/camera/android/AndroidManifest.xml b/examples/multimedia/camera/android/AndroidManifest.xml new file mode 100644 index 000000000..29c4672cf --- /dev/null +++ b/examples/multimedia/camera/android/AndroidManifest.xml @@ -0,0 +1,68 @@ +<?xml version="1.0"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="org.qtproject.example.camera" + android:installLocation="auto" + android:versionCode="-- %%INSERT_VERSION_CODE%% --" + android:versionName="-- %%INSERT_VERSION_NAME%% --"> + <!-- The comment below will be replaced with dependencies permissions upon deployment. + Remove the comment if you do not require these default permissions. --> + <!-- %%INSERT_PERMISSIONS --> + + <!-- The comment below will be replaced with dependencies permissions upon deployment. + Remove the comment if you do not require these default features. --> + <!-- %%INSERT_FEATURES --> + + <supports-screens + android:anyDensity="true" + android:largeScreens="true" + android:normalScreens="true" + android:smallScreens="true" /> + <application + android:name="org.qtproject.qt.android.bindings.QtApplication" + android:extractNativeLibs="true" + android:hardwareAccelerated="true" + android:label="-- %%INSERT_APP_NAME%% --" + android:requestLegacyExternalStorage="true"> + <activity + android:name="org.qtproject.qt.android.bindings.QtActivity" + android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" + android:label="-- %%INSERT_APP_NAME%% --" + android:launchMode="singleTop" + android:screenOrientation="portrait"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + <!-- Application arguments --> + + <meta-data + android:name="android.app.arguments" + android:value="-- %%INSERT_APP_ARGUMENTS%% --" /> + <!-- Application arguments --> + <meta-data + android:name="android.app.lib_name" + android:value="-- %%INSERT_APP_LIB_NAME%% --" /> + + <!-- Background running --> + <!-- Warning: changing this value to true may cause unexpected crashes if the + application still try to draw after + "applicationStateChanged(Qt::ApplicationSuspended)" signal is sent! --> + <meta-data + android:name="android.app.background_running" + android:value="false" /> + <!-- Background running --> + + <!-- extract android style --> + <!-- available android:values : + * default - In most cases this will be the same as "full", but it can also be + * something else if needed, e.g., for compatibility reasons + * full - useful QWidget & Quick Controls 1 apps + * minimal - useful for Quick Controls 2 apps, it is much faster than "full" + * none - useful for apps that don't use any of the above Qt modules --> + <meta-data + android:name="android.app.extract_android_style" + android:value="minimal" /> + <!-- extract android style --> + </activity> + </application> +</manifest> diff --git a/examples/multimedia/camera/camera.cpp b/examples/multimedia/camera/camera.cpp new file mode 100644 index 000000000..201e6e985 --- /dev/null +++ b/examples/multimedia/camera/camera.cpp @@ -0,0 +1,387 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "camera.h" +#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) +#include "ui_camera_mobile.h" +#else +#include "ui_camera.h" +#endif +#include "videosettings.h" +#include "imagesettings.h" +#include "metadatadialog.h" + +#include <QMediaRecorder> +#include <QVideoWidget> +#include <QCameraDevice> +#include <QMediaMetaData> +#include <QMediaDevices> +#include <QAudioDevice> +#include <QAudioInput> + +#include <QMessageBox> +#include <QPalette> +#include <QImage> + +#include <QtWidgets> +#include <QMediaDevices> +#include <QMediaFormat> + +Camera::Camera() + : ui(new Ui::Camera) +{ + ui->setupUi(this); + + m_audioInput.reset(new QAudioInput); + m_captureSession.setAudioInput(m_audioInput.get()); + + //Camera devices: + + videoDevicesGroup = new QActionGroup(this); + videoDevicesGroup->setExclusive(true); + updateCameras(); + connect(&m_devices, &QMediaDevices::videoInputsChanged, this, &Camera::updateCameras); + + connect(videoDevicesGroup, &QActionGroup::triggered, this, &Camera::updateCameraDevice); + connect(ui->captureWidget, &QTabWidget::currentChanged, this, &Camera::updateCaptureMode); + + connect(ui->metaDataButton, &QPushButton::clicked, this, &Camera::showMetaDataDialog); + + setCamera(QMediaDevices::defaultVideoInput()); +} + +void Camera::setCamera(const QCameraDevice &cameraDevice) +{ + m_camera.reset(new QCamera(cameraDevice)); + m_captureSession.setCamera(m_camera.data()); + + connect(m_camera.data(), &QCamera::activeChanged, this, &Camera::updateCameraActive); + connect(m_camera.data(), &QCamera::errorOccurred, this, &Camera::displayCameraError); + + if (!m_mediaRecorder) { + m_mediaRecorder.reset(new QMediaRecorder); + m_captureSession.setRecorder(m_mediaRecorder.data()); + connect(m_mediaRecorder.data(), &QMediaRecorder::recorderStateChanged, this, &Camera::updateRecorderState); + } + + m_imageCapture = new QImageCapture; + m_captureSession.setImageCapture(m_imageCapture); + + connect(m_mediaRecorder.data(), &QMediaRecorder::durationChanged, this, &Camera::updateRecordTime); + connect(m_mediaRecorder.data(), &QMediaRecorder::errorChanged, this, &Camera::displayRecorderError); + + connect(ui->exposureCompensation, &QAbstractSlider::valueChanged, this, &Camera::setExposureCompensation); + + m_captureSession.setVideoOutput(ui->viewfinder); + + updateCameraActive(m_camera->isActive()); + updateRecorderState(m_mediaRecorder->recorderState()); + + connect(m_imageCapture, &QImageCapture::readyForCaptureChanged, this, &Camera::readyForCapture); + connect(m_imageCapture, &QImageCapture::imageCaptured, this, &Camera::processCapturedImage); + connect(m_imageCapture, &QImageCapture::imageSaved, this, &Camera::imageSaved); + connect(m_imageCapture, &QImageCapture::errorOccurred, this, &Camera::displayCaptureError); + readyForCapture(m_imageCapture->isReadyForCapture()); + + updateCaptureMode(); + + if (m_camera->cameraFormat().isNull()) { + auto formats = cameraDevice.videoFormats(); + if (!formats.isEmpty()) { + // Choose a decent camera format: Maximum resolution at at least 30 FPS + // we use 29 FPS to compare against as some cameras report 29.97 FPS... + QCameraFormat bestFormat; + for (const auto &fmt : formats) { + if (bestFormat.maxFrameRate() < 29 && fmt.maxFrameRate() > bestFormat.maxFrameRate()) + bestFormat = fmt; + else if (bestFormat.maxFrameRate() == fmt.maxFrameRate() && + bestFormat.resolution().width()*bestFormat.resolution().height() < + fmt.resolution().width()*fmt.resolution().height()) + bestFormat = fmt; + } + + m_camera->setCameraFormat(bestFormat); + m_mediaRecorder->setVideoFrameRate(bestFormat.maxFrameRate()); + } + } + + m_camera->start(); +} + +void Camera::keyPressEvent(QKeyEvent * event) +{ + if (event->isAutoRepeat()) + return; + + switch (event->key()) { + case Qt::Key_CameraFocus: + displayViewfinder(); + event->accept(); + break; + case Qt::Key_Camera: + if (m_doImageCapture) { + takeImage(); + } else { + if (m_mediaRecorder->recorderState() == QMediaRecorder::RecordingState) + stop(); + else + record(); + } + event->accept(); + break; + default: + QMainWindow::keyPressEvent(event); + } +} + +void Camera::keyReleaseEvent(QKeyEvent *event) +{ + QMainWindow::keyReleaseEvent(event); +} + +void Camera::updateRecordTime() +{ + QString str = QString("Recorded %1 sec").arg(m_mediaRecorder->duration()/1000); + ui->statusbar->showMessage(str); +} + +void Camera::processCapturedImage(int requestId, const QImage& img) +{ + Q_UNUSED(requestId); + QImage scaledImage = img.scaled(ui->viewfinder->size(), + Qt::KeepAspectRatio, + Qt::SmoothTransformation); + + ui->lastImagePreviewLabel->setPixmap(QPixmap::fromImage(scaledImage)); + + // Display captured image for 4 seconds. + displayCapturedImage(); + QTimer::singleShot(4000, this, &Camera::displayViewfinder); +} + +void Camera::configureCaptureSettings() +{ + if (m_doImageCapture) + configureImageSettings(); + else + configureVideoSettings(); +} + +void Camera::configureVideoSettings() +{ + VideoSettings settingsDialog(m_mediaRecorder.data()); + settingsDialog.setWindowFlags(settingsDialog.windowFlags() & ~Qt::WindowContextHelpButtonHint); + + if (settingsDialog.exec()) + settingsDialog.applySettings(); +} + +void Camera::configureImageSettings() +{ + ImageSettings settingsDialog(m_imageCapture); + settingsDialog.setWindowFlags(settingsDialog.windowFlags() & ~Qt::WindowContextHelpButtonHint); + + if (settingsDialog.exec()) { + settingsDialog.applyImageSettings(); + } +} + +void Camera::record() +{ + m_mediaRecorder->record(); + updateRecordTime(); +} + +void Camera::pause() +{ + m_mediaRecorder->pause(); +} + +void Camera::stop() +{ + m_mediaRecorder->stop(); +} + +void Camera::setMuted(bool muted) +{ + m_captureSession.audioInput()->setMuted(muted); +} + +void Camera::takeImage() +{ + m_isCapturingImage = true; + m_imageCapture->captureToFile(); +} + +void Camera::displayCaptureError(int id, const QImageCapture::Error error, const QString &errorString) +{ + Q_UNUSED(id); + Q_UNUSED(error); + QMessageBox::warning(this, tr("Image Capture Error"), errorString); + m_isCapturingImage = false; +} + +void Camera::startCamera() +{ + m_camera->start(); +} + +void Camera::stopCamera() +{ + m_camera->stop(); +} + +void Camera::updateCaptureMode() +{ + int tabIndex = ui->captureWidget->currentIndex(); + m_doImageCapture = (tabIndex == 0); +} + +void Camera::updateCameraActive(bool active) +{ + if (active) { + ui->actionStartCamera->setEnabled(false); + ui->actionStopCamera->setEnabled(true); + ui->captureWidget->setEnabled(true); + ui->actionSettings->setEnabled(true); + } else { + ui->actionStartCamera->setEnabled(true); + ui->actionStopCamera->setEnabled(false); + ui->captureWidget->setEnabled(false); + ui->actionSettings->setEnabled(false); + } +} + +void Camera::updateRecorderState(QMediaRecorder::RecorderState state) +{ + switch (state) { + case QMediaRecorder::StoppedState: + ui->recordButton->setEnabled(true); + ui->pauseButton->setEnabled(true); + ui->stopButton->setEnabled(false); + ui->metaDataButton->setEnabled(true); + break; + case QMediaRecorder::PausedState: + ui->recordButton->setEnabled(true); + ui->pauseButton->setEnabled(false); + ui->stopButton->setEnabled(true); + ui->metaDataButton->setEnabled(false); + break; + case QMediaRecorder::RecordingState: + ui->recordButton->setEnabled(false); + ui->pauseButton->setEnabled(true); + ui->stopButton->setEnabled(true); + ui->metaDataButton->setEnabled(false); + break; + } +} + +void Camera::setExposureCompensation(int index) +{ + m_camera->setExposureCompensation(index*0.5); +} + +void Camera::displayRecorderError() +{ + if (m_mediaRecorder->error() != QMediaRecorder::NoError) + QMessageBox::warning(this, tr("Capture Error"), m_mediaRecorder->errorString()); +} + +void Camera::displayCameraError() +{ + if (m_camera->error() != QCamera::NoError) + QMessageBox::warning(this, tr("Camera Error"), m_camera->errorString()); +} + +void Camera::updateCameraDevice(QAction *action) +{ + setCamera(qvariant_cast<QCameraDevice>(action->data())); +} + +void Camera::displayViewfinder() +{ + ui->stackedWidget->setCurrentIndex(0); +} + +void Camera::displayCapturedImage() +{ + ui->stackedWidget->setCurrentIndex(1); +} + +void Camera::readyForCapture(bool ready) +{ + ui->takeImageButton->setEnabled(ready); +} + +void Camera::imageSaved(int id, const QString &fileName) +{ + Q_UNUSED(id); + ui->statusbar->showMessage(tr("Captured \"%1\"").arg(QDir::toNativeSeparators(fileName))); + + m_isCapturingImage = false; + if (m_applicationExiting) + close(); +} + +void Camera::closeEvent(QCloseEvent *event) +{ + if (m_isCapturingImage) { + setEnabled(false); + m_applicationExiting = true; + event->ignore(); + } else { + event->accept(); + } +} + +void Camera::updateCameras() +{ + ui->menuDevices->clear(); + const QList<QCameraDevice> availableCameras = QMediaDevices::videoInputs(); + for (const QCameraDevice &cameraDevice : availableCameras) { + QAction *videoDeviceAction = new QAction(cameraDevice.description(), videoDevicesGroup); + videoDeviceAction->setCheckable(true); + videoDeviceAction->setData(QVariant::fromValue(cameraDevice)); + if (cameraDevice == QMediaDevices::defaultVideoInput()) + videoDeviceAction->setChecked(true); + + ui->menuDevices->addAction(videoDeviceAction); + } +} + +void Camera::showMetaDataDialog() +{ + if (!m_metaDataDialog) + m_metaDataDialog = new MetaDataDialog(this); + m_metaDataDialog->setAttribute(Qt::WA_DeleteOnClose, false); + if (m_metaDataDialog->exec() == QDialog::Accepted) + saveMetaData(); +} + +void Camera::saveMetaData() +{ + QMediaMetaData data; + for (int i = 0; i < QMediaMetaData::NumMetaData; i++) { + QString val = m_metaDataDialog->m_metaDataFields[i]->text(); + if (!val.isEmpty()) { + auto key = static_cast<QMediaMetaData::Key>(i); + if (i == QMediaMetaData::CoverArtImage) { + QImage coverArt(val); + data.insert(key, coverArt); + } + else if (i == QMediaMetaData::ThumbnailImage) { + QImage thumbnail(val); + data.insert(key, thumbnail); + } + else if (i == QMediaMetaData::Date) { + QDateTime date = QDateTime::fromString(val); + data.insert(key, date); + } + else { + data.insert(key, val); + } + } + } + m_mediaRecorder->setMetaData(data); +} + diff --git a/examples/multimedia/camera/camera.h b/examples/multimedia/camera/camera.h new file mode 100644 index 000000000..ae8eb5919 --- /dev/null +++ b/examples/multimedia/camera/camera.h @@ -0,0 +1,101 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef CAMERA_H +#define CAMERA_H + +#include <QCamera> +#include <QImageCapture> +#include <QMediaRecorder> +#include <QScopedPointer> +#include <QMediaMetaData> +#include <QMediaCaptureSession> +#include <QMediaDevices> +#include <QAudioInput> + +#include <QMainWindow> + +QT_BEGIN_NAMESPACE +namespace Ui { class Camera; } +class QActionGroup; +QT_END_NAMESPACE + +class MetaDataDialog; + +class Camera : public QMainWindow +{ + Q_OBJECT + +public: + Camera(); + +public slots: + void saveMetaData(); + +private slots: + void setCamera(const QCameraDevice &cameraDevice); + + void startCamera(); + void stopCamera(); + + void record(); + void pause(); + void stop(); + void setMuted(bool); + + void takeImage(); + void displayCaptureError(int, QImageCapture::Error, const QString &errorString); + + void configureCaptureSettings(); + void configureVideoSettings(); + void configureImageSettings(); + + void displayRecorderError(); + void displayCameraError(); + + void updateCameraDevice(QAction *action); + + void updateCameraActive(bool active); + void updateCaptureMode(); + void updateRecorderState(QMediaRecorder::RecorderState state); + void setExposureCompensation(int index); + + void updateRecordTime(); + + void processCapturedImage(int requestId, const QImage &img); + + void displayViewfinder(); + void displayCapturedImage(); + + void readyForCapture(bool ready); + void imageSaved(int id, const QString &fileName); + + void updateCameras(); + + void showMetaDataDialog(); + +protected: + void keyPressEvent(QKeyEvent *event) override; + void keyReleaseEvent(QKeyEvent *event) override; + void closeEvent(QCloseEvent *event) override; + +private: + Ui::Camera *ui; + + QActionGroup *videoDevicesGroup = nullptr; + + QMediaDevices m_devices; + QMediaCaptureSession m_captureSession; + QScopedPointer<QCamera> m_camera; + QScopedPointer<QAudioInput> m_audioInput; + QImageCapture *m_imageCapture; + QScopedPointer<QMediaRecorder> m_mediaRecorder; + + bool m_isCapturingImage = false; + bool m_applicationExiting = false; + bool m_doImageCapture = true; + + MetaDataDialog *m_metaDataDialog = nullptr; +}; + +#endif diff --git a/examples/multimedia/camera/camera.pro b/examples/multimedia/camera/camera.pro new file mode 100644 index 000000000..283d84640 --- /dev/null +++ b/examples/multimedia/camera/camera.pro @@ -0,0 +1,40 @@ +TEMPLATE = app +TARGET = camera + +QT += multimedia multimediawidgets + +HEADERS = \ + camera.h \ + imagesettings.h \ + videosettings.h \ + metadatadialog.h + +SOURCES = \ + main.cpp \ + camera.cpp \ + imagesettings.cpp \ + videosettings.cpp \ + metadatadialog.cpp + +FORMS += \ + imagesettings.ui + +android|ios { + FORMS += \ + camera_mobile.ui \ + videosettings_mobile.ui +} else { + FORMS += \ + camera.ui \ + videosettings.ui +} +RESOURCES += camera.qrc + +target.path = $$[QT_INSTALL_EXAMPLES]/multimedia/camera +INSTALLS += target + +QT += widgets +include(../../multimedia/shared/shared.pri) + +ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android +OTHER_FILES += android/AndroidManifest.xml diff --git a/examples/multimedia/camera/camera.qrc b/examples/multimedia/camera/camera.qrc new file mode 100644 index 000000000..a915eb596 --- /dev/null +++ b/examples/multimedia/camera/camera.qrc @@ -0,0 +1,5 @@ +<!DOCTYPE RCC><RCC version="1.0"> +<qresource> + <file>images/shutter.svg</file> +</qresource> +</RCC> diff --git a/examples/multimedia/camera/camera.ui b/examples/multimedia/camera/camera.ui new file mode 100644 index 000000000..560ee7fed --- /dev/null +++ b/examples/multimedia/camera/camera.ui @@ -0,0 +1,488 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>Camera</class> + <widget class="QMainWindow" name="Camera"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>668</width> + <height>429</height> + </rect> + </property> + <property name="windowTitle"> + <string>Camera</string> + </property> + <widget class="QWidget" name="centralwidget"> + <layout class="QGridLayout" name="gridLayout_3"> + <item row="1" column="1" colspan="2"> + <widget class="QTabWidget" name="captureWidget"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="tab_2"> + <attribute name="title"> + <string>Image</string> + </attribute> + <layout class="QGridLayout" name="gridLayout"> + <item row="3" column="0"> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>161</height> + </size> + </property> + </spacer> + </item> + <item row="0" column="0"> + <widget class="QPushButton" name="takeImageButton"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Capture Photo</string> + </property> + <property name="icon"> + <iconset resource="camera.qrc"> + <normaloff>:/images/shutter.svg</normaloff>:/images/shutter.svg</iconset> + </property> + </widget> + </item> + <item row="5" column="0"> + <widget class="QSlider" name="exposureCompensation"> + <property name="minimum"> + <number>-4</number> + </property> + <property name="maximum"> + <number>4</number> + </property> + <property name="pageStep"> + <number>2</number> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="tickPosition"> + <enum>QSlider::TicksAbove</enum> + </property> + </widget> + </item> + <item row="4" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Exposure Compensation:</string> + </property> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="tab"> + <attribute name="title"> + <string>Video</string> + </attribute> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="0" column="0"> + <widget class="QPushButton" name="recordButton"> + <property name="text"> + <string>Record</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QPushButton" name="pauseButton"> + <property name="text"> + <string>Pause</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QPushButton" name="stopButton"> + <property name="text"> + <string>Stop</string> + </property> + </widget> + </item> + <item row="3" column="0"> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>76</height> + </size> + </property> + </spacer> + </item> + <item row="4" column="0"> + <widget class="QPushButton" name="muteButton"> + <property name="text"> + <string>Mute</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="5" column="0"> + <widget class="QPushButton" name="metaDataButton"> + <property name="text"> + <string>Set metadata</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + <item row="0" column="0" rowspan="2"> + <widget class="QStackedWidget" name="stackedWidget"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>1</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="palette"> + <palette> + <active> + <colorrole role="Base"> + <brush brushstyle="SolidPattern"> + <color alpha="255"> + <red>255</red> + <green>255</green> + <blue>255</blue> + </color> + </brush> + </colorrole> + <colorrole role="Window"> + <brush brushstyle="SolidPattern"> + <color alpha="255"> + <red>145</red> + <green>145</green> + <blue>145</blue> + </color> + </brush> + </colorrole> + </active> + <inactive> + <colorrole role="Base"> + <brush brushstyle="SolidPattern"> + <color alpha="255"> + <red>255</red> + <green>255</green> + <blue>255</blue> + </color> + </brush> + </colorrole> + <colorrole role="Window"> + <brush brushstyle="SolidPattern"> + <color alpha="255"> + <red>145</red> + <green>145</green> + <blue>145</blue> + </color> + </brush> + </colorrole> + </inactive> + <disabled> + <colorrole role="Base"> + <brush brushstyle="SolidPattern"> + <color alpha="255"> + <red>145</red> + <green>145</green> + <blue>145</blue> + </color> + </brush> + </colorrole> + <colorrole role="Window"> + <brush brushstyle="SolidPattern"> + <color alpha="255"> + <red>145</red> + <green>145</green> + <blue>145</blue> + </color> + </brush> + </colorrole> + </disabled> + </palette> + </property> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="viewfinderPage"> + <layout class="QGridLayout" name="gridLayout_5"> + <item row="0" column="0"> + <widget class="QVideoWidget" name="viewfinder" native="true"/> + </item> + </layout> + </widget> + <widget class="QWidget" name="previewPage"> + <layout class="QGridLayout" name="gridLayout_4"> + <item row="0" column="0"> + <widget class="QLabel" name="lastImagePreviewLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="frameShape"> + <enum>QFrame::Box</enum> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <widget class="QMenuBar" name="menubar"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>668</width> + <height>28</height> + </rect> + </property> + <widget class="QMenu" name="menuFile"> + <property name="title"> + <string>File</string> + </property> + <addaction name="actionStartCamera"/> + <addaction name="actionStopCamera"/> + <addaction name="separator"/> + <addaction name="actionSettings"/> + <addaction name="separator"/> + <addaction name="actionExit"/> + </widget> + <widget class="QMenu" name="menuDevices"> + <property name="title"> + <string>Devices</string> + </property> + </widget> + <addaction name="menuFile"/> + <addaction name="menuDevices"/> + </widget> + <widget class="QStatusBar" name="statusbar"/> + <action name="actionExit"> + <property name="text"> + <string>Close</string> + </property> + </action> + <action name="actionStartCamera"> + <property name="text"> + <string>Start Camera</string> + </property> + </action> + <action name="actionStopCamera"> + <property name="text"> + <string>Stop Camera</string> + </property> + </action> + <action name="actionSettings"> + <property name="text"> + <string>Change Settings</string> + </property> + </action> + </widget> + <customwidgets> + <customwidget> + <class>QVideoWidget</class> + <extends>QWidget</extends> + <header>qvideowidget.h</header> + <container>1</container> + </customwidget> + </customwidgets> + <resources> + <include location="camera.qrc"/> + </resources> + <connections> + <connection> + <sender>recordButton</sender> + <signal>clicked()</signal> + <receiver>Camera</receiver> + <slot>record()</slot> + <hints> + <hint type="sourcelabel"> + <x>647</x> + <y>149</y> + </hint> + <hint type="destinationlabel"> + <x>61</x> + <y>238</y> + </hint> + </hints> + </connection> + <connection> + <sender>stopButton</sender> + <signal>clicked()</signal> + <receiver>Camera</receiver> + <slot>stop()</slot> + <hints> + <hint type="sourcelabel"> + <x>647</x> + <y>225</y> + </hint> + <hint type="destinationlabel"> + <x>140</x> + <y>236</y> + </hint> + </hints> + </connection> + <connection> + <sender>pauseButton</sender> + <signal>clicked()</signal> + <receiver>Camera</receiver> + <slot>pause()</slot> + <hints> + <hint type="sourcelabel"> + <x>647</x> + <y>187</y> + </hint> + <hint type="destinationlabel"> + <x>234</x> + <y>237</y> + </hint> + </hints> + </connection> + <connection> + <sender>actionExit</sender> + <signal>triggered()</signal> + <receiver>Camera</receiver> + <slot>close()</slot> + <hints> + <hint type="sourcelabel"> + <x>-1</x> + <y>-1</y> + </hint> + <hint type="destinationlabel"> + <x>154</x> + <y>130</y> + </hint> + </hints> + </connection> + <connection> + <sender>takeImageButton</sender> + <signal>clicked()</signal> + <receiver>Camera</receiver> + <slot>takeImage()</slot> + <hints> + <hint type="sourcelabel"> + <x>625</x> + <y>132</y> + </hint> + <hint type="destinationlabel"> + <x>603</x> + <y>169</y> + </hint> + </hints> + </connection> + <connection> + <sender>muteButton</sender> + <signal>toggled(bool)</signal> + <receiver>Camera</receiver> + <slot>setMuted(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>647</x> + <y>377</y> + </hint> + <hint type="destinationlabel"> + <x>5</x> + <y>280</y> + </hint> + </hints> + </connection> + <connection> + <sender>exposureCompensation</sender> + <signal>valueChanged(int)</signal> + <receiver>Camera</receiver> + <slot>setExposureCompensation(int)</slot> + <hints> + <hint type="sourcelabel"> + <x>559</x> + <y>367</y> + </hint> + <hint type="destinationlabel"> + <x>665</x> + <y>365</y> + </hint> + </hints> + </connection> + <connection> + <sender>actionSettings</sender> + <signal>triggered()</signal> + <receiver>Camera</receiver> + <slot>configureCaptureSettings()</slot> + <hints> + <hint type="sourcelabel"> + <x>-1</x> + <y>-1</y> + </hint> + <hint type="destinationlabel"> + <x>333</x> + <y>210</y> + </hint> + </hints> + </connection> + <connection> + <sender>actionStartCamera</sender> + <signal>triggered()</signal> + <receiver>Camera</receiver> + <slot>startCamera()</slot> + <hints> + <hint type="sourcelabel"> + <x>-1</x> + <y>-1</y> + </hint> + <hint type="destinationlabel"> + <x>333</x> + <y>210</y> + </hint> + </hints> + </connection> + <connection> + <sender>actionStopCamera</sender> + <signal>triggered()</signal> + <receiver>Camera</receiver> + <slot>stopCamera()</slot> + <hints> + <hint type="sourcelabel"> + <x>-1</x> + <y>-1</y> + </hint> + <hint type="destinationlabel"> + <x>333</x> + <y>210</y> + </hint> + </hints> + </connection> + </connections> + <slots> + <slot>record()</slot> + <slot>pause()</slot> + <slot>stop()</slot> + <slot>enablePreview(bool)</slot> + <slot>configureCaptureSettings()</slot> + <slot>takeImage()</slot> + <slot>startCamera()</slot> + <slot>toggleLock()</slot> + <slot>setMuted(bool)</slot> + <slot>stopCamera()</slot> + <slot>setExposureCompensation(int)</slot> + </slots> +</ui> diff --git a/examples/multimedia/camera/camera_mobile.ui b/examples/multimedia/camera/camera_mobile.ui new file mode 100644 index 000000000..7f269b17b --- /dev/null +++ b/examples/multimedia/camera/camera_mobile.ui @@ -0,0 +1,504 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>Camera</class> + <widget class="QMainWindow" name="Camera"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>668</width> + <height>429</height> + </rect> + </property> + <property name="windowTitle"> + <string>Camera</string> + </property> + <widget class="QWidget" name="centralwidget"> + <layout class="QGridLayout" name="gridLayout_3"> + <item row="1" column="1" colspan="2"> + <widget class="QTabWidget" name="captureWidget"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="tab_2"> + <attribute name="title"> + <string>Image</string> + </attribute> + <layout class="QGridLayout" name="gridLayout"> + <item row="4" column="0"> + <widget class="QSlider" name="exposureCompensation"> + <property name="minimum"> + <number>-4</number> + </property> + <property name="maximum"> + <number>4</number> + </property> + <property name="pageStep"> + <number>2</number> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="tickPosition"> + <enum>QSlider::TicksAbove</enum> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="label"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Exposure Compensation:</string> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QPushButton" name="takeImageButton"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Capture Photo</string> + </property> + <property name="icon"> + <iconset> + <normaloff>:/images/shutter.svg</normaloff>:/images/shutter.svg</iconset> + </property> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="tab"> + <attribute name="title"> + <string>Video</string> + </attribute> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="0" column="0"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QPushButton" name="recordButton"> + <property name="text"> + <string>Record</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="pauseButton"> + <property name="text"> + <string>Pause</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="stopButton"> + <property name="text"> + <string>Stop</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>10</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="muteButton"> + <property name="text"> + <string>Mute</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="metaDataButton"> + <property name="text"> + <string>Set metadata</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </item> + </layout> + </widget> + </widget> + </item> + <item row="0" column="2"> + <widget class="QStackedWidget" name="stackedWidget"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>1</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="palette"> + <palette> + <active> + <colorrole role="Base"> + <brush brushstyle="SolidPattern"> + <color alpha="255"> + <red>255</red> + <green>255</green> + <blue>255</blue> + </color> + </brush> + </colorrole> + <colorrole role="Window"> + <brush brushstyle="SolidPattern"> + <color alpha="255"> + <red>145</red> + <green>145</green> + <blue>145</blue> + </color> + </brush> + </colorrole> + </active> + <inactive> + <colorrole role="Base"> + <brush brushstyle="SolidPattern"> + <color alpha="255"> + <red>255</red> + <green>255</green> + <blue>255</blue> + </color> + </brush> + </colorrole> + <colorrole role="Window"> + <brush brushstyle="SolidPattern"> + <color alpha="255"> + <red>145</red> + <green>145</green> + <blue>145</blue> + </color> + </brush> + </colorrole> + </inactive> + <disabled> + <colorrole role="Base"> + <brush brushstyle="SolidPattern"> + <color alpha="255"> + <red>145</red> + <green>145</green> + <blue>145</blue> + </color> + </brush> + </colorrole> + <colorrole role="Window"> + <brush brushstyle="SolidPattern"> + <color alpha="255"> + <red>145</red> + <green>145</green> + <blue>145</blue> + </color> + </brush> + </colorrole> + </disabled> + </palette> + </property> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="viewfinderPage"> + <layout class="QGridLayout" name="gridLayout_5"> + <item row="0" column="0"> + <widget class="QVideoWidget" name="viewfinder" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="previewPage"> + <layout class="QGridLayout" name="gridLayout_4"> + <item row="0" column="0"> + <widget class="QLabel" name="lastImagePreviewLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="frameShape"> + <enum>QFrame::Box</enum> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <widget class="QMenuBar" name="menubar"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>668</width> + <height>22</height> + </rect> + </property> + <widget class="QMenu" name="menuFile"> + <property name="title"> + <string>File</string> + </property> + <addaction name="actionStartCamera"/> + <addaction name="actionStopCamera"/> + <addaction name="separator"/> + <addaction name="actionSettings"/> + <addaction name="separator"/> + <addaction name="actionExit"/> + </widget> + <widget class="QMenu" name="menuDevices"> + <property name="title"> + <string>Devices</string> + </property> + </widget> + <addaction name="menuFile"/> + <addaction name="menuDevices"/> + </widget> + <widget class="QStatusBar" name="statusbar"/> + <action name="actionExit"> + <property name="text"> + <string>Close</string> + </property> + </action> + <action name="actionStartCamera"> + <property name="text"> + <string>Start Camera</string> + </property> + </action> + <action name="actionStopCamera"> + <property name="text"> + <string>Stop Camera</string> + </property> + </action> + <action name="actionSettings"> + <property name="text"> + <string>Change Settings</string> + </property> + </action> + </widget> + <customwidgets> + <customwidget> + <class>QVideoWidget</class> + <extends>QWidget</extends> + <header>qvideowidget.h</header> + <container>1</container> + </customwidget> + </customwidgets> + <resources/> + <connections> + <connection> + <sender>recordButton</sender> + <signal>clicked()</signal> + <receiver>Camera</receiver> + <slot>record()</slot> + <hints> + <hint type="sourcelabel"> + <x>647</x> + <y>149</y> + </hint> + <hint type="destinationlabel"> + <x>61</x> + <y>238</y> + </hint> + </hints> + </connection> + <connection> + <sender>stopButton</sender> + <signal>clicked()</signal> + <receiver>Camera</receiver> + <slot>stop()</slot> + <hints> + <hint type="sourcelabel"> + <x>647</x> + <y>225</y> + </hint> + <hint type="destinationlabel"> + <x>140</x> + <y>236</y> + </hint> + </hints> + </connection> + <connection> + <sender>pauseButton</sender> + <signal>clicked()</signal> + <receiver>Camera</receiver> + <slot>pause()</slot> + <hints> + <hint type="sourcelabel"> + <x>647</x> + <y>187</y> + </hint> + <hint type="destinationlabel"> + <x>234</x> + <y>237</y> + </hint> + </hints> + </connection> + <connection> + <sender>actionExit</sender> + <signal>triggered()</signal> + <receiver>Camera</receiver> + <slot>close()</slot> + <hints> + <hint type="sourcelabel"> + <x>-1</x> + <y>-1</y> + </hint> + <hint type="destinationlabel"> + <x>154</x> + <y>130</y> + </hint> + </hints> + </connection> + <connection> + <sender>takeImageButton</sender> + <signal>clicked()</signal> + <receiver>Camera</receiver> + <slot>takeImage()</slot> + <hints> + <hint type="sourcelabel"> + <x>625</x> + <y>132</y> + </hint> + <hint type="destinationlabel"> + <x>603</x> + <y>169</y> + </hint> + </hints> + </connection> + <connection> + <sender>muteButton</sender> + <signal>toggled(bool)</signal> + <receiver>Camera</receiver> + <slot>setMuted(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>647</x> + <y>377</y> + </hint> + <hint type="destinationlabel"> + <x>5</x> + <y>280</y> + </hint> + </hints> + </connection> + <connection> + <sender>exposureCompensation</sender> + <signal>valueChanged(int)</signal> + <receiver>Camera</receiver> + <slot>setExposureCompensation(int)</slot> + <hints> + <hint type="sourcelabel"> + <x>559</x> + <y>367</y> + </hint> + <hint type="destinationlabel"> + <x>665</x> + <y>365</y> + </hint> + </hints> + </connection> + <connection> + <sender>actionSettings</sender> + <signal>triggered()</signal> + <receiver>Camera</receiver> + <slot>configureCaptureSettings()</slot> + <hints> + <hint type="sourcelabel"> + <x>-1</x> + <y>-1</y> + </hint> + <hint type="destinationlabel"> + <x>333</x> + <y>210</y> + </hint> + </hints> + </connection> + <connection> + <sender>actionStartCamera</sender> + <signal>triggered()</signal> + <receiver>Camera</receiver> + <slot>startCamera()</slot> + <hints> + <hint type="sourcelabel"> + <x>-1</x> + <y>-1</y> + </hint> + <hint type="destinationlabel"> + <x>333</x> + <y>210</y> + </hint> + </hints> + </connection> + <connection> + <sender>actionStopCamera</sender> + <signal>triggered()</signal> + <receiver>Camera</receiver> + <slot>stopCamera()</slot> + <hints> + <hint type="sourcelabel"> + <x>-1</x> + <y>-1</y> + </hint> + <hint type="destinationlabel"> + <x>333</x> + <y>210</y> + </hint> + </hints> + </connection> + </connections> + <slots> + <slot>record()</slot> + <slot>pause()</slot> + <slot>stop()</slot> + <slot>enablePreview(bool)</slot> + <slot>configureCaptureSettings()</slot> + <slot>takeImage()</slot> + <slot>startCamera()</slot> + <slot>toggleLock()</slot> + <slot>setMuted(bool)</slot> + <slot>stopCamera()</slot> + <slot>setExposureCompensation(int)</slot> + </slots> +</ui> diff --git a/examples/multimedia/camera/doc/images/camera-example.png b/examples/multimedia/camera/doc/images/camera-example.png Binary files differnew file mode 100644 index 000000000..12e1b5728 --- /dev/null +++ b/examples/multimedia/camera/doc/images/camera-example.png diff --git a/examples/multimedia/camera/doc/src/camera.qdoc b/examples/multimedia/camera/doc/src/camera.qdoc new file mode 100644 index 000000000..7a3b88d1b --- /dev/null +++ b/examples/multimedia/camera/doc/src/camera.qdoc @@ -0,0 +1,58 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only +/*! + +\example camera +\title Camera Example +\ingroup multimedia_examples +\ingroup video_examples +\ingroup camera_examples +\meta {tag} {widgets} +\brief Shows how to capture a still image or record video. +or video. + +The Camera Example demonstrates how you can use \l{Qt Multimedia} to implement +some basic Camera functionality to take still images and record video clips +with audio. + +\include examples-run.qdocinc + +The example implements a \c Camera class that acts as our camera interface. It +has a user interface, control functions, setting values and a means of defining +the location where the image or video clip is to be saved. It will also store +the image and video settings. + +The Camera class uses: +\list + \li An instance of \l {QCamera}, the API class interface to the hardware. + \li An instance of \l {QImageCapture} to take still images. + \li An instance of \l {QMediaRecorder} to record video. It also contains + the user interface object. +\endlist + +The Camera constructor does some basic initialization: +\list + \li The user interface is initialized. + \li UI signals are connected to slots that react to the triggering event. +\endlist +However, most of the work is done when the \e{setCamera()} function is called, +passing in a \l QCameraDevice. + +\e{setCamera()} sets up various connections between the user interface and the +functionality of the Camera class using signals and slots. It also instantiates +and initializes the \l {QCamera}, \l {QImageCapture}, and \l {QMediaRecorder} +objects mentioned above. The still and video recording visual tabs are enabled +and finally the \l {QCamera::start}{start()} function of the \l{QCamera} +object is called. + +Now that the camera is ready for user commands it waits for a suitable event. +Such an event can be a key press of either the \l {Qt::Key_CameraFocus} or +\l {Qt::Key_Camera} buttons on the application window. Camera focus will +simply display the preview and lock the camera settings. \c Key_Camera will +either call \e{takeImage()} if doing an image capture, or call +\c record() or \c stop() (if already recording) on the QMediaRecorder instance +when recording video. + +\image camera-example.png + +*/ diff --git a/examples/multimedia/camera/images/shutter.svg b/examples/multimedia/camera/images/shutter.svg new file mode 100644 index 000000000..18493361d --- /dev/null +++ b/examples/multimedia/camera/images/shutter.svg @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 20.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 23.3 19.4" style="enable-background:new 0 0 23.3 19.4;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:none;} +</style> +<g> + <path class="st0" d="M6.2,4.8H2.4c-0.2,0-0.1-0.1-0.1,0.1V17c0,0.2-0.1,0.8,0.1,0.8h3.9V4.8z"/> + <circle class="st0" cx="14" cy="11" r="4.5"/> + <path class="st0" d="M20.9,4.8h-1.8c-0.3,0-0.6-0.4-0.8-0.6l-1.7-2.4h-5.3L9.7,4.2C9.5,4.4,9.2,4.8,8.9,4.8H7.2v13h13.7 + c0.2,0,0.3-0.6,0.3-0.8V4.9C21.2,4.7,21.1,4.8,20.9,4.8z M14,16.4c-3,0-5.5-2.4-5.5-5.5c0-3,2.4-5.5,5.5-5.5c3,0,5.5,2.4,5.5,5.5 + C19.5,14,17,16.4,14,16.4z"/> + <path d="M14,5.5C11,5.5,8.6,8,8.6,11c0,3,2.4,5.5,5.5,5.5c3,0,5.5-2.4,5.5-5.5C19.5,8,17,5.5,14,5.5z M14,15.4 + c-2.5,0-4.5-2-4.5-4.5c0-2.5,2-4.5,4.5-4.5c2.5,0,4.5,2,4.5,4.5C18.5,13.4,16.5,15.4,14,15.4z"/> + <path d="M20.9,2.8h-1.3l-1.7-2.4c-0.2-0.2-0.5-0.6-0.8-0.6h-6.3c-0.3,0-0.6,0.4-0.8,0.6L8.4,2.8h-6c-1.3,0-2.1,0.8-2.1,2.1V17 + c0,1.3,0.8,2.8,2.1,2.8h18.5c1.3,0,2.3-1.5,2.3-2.8V4.9C23.2,3.6,22.2,2.8,20.9,2.8z M2.2,17V4.9c0-0.2-0.1-0.1,0.1-0.1h3.9v13H2.4 + C2.2,17.8,2.2,17.2,2.2,17z M21.2,17c0,0.2-0.1,0.8-0.3,0.8H7.2v-13h1.7c0.3,0,0.6-0.4,0.8-0.6l1.7-2.4h5.3l1.7,2.4 + c0.2,0.2,0.5,0.6,0.8,0.6h1.8c0.2,0,0.3-0.1,0.3,0.1V17z"/> +</g> +</svg> diff --git a/examples/multimedia/camera/imagesettings.cpp b/examples/multimedia/camera/imagesettings.cpp new file mode 100644 index 000000000..a107cc62d --- /dev/null +++ b/examples/multimedia/camera/imagesettings.cpp @@ -0,0 +1,83 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "imagesettings.h" +#include "ui_imagesettings.h" + +#include <QComboBox> +#include <QDebug> +#include <QImageCapture> +#include <QCamera> +#include <QMediaCaptureSession> + +ImageSettings::ImageSettings(QImageCapture *imageCapture, QWidget *parent) : + QDialog(parent), + ui(new Ui::ImageSettingsUi), + imagecapture(imageCapture) +{ + ui->setupUi(this); + + //image codecs + ui->imageCodecBox->addItem(tr("Default image format"), QVariant(QString())); + const auto supportedImageFormats = QImageCapture::supportedFormats(); + for (const auto &f : supportedImageFormats) { + QString description = QImageCapture::fileFormatDescription(f); + ui->imageCodecBox->addItem(QImageCapture::fileFormatName(f) + ": " + description, QVariant::fromValue(f)); + } + + ui->imageQualitySlider->setRange(0, int(QImageCapture::VeryHighQuality)); + + ui->imageResolutionBox->addItem(tr("Default Resolution")); + const QList<QSize> supportedResolutions = imagecapture->captureSession()->camera()->cameraDevice().photoResolutions(); + for (const QSize &resolution : supportedResolutions) { + ui->imageResolutionBox->addItem(QString("%1x%2").arg(resolution.width()).arg(resolution.height()), + QVariant(resolution)); + } + + selectComboBoxItem(ui->imageCodecBox, QVariant::fromValue(imagecapture->fileFormat())); + selectComboBoxItem(ui->imageResolutionBox, QVariant(imagecapture->resolution())); + ui->imageQualitySlider->setValue(imagecapture->quality()); +} + +ImageSettings::~ImageSettings() +{ + delete ui; +} + +void ImageSettings::changeEvent(QEvent *e) +{ + QDialog::changeEvent(e); + switch (e->type()) { + case QEvent::LanguageChange: + ui->retranslateUi(this); + break; + default: + break; + } +} + +void ImageSettings::applyImageSettings() const +{ + imagecapture->setFileFormat(boxValue(ui->imageCodecBox).value<QImageCapture::FileFormat>()); + imagecapture->setQuality(QImageCapture::Quality(ui->imageQualitySlider->value())); + imagecapture->setResolution(boxValue(ui->imageResolutionBox).toSize()); +} + +QVariant ImageSettings::boxValue(const QComboBox *box) const +{ + int idx = box->currentIndex(); + if (idx == -1) + return QVariant(); + + return box->itemData(idx); +} + +void ImageSettings::selectComboBoxItem(QComboBox *box, const QVariant &value) +{ + for (int i = 0; i < box->count(); ++i) { + if (box->itemData(i) == value) { + box->setCurrentIndex(i); + break; + } + } +} diff --git a/examples/multimedia/camera/imagesettings.h b/examples/multimedia/camera/imagesettings.h new file mode 100644 index 000000000..13bd6dc4a --- /dev/null +++ b/examples/multimedia/camera/imagesettings.h @@ -0,0 +1,39 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef IMAGESETTINGS_H +#define IMAGESETTINGS_H + +#include <QDialog> + +QT_BEGIN_NAMESPACE +class QComboBox; +class QImageCapture; +namespace Ui { class ImageSettingsUi; } +QT_END_NAMESPACE + +class ImageSettings : public QDialog +{ + Q_OBJECT + +public: + explicit ImageSettings(QImageCapture *imageCapture, QWidget *parent = nullptr); + ~ImageSettings(); + + void applyImageSettings() const; + + QString format() const; + void setFormat(const QString &format); + +protected: + void changeEvent(QEvent *e) override; + +private: + QVariant boxValue(const QComboBox *box) const; + void selectComboBoxItem(QComboBox *box, const QVariant &value); + + Ui::ImageSettingsUi *ui; + QImageCapture *imagecapture; +}; + +#endif // IMAGESETTINGS_H diff --git a/examples/multimedia/camera/imagesettings.ui b/examples/multimedia/camera/imagesettings.ui new file mode 100644 index 000000000..8c59ca01d --- /dev/null +++ b/examples/multimedia/camera/imagesettings.ui @@ -0,0 +1,123 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ImageSettingsUi</class> + <widget class="QDialog" name="ImageSettingsUi"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>332</width> + <height>270</height> + </rect> + </property> + <property name="windowTitle"> + <string>Image Settings</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QGroupBox" name="groupBox_2"> + <property name="title"> + <string>Image</string> + </property> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="0" column="0" colspan="2"> + <widget class="QLabel" name="label_8"> + <property name="text"> + <string>Resolution:</string> + </property> + </widget> + </item> + <item row="1" column="0" colspan="2"> + <widget class="QComboBox" name="imageResolutionBox"/> + </item> + <item row="2" column="0" colspan="2"> + <widget class="QLabel" name="label_6"> + <property name="text"> + <string>Image Format:</string> + </property> + </widget> + </item> + <item row="3" column="0" colspan="2"> + <widget class="QComboBox" name="imageCodecBox"/> + </item> + <item row="4" column="0"> + <widget class="QLabel" name="label_7"> + <property name="text"> + <string>Quality:</string> + </property> + </widget> + </item> + <item row="4" column="1"> + <widget class="QSlider" name="imageQualitySlider"> + <property name="maximum"> + <number>4</number> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item row="1" column="0"> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>14</height> + </size> + </property> + </spacer> + </item> + <item row="2" column="0"> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>ImageSettingsUi</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>322</x> + <y>272</y> + </hint> + <hint type="destinationlabel"> + <x>44</x> + <y>230</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>ImageSettingsUi</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>405</x> + <y>262</y> + </hint> + <hint type="destinationlabel"> + <x>364</x> + <y>227</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/examples/multimedia/camera/ios/Info.plist.in b/examples/multimedia/camera/ios/Info.plist.in new file mode 100644 index 000000000..6a6b8db11 --- /dev/null +++ b/examples/multimedia/camera/ios/Info.plist.in @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + + <key>CFBundleName</key> + <string>${MACOSX_BUNDLE_BUNDLE_NAME}</string> + <key>CFBundleIdentifier</key> + <string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string> + <key>CFBundleExecutable</key> + <string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string> + + <key>CFBundleVersion</key> + <string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string> + <key>CFBundleShortVersionString</key> + <string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string> + + <key>CFBundleGetInfoString</key> + <string>${MACOSX_BUNDLE_INFO_STRING}</string> + <key>NSHumanReadableCopyright</key> + <string>${MACOSX_BUNDLE_COPYRIGHT}</string> + + <key>CFBundleIconFile</key> + <string>${MACOSX_BUNDLE_ICON_FILE}</string> + + <key>CFBundleDevelopmentRegion</key> + <string>English</string> + + <key>LSRequiresIPhoneOS</key> + <true/> + + <key>UISupportedInterfaceOrientations</key> + <array> + <string>UIInterfaceOrientationPortrait</string> + <string>UIInterfaceOrientationPortraitUpsideDown</string> + <string>UIInterfaceOrientationLandscapeLeft</string> + <string>UIInterfaceOrientationLandscapeRight</string> + </array> + + <key>NSCameraUsageDescription</key> + <string>Qt Multimedia Example</string> + <key>NSMicrophoneUsageDescription</key> + <string>Qt Multimedia Example</string> +</dict> +</plist> + diff --git a/examples/multimedia/camera/macos/Info.plist.in b/examples/multimedia/camera/macos/Info.plist.in new file mode 100644 index 000000000..ae2d945f1 --- /dev/null +++ b/examples/multimedia/camera/macos/Info.plist.in @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + + <key>CFBundleName</key> + <string>${MACOSX_BUNDLE_BUNDLE_NAME}</string> + <key>CFBundleIdentifier</key> + <string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string> + <key>CFBundleExecutable</key> + <string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string> + + <key>CFBundleVersion</key> + <string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string> + <key>CFBundleShortVersionString</key> + <string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string> + <key>CFBundleLongVersionString</key> + <string>${MACOSX_BUNDLE_LONG_VERSION_STRING}</string> + + <key>LSMinimumSystemVersion</key> + <string>${CMAKE_OSX_DEPLOYMENT_TARGET}</string> + + <key>CFBundleGetInfoString</key> + <string>${MACOSX_BUNDLE_INFO_STRING}</string> + <key>NSHumanReadableCopyright</key> + <string>${MACOSX_BUNDLE_COPYRIGHT}</string> + + <key>CFBundleIconFile</key> + <string>${MACOSX_BUNDLE_ICON_FILE}</string> + + <key>CFBundleDevelopmentRegion</key> + <string>English</string> + + <key>NSPrincipalClass</key> + <string>NSApplication</string> + + <key>NSCameraUsageDescription</key> + <string>Qt Multimedia Example</string> + <key>NSMicrophoneUsageDescription</key> + <string>Qt Multimedia Example</string> + + <key>NSSupportsAutomaticGraphicsSwitching</key> + <true/> +</dict> +</plist> diff --git a/examples/multimedia/camera/main.cpp b/examples/multimedia/camera/main.cpp new file mode 100644 index 000000000..50b411e4e --- /dev/null +++ b/examples/multimedia/camera/main.cpp @@ -0,0 +1,16 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "camera.h" + +#include <QtWidgets> + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + Camera camera; + camera.show(); + + return app.exec(); +}; diff --git a/examples/multimedia/camera/metadatadialog.cpp b/examples/multimedia/camera/metadatadialog.cpp new file mode 100644 index 000000000..096217014 --- /dev/null +++ b/examples/multimedia/camera/metadatadialog.cpp @@ -0,0 +1,80 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "metadatadialog.h" +#include "camera.h" + +#include <QtWidgets> +#include <QFormLayout> +#include <QMediaMetaData> + +MetaDataDialog::MetaDataDialog(QWidget *parent) + : QDialog(parent) +{ + QFormLayout *metaDataLayout = new QFormLayout; + for (int key = 0; key < QMediaMetaData::NumMetaData; key++) { + QString label = QMediaMetaData::metaDataKeyToString(static_cast<QMediaMetaData::Key>(key)); + m_metaDataFields[key] = new QLineEdit; + if (key == QMediaMetaData::ThumbnailImage) { + QPushButton *openThumbnail = new QPushButton(tr("Open")); + connect(openThumbnail, &QPushButton::clicked, this, &MetaDataDialog::openThumbnailImage); + QHBoxLayout *layout = new QHBoxLayout; + layout->addWidget(m_metaDataFields[key]); + layout->addWidget(openThumbnail); + metaDataLayout->addRow(label, layout); + } + else if (key == QMediaMetaData::CoverArtImage) { + QPushButton *openCoverArt = new QPushButton(tr("Open")); + connect(openCoverArt, &QPushButton::clicked, this, &MetaDataDialog::openCoverArtImage); + QHBoxLayout *layout = new QHBoxLayout; + layout->addWidget(m_metaDataFields[key]); + layout->addWidget(openCoverArt); + metaDataLayout->addRow(label, layout); + } + else { + if (key == QMediaMetaData::Title) + m_metaDataFields[key]->setText(tr("Qt Camera Example")); + else if (key == QMediaMetaData::Author) + m_metaDataFields[key]->setText(tr("The Qt Company")); + else if (key == QMediaMetaData::Date) + m_metaDataFields[key]->setText(QDateTime::currentDateTime().toString()); + else if (key == QMediaMetaData::Date) + m_metaDataFields[key]->setText(QDate::currentDate().toString()); + metaDataLayout->addRow(label, m_metaDataFields[key]); + } + } + + QWidget *viewport = new QWidget; + viewport->setLayout(metaDataLayout); + QScrollArea *scrollArea = new QScrollArea; + scrollArea->setWidget(viewport); + QVBoxLayout *dialogLayout = new QVBoxLayout(); + this->setLayout(dialogLayout); + this->layout()->addWidget(scrollArea); + + auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok + | QDialogButtonBox::Cancel); + this->layout()->addWidget(buttonBox); + + this->setWindowTitle(tr("Set Metadata")); + this->resize(400, 300); + + connect(buttonBox, &QDialogButtonBox::accepted, this, &MetaDataDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, this, &MetaDataDialog::reject); +} + +void MetaDataDialog::openThumbnailImage() +{ + QString fileName = QFileDialog::getOpenFileName(this, + tr("Open Image"), QDir::currentPath(), tr("Image Files (*.png *.jpg *.bmp)")); + if (!fileName.isEmpty()) + m_metaDataFields[QMediaMetaData::ThumbnailImage]->setText(fileName); +} + +void MetaDataDialog::openCoverArtImage() +{ + QString fileName = QFileDialog::getOpenFileName(this, + tr("Open Image"), QDir::currentPath(), tr("Image Files (*.png *.jpg *.bmp)")); + if (!fileName.isEmpty()) + m_metaDataFields[QMediaMetaData::CoverArtImage]->setText(fileName); +} diff --git a/examples/multimedia/camera/metadatadialog.h b/examples/multimedia/camera/metadatadialog.h new file mode 100644 index 000000000..5bb5a4b0b --- /dev/null +++ b/examples/multimedia/camera/metadatadialog.h @@ -0,0 +1,31 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef DIALOG_H +#define DIALOG_H + +#include <QDialog> +#include <QMediaMetaData> + +QT_BEGIN_NAMESPACE +class QLabel; +class QLineEdit; +QT_END_NAMESPACE + +//! [0] +class MetaDataDialog : public QDialog +{ + Q_OBJECT + +public: + explicit MetaDataDialog(QWidget *parent = nullptr); + + QLineEdit *m_metaDataFields[QMediaMetaData::NumMetaData] = {}; + +private slots: + void openThumbnailImage(); + void openCoverArtImage(); +}; +//! [0] + +#endif diff --git a/examples/multimedia/camera/videosettings.cpp b/examples/multimedia/camera/videosettings.cpp new file mode 100644 index 000000000..b2c62bafc --- /dev/null +++ b/examples/multimedia/camera/videosettings.cpp @@ -0,0 +1,201 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "videosettings.h" +#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) +#include "ui_videosettings_mobile.h" +#else +#include "ui_videosettings.h" +#endif + +#include <QComboBox> +#include <QSpinBox> +#include <QDebug> +#include <QMediaRecorder> +#include <QMediaFormat> +#include <QAudioDevice> +#include <QMediaCaptureSession> +#include <QCameraDevice> +#include <QCamera> +#include <QAudioInput> + +QString toFormattedString(const QCameraFormat &cameraFormat) +{ + QString string; + const auto &separator = QStringLiteral(" "); + + string.append(QVideoFrameFormat::pixelFormatToString(cameraFormat.pixelFormat())); + string.append(separator); + + string.append(QString::number(cameraFormat.resolution().width())); + string.append(QStringLiteral("x")); + string.append(QString::number(cameraFormat.resolution().height())); + string.append(separator); + + string.append(QString::number(cameraFormat.minFrameRate())); + string.append(QStringLiteral("-")); + string.append(QString::number(cameraFormat.maxFrameRate())); + string.append(QStringLiteral("FPS")); + + return string; +} + +VideoSettings::VideoSettings(QMediaRecorder *mediaRecorder, QWidget *parent) + : QDialog(parent), + ui(new Ui::VideoSettingsUi), + mediaRecorder(mediaRecorder) +{ + ui->setupUi(this); + + //sample rate: + auto audioDevice = mediaRecorder->captureSession()->audioInput()->device(); + ui->audioSampleRateBox->setRange(audioDevice.minimumSampleRate(), + audioDevice.maximumSampleRate()); + + // camera format + ui->videoFormatBox->addItem(tr("Default camera format")); + + const QList<QCameraFormat> videoFormats = + mediaRecorder->captureSession()->camera()->cameraDevice().videoFormats(); + + for (const QCameraFormat &format : videoFormats) { + ui->videoFormatBox->addItem(toFormattedString(format), QVariant::fromValue(format)); + } + + connect(ui->videoFormatBox, &QComboBox::currentIndexChanged, [this](int /*index*/) { + const auto &cameraFormat = boxValue(ui->videoFormatBox).value<QCameraFormat>(); + ui->fpsSlider->setRange(cameraFormat.minFrameRate(), cameraFormat.maxFrameRate()); + ui->fpsSpinBox->setRange(cameraFormat.minFrameRate(), cameraFormat.maxFrameRate()); + }); + + auto currentCameraFormat = mediaRecorder->captureSession()->camera()->cameraFormat(); + ui->fpsSlider->setRange(currentCameraFormat.minFrameRate(), currentCameraFormat.maxFrameRate()); + ui->fpsSpinBox->setRange(currentCameraFormat.minFrameRate(), + currentCameraFormat.maxFrameRate()); + + connect(ui->fpsSlider, &QSlider::valueChanged, ui->fpsSpinBox, &QSpinBox::setValue); + connect(ui->fpsSpinBox, &QSpinBox::valueChanged, ui->fpsSlider, &QSlider::setValue); + + updateFormatsAndCodecs(); + connect(ui->audioCodecBox, &QComboBox::currentIndexChanged, this, &VideoSettings::updateFormatsAndCodecs); + connect(ui->videoCodecBox, &QComboBox::currentIndexChanged, this, &VideoSettings::updateFormatsAndCodecs); + connect(ui->containerFormatBox, &QComboBox::currentIndexChanged, this, &VideoSettings::updateFormatsAndCodecs); + + ui->qualitySlider->setRange(0, int(QMediaRecorder::VeryHighQuality)); + + QMediaFormat format = mediaRecorder->mediaFormat(); + selectComboBoxItem(ui->containerFormatBox, QVariant::fromValue(format.fileFormat())); + selectComboBoxItem(ui->audioCodecBox, QVariant::fromValue(format.audioCodec())); + selectComboBoxItem(ui->videoCodecBox, QVariant::fromValue(format.videoCodec())); + + ui->qualitySlider->setValue(mediaRecorder->quality()); + ui->audioSampleRateBox->setValue(mediaRecorder->audioSampleRate()); + selectComboBoxItem( + ui->videoFormatBox, + QVariant::fromValue(mediaRecorder->captureSession()->camera()->cameraFormat())); + + ui->fpsSlider->setValue(mediaRecorder->videoFrameRate()); + ui->fpsSpinBox->setValue(mediaRecorder->videoFrameRate()); +} + +VideoSettings::~VideoSettings() +{ + delete ui; +} + +void VideoSettings::changeEvent(QEvent *e) +{ + QDialog::changeEvent(e); + switch (e->type()) { + case QEvent::LanguageChange: + ui->retranslateUi(this); + break; + default: + break; + } +} + +void VideoSettings::applySettings() +{ + QMediaFormat format; + format.setFileFormat(boxValue(ui->containerFormatBox).value<QMediaFormat::FileFormat>()); + format.setAudioCodec(boxValue(ui->audioCodecBox).value<QMediaFormat::AudioCodec>()); + format.setVideoCodec(boxValue(ui->videoCodecBox).value<QMediaFormat::VideoCodec>()); + + mediaRecorder->setMediaFormat(format); + mediaRecorder->setQuality(QMediaRecorder::Quality(ui->qualitySlider->value())); + mediaRecorder->setAudioSampleRate(ui->audioSampleRateBox->value()); + + const auto &cameraFormat = boxValue(ui->videoFormatBox).value<QCameraFormat>(); + mediaRecorder->setVideoResolution(cameraFormat.resolution()); + mediaRecorder->setVideoFrameRate(ui->fpsSlider->value()); + + mediaRecorder->captureSession()->camera()->setCameraFormat(cameraFormat); +} + +void VideoSettings::updateFormatsAndCodecs() +{ + if (m_updatingFormats) + return; + m_updatingFormats = true; + + QMediaFormat format; + if (ui->containerFormatBox->count()) + format.setFileFormat(boxValue(ui->containerFormatBox).value<QMediaFormat::FileFormat>()); + if (ui->audioCodecBox->count()) + format.setAudioCodec(boxValue(ui->audioCodecBox).value<QMediaFormat::AudioCodec>()); + if (ui->videoCodecBox->count()) + format.setVideoCodec(boxValue(ui->videoCodecBox).value<QMediaFormat::VideoCodec>()); + + int currentIndex = 0; + ui->audioCodecBox->clear(); + ui->audioCodecBox->addItem(tr("Default audio codec"), QVariant::fromValue(QMediaFormat::AudioCodec::Unspecified)); + for (auto codec : format.supportedAudioCodecs(QMediaFormat::Encode)) { + if (codec == format.audioCodec()) + currentIndex = ui->audioCodecBox->count(); + ui->audioCodecBox->addItem(QMediaFormat::audioCodecDescription(codec), QVariant::fromValue(codec)); + } + ui->audioCodecBox->setCurrentIndex(currentIndex); + + currentIndex = 0; + ui->videoCodecBox->clear(); + ui->videoCodecBox->addItem(tr("Default video codec"), QVariant::fromValue(QMediaFormat::VideoCodec::Unspecified)); + for (auto codec : format.supportedVideoCodecs(QMediaFormat::Encode)) { + if (codec == format.videoCodec()) + currentIndex = ui->videoCodecBox->count(); + ui->videoCodecBox->addItem(QMediaFormat::videoCodecDescription(codec), QVariant::fromValue(codec)); + } + ui->videoCodecBox->setCurrentIndex(currentIndex); + + currentIndex = 0; + ui->containerFormatBox->clear(); + ui->containerFormatBox->addItem(tr("Default file format"), QVariant::fromValue(QMediaFormat::UnspecifiedFormat)); + for (auto container : format.supportedFileFormats(QMediaFormat::Encode)) { + if (container == format.fileFormat()) + currentIndex = ui->containerFormatBox->count(); + ui->containerFormatBox->addItem(QMediaFormat::fileFormatDescription(container), QVariant::fromValue(container)); + } + ui->containerFormatBox->setCurrentIndex(currentIndex); + + m_updatingFormats = false; + +} + +QVariant VideoSettings::boxValue(const QComboBox *box) const +{ + int idx = box->currentIndex(); + if (idx == -1) + return QVariant(); + + return box->itemData(idx); +} + +void VideoSettings::selectComboBoxItem(QComboBox *box, const QVariant &value) +{ + for (int i = 0; i < box->count(); ++i) { + if (box->itemData(i) == value) { + box->setCurrentIndex(i); + break; + } + } +} diff --git a/examples/multimedia/camera/videosettings.h b/examples/multimedia/camera/videosettings.h new file mode 100644 index 000000000..2f356d90f --- /dev/null +++ b/examples/multimedia/camera/videosettings.h @@ -0,0 +1,38 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef VIDEOSETTINGS_H +#define VIDEOSETTINGS_H + +#include <QDialog> + +QT_BEGIN_NAMESPACE +class QComboBox; +class QMediaRecorder; +namespace Ui { class VideoSettingsUi; } +QT_END_NAMESPACE + +class VideoSettings : public QDialog +{ + Q_OBJECT + +public: + explicit VideoSettings(QMediaRecorder *mediaRecorder, QWidget *parent = nullptr); + ~VideoSettings(); + + void applySettings(); + void updateFormatsAndCodecs(); + +protected: + void changeEvent(QEvent *e) override; + +private: + QVariant boxValue(const QComboBox*) const; + void selectComboBoxItem(QComboBox *box, const QVariant &value); + + Ui::VideoSettingsUi *ui; + QMediaRecorder *mediaRecorder; + bool m_updatingFormats = false; +}; + +#endif // VIDEOSETTINGS_H diff --git a/examples/multimedia/camera/videosettings.ui b/examples/multimedia/camera/videosettings.ui new file mode 100644 index 000000000..3c1f71f11 --- /dev/null +++ b/examples/multimedia/camera/videosettings.ui @@ -0,0 +1,213 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>VideoSettingsUi</class> + <widget class="QDialog" name="VideoSettingsUi"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>686</width> + <height>499</height> + </rect> + </property> + <property name="windowTitle"> + <string>Video Settings</string> + </property> + <layout class="QGridLayout" name="gridLayout_3"> + <item row="4" column="1"> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QGroupBox" name="groupBox_2"> + <property name="title"> + <string>Video</string> + </property> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="0" column="0" colspan="2"> + <widget class="QLabel" name="label_8"> + <property name="text"> + <string>Camera Format</string> + </property> + </widget> + </item> + <item row="5" column="0" colspan="2"> + <widget class="QComboBox" name="videoCodecBox"/> + </item> + <item row="2" column="0" colspan="2"> + <widget class="QLabel" name="label_9"> + <property name="text"> + <string>Framerate:</string> + </property> + </widget> + </item> + <item row="4" column="0" colspan="2"> + <widget class="QLabel" name="label_6"> + <property name="text"> + <string>Video Codec:</string> + </property> + </widget> + </item> + <item row="1" column="0" colspan="2"> + <widget class="QComboBox" name="videoFormatBox"/> + </item> + <item row="3" column="0" colspan="2"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QSpinBox" name="fpsSpinBox"/> + </item> + <item> + <widget class="QSlider" name="fpsSlider"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item row="2" column="0"> + <widget class="QWidget" name="widget" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QGroupBox" name="groupBox_3"> + <property name="title"> + <string>Audio</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Audio Codec:</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="audioCodecBox"/> + </item> + <item> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>Sample Rate:</string> + </property> + </widget> + </item> + <item> + <widget class="QSpinBox" name="audioSampleRateBox"/> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox"> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Quality:</string> + </property> + </widget> + </item> + <item> + <widget class="QSlider" name="qualitySlider"> + <property name="maximum"> + <number>4</number> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>File Format:</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="containerFormatBox"/> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + </item> + <item row="3" column="0"> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>VideoSettingsUi</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>322</x> + <y>272</y> + </hint> + <hint type="destinationlabel"> + <x>44</x> + <y>230</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>VideoSettingsUi</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>405</x> + <y>262</y> + </hint> + <hint type="destinationlabel"> + <x>364</x> + <y>227</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/examples/multimedia/camera/videosettings_mobile.ui b/examples/multimedia/camera/videosettings_mobile.ui new file mode 100644 index 000000000..6584f07f9 --- /dev/null +++ b/examples/multimedia/camera/videosettings_mobile.ui @@ -0,0 +1,207 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>VideoSettingsUi</class> + <widget class="QDialog" name="VideoSettingsUi"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>329</width> + <height>591</height> + </rect> + </property> + <property name="windowTitle"> + <string>Video Settings</string> + </property> + <layout class="QGridLayout" name="gridLayout_3"> + <item row="2" column="0"> + <widget class="QWidget" name="widget" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QGroupBox" name="groupBox_3"> + <property name="title"> + <string>Audio</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Audio Codec:</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="audioCodecBox"/> + </item> + <item> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>Sample Rate:</string> + </property> + </widget> + </item> + <item> + <widget class="QSpinBox" name="audioSampleRateBox"/> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox"> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Quality:</string> + </property> + </widget> + </item> + <item> + <widget class="QSlider" name="qualitySlider"> + <property name="maximum"> + <number>4</number> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>File Format:</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="containerFormatBox"/> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + </item> + <item row="3" column="0"> + <widget class="QGroupBox" name="groupBox_2"> + <property name="title"> + <string>Video</string> + </property> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="2" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Frames per second:</string> + </property> + </widget> + </item> + <item row="6" column="0" colspan="2"> + <widget class="QComboBox" name="videoCodecBox"/> + </item> + <item row="0" column="0" colspan="2"> + <widget class="QLabel" name="label_8"> + <property name="text"> + <string>Camera Format:</string> + </property> + </widget> + </item> + <item row="5" column="0" colspan="2"> + <widget class="QLabel" name="label_6"> + <property name="text"> + <string>Video Codec:</string> + </property> + </widget> + </item> + <item row="1" column="0" colspan="2"> + <widget class="QComboBox" name="videoFormatBox"/> + </item> + <item row="7" column="0"> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + <item row="3" column="0"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QSpinBox" name="fpsSpinBox"> + <property name="minimum"> + <number>8</number> + </property> + <property name="maximum"> + <number>30</number> + </property> + </widget> + </item> + <item> + <widget class="QSlider" name="fpsSlider"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>VideoSettingsUi</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>322</x> + <y>272</y> + </hint> + <hint type="destinationlabel"> + <x>44</x> + <y>230</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>VideoSettingsUi</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>405</x> + <y>262</y> + </hint> + <hint type="destinationlabel"> + <x>364</x> + <y>227</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/examples/multimedia/multimedia.pro b/examples/multimedia/multimedia.pro index 1f737a76c..cf631bb2c 100644 --- a/examples/multimedia/multimedia.pro +++ b/examples/multimedia/multimedia.pro @@ -8,10 +8,14 @@ SUBDIRS += \ # These examples all need widgets for now (using creator templates that use widgets) qtHaveModule(widgets) { SUBDIRS += \ - spectrum \ - audiorecorder \ audiodevices \ - audiooutput + audiooutput \ + audiorecorder \ + camera \ + player \ + spectrum \ + videographicsitem \ + videowidget } qtHaveModule(quick) { 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 diff --git a/examples/multimedia/videographicsitem/CMakeLists.txt b/examples/multimedia/videographicsitem/CMakeLists.txt new file mode 100644 index 000000000..653627c25 --- /dev/null +++ b/examples/multimedia/videographicsitem/CMakeLists.txt @@ -0,0 +1,39 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +cmake_minimum_required(VERSION 3.16) +project(videographicsitem LANGUAGES CXX) + +set(CMAKE_AUTOMOC ON) + +if(NOT DEFINED INSTALL_EXAMPLESDIR) + set(INSTALL_EXAMPLESDIR "examples") +endif() + +set(INSTALL_EXAMPLEDIR "${INSTALL_EXAMPLESDIR}/multimedia/videographicsitem") + +find_package(Qt6 REQUIRED COMPONENTS Core Gui Multimedia MultimediaWidgets Widgets) + +qt_add_executable(videographicsitem + main.cpp + videoplayer.cpp videoplayer.h +) + +set_target_properties(videographicsitem PROPERTIES + WIN32_EXECUTABLE TRUE + MACOSX_BUNDLE TRUE +) + +target_link_libraries(videographicsitem PUBLIC + Qt::Core + Qt::Gui + Qt::Multimedia + Qt::MultimediaWidgets + Qt::Widgets +) + +install(TARGETS videographicsitem + RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}" + BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}" + LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}" +) diff --git a/examples/multimedia/videographicsitem/doc/images/video-videographicsitem.png b/examples/multimedia/videographicsitem/doc/images/video-videographicsitem.png Binary files differnew file mode 100644 index 000000000..e333c54a2 --- /dev/null +++ b/examples/multimedia/videographicsitem/doc/images/video-videographicsitem.png diff --git a/examples/multimedia/videographicsitem/doc/src/videographicsitem.qdoc b/examples/multimedia/videographicsitem/doc/src/videographicsitem.qdoc new file mode 100644 index 000000000..5dcb7143e --- /dev/null +++ b/examples/multimedia/videographicsitem/doc/src/videographicsitem.qdoc @@ -0,0 +1,19 @@ +// Copyright (C) 2015 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only + +/*! + \example videographicsitem + \title Video Graphics Item Example + \ingroup multimedia_examples + \brief Streaming video on a graphics scene. + \meta {tag} {widgets} + + \e{Video Graphics Item} demonstrates how to implement a QGraphicsItem that + displays video on a graphics scene using QVideoSink. + + \image video-videographicsitem.png + + \sa {Video Widget Example} + + \include examples-run.qdocinc +*/ diff --git a/examples/multimedia/videographicsitem/main.cpp b/examples/multimedia/videographicsitem/main.cpp new file mode 100644 index 000000000..85d1ec92b --- /dev/null +++ b/examples/multimedia/videographicsitem/main.cpp @@ -0,0 +1,39 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "videoplayer.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 QGraphicsView Example"); + parser.addHelpOption(); + parser.addVersionOption(); + parser.addPositionalArgument("url", "The URL to open."); + parser.process(app); + + VideoPlayer player; + + if (!parser.positionalArguments().isEmpty() && player.isPlayerAvailable()) { + const QUrl url = + QUrl::fromUserInput(parser.positionalArguments().constFirst(), + QDir::currentPath(), QUrl::AssumeLocalFile); + player.load(url); + } + + player.show(); + + return app.exec(); +} + diff --git a/examples/multimedia/videographicsitem/videographicsitem.pro b/examples/multimedia/videographicsitem/videographicsitem.pro new file mode 100644 index 000000000..3415ef64c --- /dev/null +++ b/examples/multimedia/videographicsitem/videographicsitem.pro @@ -0,0 +1,14 @@ +TEMPLATE = app +TARGET = videographicsitem + +QT += multimedia multimediawidgets + +HEADERS += videoplayer.h + +SOURCES += main.cpp \ + videoplayer.cpp + +target.path = $$[QT_INSTALL_EXAMPLES]/multimedia/videographicsitem +INSTALLS += target + +QT+=widgets diff --git a/examples/multimedia/videographicsitem/videoplayer.cpp b/examples/multimedia/videographicsitem/videoplayer.cpp new file mode 100644 index 000000000..096fabd77 --- /dev/null +++ b/examples/multimedia/videographicsitem/videoplayer.cpp @@ -0,0 +1,139 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "videoplayer.h" + +#include <QtWidgets> +#include <QGraphicsVideoItem> + +VideoPlayer::VideoPlayer(QWidget *parent) + : QWidget(parent) +{ + m_mediaPlayer = new QMediaPlayer(this); + const QSize screenGeometry = screen()->availableSize(); + m_videoItem = new QGraphicsVideoItem; + m_videoItem->setSize(QSizeF(screenGeometry.width() / 3, screenGeometry.height() / 2)); + + QGraphicsScene *scene = new QGraphicsScene(this); + QGraphicsView *graphicsView = new QGraphicsView(scene); + + scene->addItem(m_videoItem); + + QSlider *rotateSlider = new QSlider(Qt::Horizontal); + rotateSlider->setToolTip(tr("Rotate Video")); + rotateSlider->setRange(-180, 180); + rotateSlider->setValue(0); + + connect(rotateSlider, &QAbstractSlider::valueChanged, + this, &VideoPlayer::rotateVideo); + + QAbstractButton *openButton = new QPushButton(tr("Open...")); + connect(openButton, &QAbstractButton::clicked, this, &VideoPlayer::openFile); + + m_playButton = new QPushButton; + m_playButton->setEnabled(false); + m_playButton->setIcon(style()->standardIcon(QStyle::SP_MediaPlay)); + + connect(m_playButton, &QAbstractButton::clicked, this, &VideoPlayer::play); + + m_positionSlider = new QSlider(Qt::Horizontal); + m_positionSlider->setRange(0, 0); + + connect(m_positionSlider, &QAbstractSlider::sliderMoved, + this, &VideoPlayer::setPosition); + + QBoxLayout *controlLayout = new QHBoxLayout; + controlLayout->setContentsMargins(0, 0, 0, 0); + controlLayout->addWidget(openButton); + controlLayout->addWidget(m_playButton); + controlLayout->addWidget(m_positionSlider); + + QBoxLayout *layout = new QVBoxLayout(this); + layout->addWidget(graphicsView); + layout->addWidget(rotateSlider); + layout->addLayout(controlLayout); + + m_mediaPlayer->setVideoOutput(m_videoItem); + connect(m_mediaPlayer, &QMediaPlayer::playbackStateChanged, + this, &VideoPlayer::mediaStateChanged); + connect(m_mediaPlayer, &QMediaPlayer::positionChanged, this, &VideoPlayer::positionChanged); + connect(m_mediaPlayer, &QMediaPlayer::durationChanged, this, &VideoPlayer::durationChanged); +} + +VideoPlayer::~VideoPlayer() +{ +} + +QSize VideoPlayer::sizeHint() const +{ + return (m_videoItem->size() * qreal(3) / qreal(2)).toSize(); +} + +bool VideoPlayer::isPlayerAvailable() const +{ + return m_mediaPlayer->isAvailable(); +} + +void VideoPlayer::openFile() +{ + QFileDialog fileDialog(this); + fileDialog.setAcceptMode(QFileDialog::AcceptOpen); + fileDialog.setWindowTitle(tr("Open Movie")); + fileDialog.setDirectory(QStandardPaths::standardLocations(QStandardPaths::MoviesLocation).value(0, QDir::homePath())); + if (fileDialog.exec() == QDialog::Accepted) + load(fileDialog.selectedUrls().constFirst()); +} + +void VideoPlayer::load(const QUrl &url) +{ + m_mediaPlayer->setSource(url); + m_playButton->setEnabled(true); +} + +void VideoPlayer::play() +{ + switch (m_mediaPlayer->playbackState()) { + case QMediaPlayer::PlayingState: + m_mediaPlayer->pause(); + break; + default: + m_mediaPlayer->play(); + break; + } +} + +void VideoPlayer::mediaStateChanged(QMediaPlayer::PlaybackState state) +{ + switch(state) { + case QMediaPlayer::PlayingState: + m_playButton->setIcon(style()->standardIcon(QStyle::SP_MediaPause)); + break; + default: + m_playButton->setIcon(style()->standardIcon(QStyle::SP_MediaPlay)); + break; + } +} + +void VideoPlayer::positionChanged(qint64 position) +{ + m_positionSlider->setValue(position); +} + +void VideoPlayer::durationChanged(qint64 duration) +{ + m_positionSlider->setRange(0, duration); +} + +void VideoPlayer::setPosition(int position) +{ + m_mediaPlayer->setPosition(position); +} + + +void VideoPlayer::rotateVideo(int angle) +{ + //rotate around the center of video element + qreal x = m_videoItem->boundingRect().width() / 2.0; + qreal y = m_videoItem->boundingRect().height() / 2.0; + m_videoItem->setTransform(QTransform().translate(x, y).rotate(angle).translate(-x, -y)); +} diff --git a/examples/multimedia/videographicsitem/videoplayer.h b/examples/multimedia/videographicsitem/videoplayer.h new file mode 100644 index 000000000..a5be31efc --- /dev/null +++ b/examples/multimedia/videographicsitem/videoplayer.h @@ -0,0 +1,48 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef VIDEOPLAYER_H +#define VIDEOPLAYER_H + +#include <QMediaPlayer> +#include <QWidget> + +QT_BEGIN_NAMESPACE +class QAbstractButton; +class QSlider; +class QGraphicsVideoItem; +QT_END_NAMESPACE + +class VideoPlayer : public QWidget +{ + Q_OBJECT + +public: + VideoPlayer(QWidget *parent = nullptr); + ~VideoPlayer(); + + void load(const QUrl &url); + bool isPlayerAvailable() const; + + QSize sizeHint() const override; + +public slots: + void openFile(); + void play(); + +private slots: + void mediaStateChanged(QMediaPlayer::PlaybackState state); + void positionChanged(qint64 position); + void durationChanged(qint64 duration); + void setPosition(int position); + void rotateVideo(int angle); + +private: + QMediaPlayer *m_mediaPlayer = nullptr; + QGraphicsVideoItem *m_videoItem = nullptr; + QAbstractButton *m_playButton = nullptr; + QSlider *m_positionSlider = nullptr; +}; + +#endif + diff --git a/examples/multimedia/videowidget/CMakeLists.txt b/examples/multimedia/videowidget/CMakeLists.txt new file mode 100644 index 000000000..c33efeb0f --- /dev/null +++ b/examples/multimedia/videowidget/CMakeLists.txt @@ -0,0 +1,39 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +cmake_minimum_required(VERSION 3.16) +project(videowidget LANGUAGES CXX) + +set(CMAKE_AUTOMOC ON) + +if(NOT DEFINED INSTALL_EXAMPLESDIR) + set(INSTALL_EXAMPLESDIR "examples") +endif() + +set(INSTALL_EXAMPLEDIR "${INSTALL_EXAMPLESDIR}/multimedia/videowidget") + +find_package(Qt6 REQUIRED COMPONENTS Core Gui Multimedia MultimediaWidgets Widgets) + +qt_add_executable(videowidget + main.cpp + videoplayer.cpp videoplayer.h +) + +set_target_properties(videowidget PROPERTIES + WIN32_EXECUTABLE TRUE + MACOSX_BUNDLE TRUE +) + +target_link_libraries(videowidget PUBLIC + Qt::Core + Qt::Gui + Qt::Multimedia + Qt::MultimediaWidgets + Qt::Widgets +) + +install(TARGETS videowidget + RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}" + BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}" + LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}" +) diff --git a/examples/multimedia/videowidget/doc/images/video-videowidget.png b/examples/multimedia/videowidget/doc/images/video-videowidget.png Binary files differnew file mode 100644 index 000000000..a3c7bcb44 --- /dev/null +++ b/examples/multimedia/videowidget/doc/images/video-videowidget.png diff --git a/examples/multimedia/videowidget/doc/src/videowidget.qdoc b/examples/multimedia/videowidget/doc/src/videowidget.qdoc new file mode 100644 index 000000000..e999cc19c --- /dev/null +++ b/examples/multimedia/videowidget/doc/src/videowidget.qdoc @@ -0,0 +1,17 @@ +// Copyright (C) 2015 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only + +/*! + \example videowidget + \title Video Widget Example + \ingroup multimedia_examples + \brief Implementing a video player widget. + \meta {tag} {widgets,QVideoWidget} + + \e{Video Widget} demonstrates how to implement a simple video player using + QVideoWidget. + + \image video-videowidget.png + + \include examples-run.qdocinc +*/ diff --git a/examples/multimedia/videowidget/main.cpp b/examples/multimedia/videowidget/main.cpp new file mode 100644 index 000000000..ccf5983dd --- /dev/null +++ b/examples/multimedia/videowidget/main.cpp @@ -0,0 +1,41 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "videoplayer.h" + +#include <QtWidgets/QApplication> +#include <QtCore/QCommandLineParser> +#include <QtCore/QCommandLineOption> +#include <QtCore/QDir> +#include <QtCore/QUrl> +#include <QScreen> + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + QCoreApplication::setApplicationName("Video Widget Example"); + QCoreApplication::setOrganizationName("QtProject"); + QGuiApplication::setApplicationDisplayName(QCoreApplication::applicationName()); + QCoreApplication::setApplicationVersion(QT_VERSION_STR); + QCommandLineParser parser; + parser.setApplicationDescription("Qt Video Widget Example"); + parser.addHelpOption(); + parser.addVersionOption(); + parser.addPositionalArgument("url", "The URL to open."); + parser.process(app); + + VideoPlayer player; + if (!parser.positionalArguments().isEmpty()) { + const QUrl url = + QUrl::fromUserInput(parser.positionalArguments().constFirst(), + QDir::currentPath(), QUrl::AssumeLocalFile); + player.setUrl(url); + } + + const QSize availableGeometry = player.screen()->availableSize(); + player.resize(availableGeometry.width() / 6, availableGeometry.height() / 4); + player.show(); + + return app.exec(); +} diff --git a/examples/multimedia/videowidget/videoplayer.cpp b/examples/multimedia/videowidget/videoplayer.cpp new file mode 100644 index 000000000..4a34bfded --- /dev/null +++ b/examples/multimedia/videowidget/videoplayer.cpp @@ -0,0 +1,130 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "videoplayer.h" + +#include <QtWidgets> +#include <QVideoWidget> + +VideoPlayer::VideoPlayer(QWidget *parent) + : QWidget(parent) +{ + m_mediaPlayer = new QMediaPlayer(this); + QVideoWidget *videoWidget = new QVideoWidget; + + QAbstractButton *openButton = new QPushButton(tr("Open...")); + connect(openButton, &QAbstractButton::clicked, this, &VideoPlayer::openFile); + + m_playButton = new QPushButton; + m_playButton->setEnabled(false); + m_playButton->setIcon(style()->standardIcon(QStyle::SP_MediaPlay)); + + connect(m_playButton, &QAbstractButton::clicked, + this, &VideoPlayer::play); + + m_positionSlider = new QSlider(Qt::Horizontal); + m_positionSlider->setRange(0, 0); + + connect(m_positionSlider, &QAbstractSlider::sliderMoved, + this, &VideoPlayer::setPosition); + + m_errorLabel = new QLabel; + m_errorLabel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); + + QBoxLayout *controlLayout = new QHBoxLayout; + controlLayout->setContentsMargins(0, 0, 0, 0); + controlLayout->addWidget(openButton); + controlLayout->addWidget(m_playButton); + controlLayout->addWidget(m_positionSlider); + + QBoxLayout *layout = new QVBoxLayout; + layout->addWidget(videoWidget); + layout->addLayout(controlLayout); + layout->addWidget(m_errorLabel); + + setLayout(layout); + + m_mediaPlayer->setVideoOutput(videoWidget); + connect(m_mediaPlayer, &QMediaPlayer::playbackStateChanged, + this, &VideoPlayer::mediaStateChanged); + connect(m_mediaPlayer, &QMediaPlayer::positionChanged, this, &VideoPlayer::positionChanged); + connect(m_mediaPlayer, &QMediaPlayer::durationChanged, this, &VideoPlayer::durationChanged); + connect(m_mediaPlayer, &QMediaPlayer::errorChanged, + this, &VideoPlayer::handleError); +} + +VideoPlayer::~VideoPlayer() +{ +} + +void VideoPlayer::openFile() +{ + QFileDialog fileDialog(this); + fileDialog.setAcceptMode(QFileDialog::AcceptOpen); + fileDialog.setWindowTitle(tr("Open Movie")); + fileDialog.setDirectory(QStandardPaths::standardLocations(QStandardPaths::MoviesLocation).value(0, QDir::homePath())); + if (fileDialog.exec() == QDialog::Accepted) + setUrl(fileDialog.selectedUrls().constFirst()); +} + +void VideoPlayer::setUrl(const QUrl &url) +{ + m_errorLabel->setText(QString()); + setWindowFilePath(url.isLocalFile() ? url.toLocalFile() : QString()); + m_mediaPlayer->setSource(url); + m_playButton->setEnabled(true); +} + +void VideoPlayer::play() +{ + switch (m_mediaPlayer->playbackState()) { + case QMediaPlayer::PlayingState: + m_mediaPlayer->pause(); + break; + default: + m_mediaPlayer->play(); + break; + } +} + +void VideoPlayer::mediaStateChanged(QMediaPlayer::PlaybackState state) +{ + switch(state) { + case QMediaPlayer::PlayingState: + m_playButton->setIcon(style()->standardIcon(QStyle::SP_MediaPause)); + break; + default: + m_playButton->setIcon(style()->standardIcon(QStyle::SP_MediaPlay)); + break; + } +} + +void VideoPlayer::positionChanged(qint64 position) +{ + m_positionSlider->setValue(position); +} + +void VideoPlayer::durationChanged(qint64 duration) +{ + m_positionSlider->setRange(0, duration); +} + +void VideoPlayer::setPosition(int position) +{ + m_mediaPlayer->setPosition(position); +} + +void VideoPlayer::handleError() +{ + if (m_mediaPlayer->error() == QMediaPlayer::NoError) + return; + + m_playButton->setEnabled(false); + const QString errorString = m_mediaPlayer->errorString(); + QString message = "Error: "; + if (errorString.isEmpty()) + message += " #" + QString::number(int(m_mediaPlayer->error())); + else + message += errorString; + m_errorLabel->setText(message); +} diff --git a/examples/multimedia/videowidget/videoplayer.h b/examples/multimedia/videowidget/videoplayer.h new file mode 100644 index 000000000..b06df8280 --- /dev/null +++ b/examples/multimedia/videowidget/videoplayer.h @@ -0,0 +1,44 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef VIDEOPLAYER_H +#define VIDEOPLAYER_H + +#include <QMediaPlayer> +#include <QWidget> + +QT_BEGIN_NAMESPACE +class QAbstractButton; +class QSlider; +class QLabel; +class QUrl; +QT_END_NAMESPACE + +class VideoPlayer : public QWidget +{ + Q_OBJECT +public: + VideoPlayer(QWidget *parent = nullptr); + ~VideoPlayer(); + + void setUrl(const QUrl &url); + +public slots: + void openFile(); + void play(); + +private slots: + void mediaStateChanged(QMediaPlayer::PlaybackState state); + void positionChanged(qint64 position); + void durationChanged(qint64 duration); + void setPosition(int position); + void handleError(); + +private: + QMediaPlayer* m_mediaPlayer; + QAbstractButton *m_playButton; + QSlider *m_positionSlider; + QLabel *m_errorLabel; +}; + +#endif diff --git a/examples/multimedia/videowidget/videowidget.pro b/examples/multimedia/videowidget/videowidget.pro new file mode 100644 index 000000000..56312a028 --- /dev/null +++ b/examples/multimedia/videowidget/videowidget.pro @@ -0,0 +1,16 @@ +TEMPLATE = app +TARGET = videowidget + +QT += multimedia multimediawidgets + +HEADERS = \ + videoplayer.h + +SOURCES = \ + main.cpp \ + videoplayer.cpp + +target.path = $$[QT_INSTALL_EXAMPLES]/multimedia/videowidget +INSTALLS += target + +QT+=widgets |