Media Player Example¶
This example demonstrates a simple multimedia player that can play audio and video files using various codecs.
At its core this is a QML application, see Getting Started Programming with Qt Quick for information specific to that.
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
pragma Singleton
import QtQuick
QtObject {
enum Theme {
Light,
Dark
}
property int activeTheme : Config.Theme.Dark
readonly property bool isMobileTarget : Qt.platform.os === "android" || Qt.platform.os === "ios"
readonly property color mainColor : activeTheme ? "#09102B" : "#FFFFFF"
readonly property color secondaryColor : activeTheme ? "#FFFFFF" : "#09102B"
readonly property color highlightColor : "#41CD52"
function iconName(fileName, addSuffix = true) {
return `${fileName}${activeTheme === Config.Theme.Dark && addSuffix ? "_Dark.svg" : ".svg"}`
}
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Layouts
Item {
id: root
property real volume: volumeSlider.value / 100
Layout.minimumWidth: 50
Layout.maximumWidth: 200
RowLayout {
anchors.fill: root
spacing: root.width >= 85 ? 10 : 2
Image {
visible: root.width >= 85
source: ControlImages.iconSource("Mute_Icon")
}
CustomSlider {
id: volumeSlider
enabled: true
to: 100.0
value: 100.0
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
}
Image {
source: ControlImages.iconSource("Volume_Icon")
}
}
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Controls.Fusion
import QtQuick.Effects
Button {
id: control
flat: true
contentItem: Image {
id: image
source: control.icon.source
}
background: MultiEffect {
source: image
anchors.fill: control
visible: control.down
opacity: 0.5
shadowEnabled: true
blurEnabled: true
blur: 0.5
}
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Controls.Fusion
import Config
RadioButton {
id: control
height: 20
indicator: Rectangle {
width: 20
height: 20
radius: 10
x: control.leftPadding
y: control.height / 2 - height / 2
color: "transparent"
border.color: Config.secondaryColor
border.width: 2
Rectangle {
width: 10
height: 10
radius: 5
color: Config.secondaryColor
anchors.centerIn: parent
visible: control.checked
}
}
contentItem: Label {
text: control.text
font.pixelSize: 18
verticalAlignment: Text.AlignVCenter
leftPadding: control.indicator.width + control.spacing
color: Config.secondaryColor
}
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Controls.Fusion
import Config
Slider {
id: slider
property alias backgroundColor: backgroundRec.color
property alias backgroundOpacity: backgroundRec.opacity
background: Rectangle {
id: backgroundRec
x: slider.leftPadding
y: slider.topPadding + slider.availableHeight / 2 - height / 2
implicitWidth: 120
implicitHeight: 8
width: slider.availableWidth
height: implicitHeight
radius: 10
color: Config.highlightColor
opacity: 0.2
border.color: Config.highlightColor
border.width: 1
}
handle: Rectangle {
x: slider.leftPadding + slider.visualPosition * (slider.availableWidth - width)
y: slider.topPadding + slider.availableHeight / 2 - height / 2
implicitWidth: 8
implicitHeight: 8
color: "transparent"
}
Rectangle {
width: slider.visualPosition * slider.availableWidth
x: slider.leftPadding
y: slider.topPadding + slider.availableHeight / 2 - height / 2
height: 8
color: Config.highlightColor
radius: 10
}
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Layouts
import QtMultimedia
import Config
Item {
id: root
implicitHeight: 100
required property MediaPlayer mediaPlayer
readonly property int mediaPlayerState: root.mediaPlayer.playbackState
property bool isPlaylistShuffled: false
property bool isPlaylistLooped: false
property bool isPlaylistVisible: false
property url playlistIcon: !root.isPlaylistVisible ? ControlImages.iconSource("Playlist_Icon") : ControlImages.iconSource("Playlist_Active", false)
property url shuffleIcon: !root.isPlaylistShuffled ? ControlImages.iconSource("Shuffle_Icon") : ControlImages.iconSource("Shuffle_Active", false)
property alias volume: audio.volume
property alias playbackRate: rate.playbackRate
property alias playlistButton: playlistButton
property alias menuButton: menuButton
signal playNextFile()
signal playPreviousFile()
function changeLoopMode() {
if (root.mediaPlayer.loops === 1 && !root.isPlaylistLooped) {
root.mediaPlayer.loops = MediaPlayer.Infinite
} else if (root.mediaPlayer.loops === MediaPlayer.Infinite) {
root.mediaPlayer.loops = 1
root.isPlaylistLooped = true
} else {
root.mediaPlayer.loops = 1
root.isPlaylistLooped = false
}
}
Item {
anchors.fill: root
RowLayout {
id: playerButtons
anchors.fill: parent
spacing: Screen.primaryOrientation === Qt.LandscapeOrientation ? 5 : 1
Item {
CustomButton {
id: menuButton
icon.source: ControlImages.iconSource("Menu_Icon")
visible: Config.isMobileTarget
anchors.centerIn: parent
}
Layout.fillWidth: true
Layout.minimumWidth: 40
Layout.maximumWidth: 95
}
PlaybackRateControl {
id: rate
Layout.fillHeight: true
Layout.fillWidth: true
}
Item {
Layout.fillWidth: true
}
RowLayout {
id: controlButtons
spacing: Screen.primaryOrientation === Qt.LandscapeOrientation ? 25 : 1
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
CustomButton {
id: shuffleButton
icon.source: root.shuffleIcon
visible: Screen.primaryOrientation === Qt.LandscapeOrientation
onClicked: root.isPlaylistShuffled = !root.isPlaylistShuffled
}
CustomButton {
id: previousButton
icon.source: ControlImages.iconSource("Previous_Icon")
onClicked: root.playPreviousFile()
}
CustomButton {
id: playButton
icon.source: ControlImages.iconSource("Play_Icon", false)
onClicked: root.mediaPlayer.play()
}
CustomButton {
id: pausedButton
icon.source: ControlImages.iconSource("Stop_Icon", false)
onClicked: root.mediaPlayer.pause()
}
CustomButton {
id: nextButton
icon.source: ControlImages.iconSource("Next_Icon")
onClicked: root.playNextFile()
}
CustomButton {
id: loopButton
icon.source: ControlImages.iconSource("Loop_Icon")
visible: Screen.primaryOrientation === Qt.LandscapeOrientation
onClicked: root.changeLoopMode()
states: [
State {
name: "noActiveLooping"
when: root.mediaPlayer.loops === 1 && !root.isPlaylistLooped
PropertyChanges {
loopButton.icon.source: ControlImages.iconSource("Loop_Icon")
}
},
State {
name: "singleLoop"
when: root.mediaPlayer.loops === MediaPlayer.Infinite
PropertyChanges {
loopButton.icon.source: ControlImages.iconSource("Single_Loop", false)
}
},
State {
name: "playlistLoop"
when: root.isPlaylistLooped
PropertyChanges {
loopButton.icon.source: ControlImages.iconSource("Loop_Playlist", false)
}
}
]
}
}
Item {
Layout.fillWidth: true
}
AudioControl {
id: audio
Layout.fillHeight: true
Layout.fillWidth: true
}
Item {
Layout.fillWidth: true
Layout.minimumWidth:40
Layout.maximumWidth: 95
CustomButton {
id: playlistButton
anchors.centerIn: parent
icon.source: root.playlistIcon
}
}
}
}
states: [
State {
name: "playing"
when: root.mediaPlayerState == MediaPlayer.PlayingState
PropertyChanges {
playButton.visible: false
}
PropertyChanges {
pausedButton.visible: true
}
},
State {
name: "paused"
when: root.mediaPlayerState == MediaPlayer.PausedState || root.mediaPlayerState == MediaPlayer.StoppedState
PropertyChanges {
playButton.visible: true
}
PropertyChanges {
pausedButton.visible: false
}
}
]
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Controls.Fusion
import QtQuick.Layouts
import Config
Item {
id: root
property alias playbackRate: slider.value
Layout.minimumWidth: 50
Layout.maximumWidth: 200
RowLayout {
anchors.fill: root
spacing: root.width >= 85 ? 10 : 2
Image {
visible: root.width >= 85
source: ControlImages.iconSource("Rate_Icon")
}
CustomSlider {
id: slider
Layout.fillWidth: true
snapMode: Slider.SnapOnRelease
from: 0.5
to: 2.5
stepSize: 0.5
value: 1.0
}
Label {
text: slider.value.toFixed(1) + "x"
color: Config.highlightColor
}
}
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Controls.Fusion
import QtQuick.Layouts
import QtMultimedia
import Config
Item {
id: root
implicitHeight: 40
required property MediaPlayer mediaPlayer
property alias fullScreenButton: fullScreenButton
property alias settingsButton: settingsButton
property alias isMediaSliderPressed: mediaSlider.pressed
property alias showSeeker: showSeekerAnim
property alias hideSeeker: hideSeekerAnim
function getTime(time : int) : string {
const h = Math.floor(time / 3600000).toString()
const m = Math.floor(time / 60000).toString()
const s = Math.floor(time / 1000 - m * 60).toString()
return `${h.padStart(2,'0')}:${m.padStart(2,'0')}:${s.padStart(2, '0')}`
}
RowLayout {
anchors.fill: root
anchors.leftMargin: 10
anchors.rightMargin: 10
Label {
id: mediaTime
color: Config.secondaryColor
font.bold: true
text: root.getTime(root.mediaPlayer.position)
}
CustomSlider {
id: mediaSlider
backgroundColor: !Config.activeTheme ? "white" : Config.highlightColor
backgroundOpacity: !Config.activeTheme ? 0.8 : 0.2
enabled: root.mediaPlayer.seekable
to: 1.0
value: root.mediaPlayer.position / root.mediaPlayer.duration
Layout.fillWidth: true
onMoved: root.mediaPlayer.setPosition(value * root.mediaPlayer.duration)
}
Label {
id: durationTime
color: Config.secondaryColor
font.bold: true
text: root.getTime(root.mediaPlayer.duration)
}
CustomButton {
id: settingsButton
icon.source: ControlImages.iconSource("Settings_Icon")
}
CustomButton {
id: fullScreenButton
icon.source: ControlImages.iconSource("FullScreen_Icon")
}
}
ParallelAnimation {
id: hideSeekerAnim
NumberAnimation {
target: root
properties: "opacity"
to: 0
duration: 1000
easing.type: Easing.InOutQuad
}
NumberAnimation {
target: root
properties: "anchors.bottomMargin"
to: -root.height
duration: 1000
easing.type: Easing.InOutQuad
}
}
ParallelAnimation {
id: showSeekerAnim
PropertyAnimation {
target: root
properties: "opacity"
to: 1
duration: 1000
easing.type: Easing.InOutQuad
}
PropertyAnimation {
target: root
properties: "anchors.bottomMargin"
to: 0
duration: 500
easing.type: Easing.InOutQuad
}
}
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Controls.Fusion
import MediaControls
Popup {
id: errorPopup
anchors.centerIn: Overlay.overlay
padding: 30
property alias errorMsg: label.text
onOpened: closeTimer.restart()
background: Rectangle {
color: "transparent"
}
Column {
spacing: 15
Image {
source: ControlImages.iconSource("Error", false)
anchors.horizontalCenter: parent.horizontalCenter
}
Label {
id: label
color: "#FFE353"
font.pixelSize: 24
}
}
Timer {
id: closeTimer
interval: 5000
onTriggered: errorPopup.close()
}
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Window
import QtQuick.Controls.Fusion
import QtMultimedia
import QtQuick.Effects
import MediaControls
import Config
ApplicationWindow {
id: root
width: 1200
height: 780
minimumHeight: 460
minimumWidth: 640
visible: true
color: Config.mainColor
title: qsTr("Multimedia Player")
required property url source
required property list<string> nameFilters
required property int selectedNameFilter
property alias currentFile: playlistInfo.currentIndex
property alias playlistLooped: playbackControl.isPlaylistLooped
property alias metadataInfo: settingsInfo.metadataInfo
property alias tracksInfo: settingsInfo.tracksInfo
function playMedia() {
mediaPlayer.source = playlistInfo.getSource()
mediaPlayer.play()
}
function closeOverlays() {
settingsInfo.visible = false
playlistInfo.visible = false
}
function showOverlay(overlay) {
closeOverlays()
overlay.visible = true
}
function openFile(path) {
++currentFile
playlistInfo.addFile(currentFile, path)
mediaPlayer.source = path
mediaPlayer.play()
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onPositionChanged: {
if (!seeker.opacity) {
if (videoOutput.fullScreen) {
showControls.start()
} else {
seeker.showSeeker.start()
}
} else {
timer.restart()
}
}
onClicked: root.closeOverlays()
}
Timer {
id: timer
interval: 3000
onTriggered: {
if (!seeker.isMediaSliderPressed) {
if (videoOutput.fullScreen) {
hideControls.start()
} else {
seeker.hideSeeker.start()
}
} else {
timer.restart()
}
}
}
ErrorPopup {
id: errorPopup
}
Label {
text: qsTr("Click <font color=\"#41CD52\">here</font> to open media file.")
font.pixelSize: 24
color: Config.secondaryColor
anchors.centerIn: parent
visible: !errorPopup.visible && !videoOutput.visible && !defaultCoverArt.visible
TapHandler {
onTapped: menuBar.openFileMenu.open()
}
}
PlayerMenuBar {
id: menuBar
anchors.left: parent.left
anchors.right: parent.right
visible: !videoOutput.fullScreen
onFileOpened: (path) => openFile(path)
nameFilters : root.nameFilters
selectedNameFilter : root.selectedNameFilter
}
TouchMenu {
id: menuPopup
x: (parent.width - width) / 2
y: parent.height - height - 32
width: root.width - 64
openFileMenuItem.onClicked: {
menuPopup.close()
menuBar.openFileMenu.open()
}
openUrlMenuItem.onClicked: {
menuPopup.close()
menuBar.openUrlPopup.open()
}
}
MediaPlayer {
id: mediaPlayer
playbackRate: playbackControl.playbackRate
videoOutput: videoOutput
audioOutput: AudioOutput {
id: audio
volume: playbackControl.volume
}
source: new URL("https://download.qt.io/learning/videos/media-player-example/Qt_LogoMergeEffect.mp4")
function updateMetadata() {
root.metadataInfo.clear()
root.metadataInfo.read(mediaPlayer.metaData)
}
onMetaDataChanged: updateMetadata()
onActiveTracksChanged: updateMetadata()
onErrorOccurred: {
errorPopup.errorMsg = mediaPlayer.errorString
errorPopup.open()
}
onTracksChanged: {
settingsInfo.tracksInfo.selectedAudioTrack = mediaPlayer.activeAudioTrack
settingsInfo.tracksInfo.selectedVideoTrack = mediaPlayer.activeVideoTrack
settingsInfo.tracksInfo.selectedSubtitleTrack = mediaPlayer.activeSubtitleTrack
updateMetadata()
}
onMediaStatusChanged: {
if ((MediaPlayer.EndOfMedia === mediaStatus && mediaPlayer.loops !== MediaPlayer.Infinite) &&
((root.currentFile < playlistInfo.mediaCount - 1) || playlistInfo.isShuffled)) {
if (!playlistInfo.isShuffled) {
++root.currentFile
}
root.playMedia()
} else if (MediaPlayer.EndOfMedia === mediaStatus && root.playlistLooped && playlistInfo.mediaCount) {
root.currentFile = 0
root.playMedia()
}
}
}
VideoOutput {
id: videoOutput
anchors.top: fullScreen || Config.isMobileTarget ? parent.top : menuBar.bottom
anchors.bottom: fullScreen ? parent.bottom : playbackControl.top
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: fullScreen ? 0 : 20
anchors.rightMargin: fullScreen ? 0 : 20
visible: mediaPlayer.hasVideo
property bool fullScreen: false
TapHandler {
onDoubleTapped: {
if (parent.fullScreen) {
root.showNormal()
} else {
root.showFullScreen()
}
parent.fullScreen = !parent.fullScreen
}
onTapped: {
root.closeOverlays()
}
}
}
Image {
id: defaultCoverArt
anchors.horizontalCenter: videoOutput.horizontalCenter
anchors.verticalCenter: videoOutput.verticalCenter
visible: !videoOutput.visible && mediaPlayer.hasAudio
source: Images.iconSource("Default_CoverArt", false)
}
Rectangle {
id: background
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.top: seeker.opacity ? seeker.top : playbackControl.top
color: Config.mainColor
opacity: videoOutput.fullScreen ? 0.75 : 0.5
}
Image {
id: shadow
source: `icons/Shadow.png`
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
}
PlaybackSeekControl {
id: seeker
anchors.left: videoOutput.left
anchors.right: videoOutput.right
anchors.bottom: playbackControl.top
mediaPlayer: mediaPlayer
fullScreenButton.onClicked: {
if (mediaPlayer.hasVideo) {
videoOutput.fullScreen ? root.showNormal() : root.showFullScreen()
videoOutput.fullScreen = !videoOutput.fullScreen
}
}
settingsButton.onClicked: !settingsInfo.visible ? root.showOverlay(settingsInfo) : root.closeOverlays()
}
PlaybackControl {
id: playbackControl
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
mediaPlayer: mediaPlayer
isPlaylistVisible: playlistInfo.visible
onPlayNextFile: {
if (playlistInfo.mediaCount) {
if (!playlistInfo.isShuffled){
++root.currentFile
if (root.currentFile > playlistInfo.mediaCount - 1 && root.playlistLooped) {
root.currentFile = 0
} else if (root.currentFile > playlistInfo.mediaCount - 1 && !root.playlistLooped) {
--root.currentFile
return
}
}
root.playMedia()
}
}
onPlayPreviousFile: {
if (playlistInfo.mediaCount) {
if (!playlistInfo.isShuffled){
--root.currentFile
if (root.currentFile < 0 && isPlaylistLooped) {
root.currentFile = playlistInfo.mediaCount - 1
} else if (root.currentFile < 0 && !root.playlistLooped) {
++root.currentFile
return
}
}
root.playMedia()
}
}
playlistButton.onClicked: !playlistInfo.visible ? root.showOverlay(playlistInfo) : root.closeOverlays()
menuButton.onClicked: menuPopup.open()
}
MultiEffect {
source: settingsInfo
anchors.fill: settingsInfo
shadowEnabled: settingsInfo.visible || playlistInfo.visible
visible: settingsInfo.visible || playlistInfo.visible
}
PlaylistInfo {
id: playlistInfo
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: seeker.opacity ? seeker.top : playbackControl.top
anchors.topMargin: 10
anchors.rightMargin: 5
visible: false
isShuffled: playbackControl.isPlaylistShuffled
onPlaylistUpdated: {
if (mediaPlayer.playbackState == MediaPlayer.StoppedState && root.currentFile < playlistInfo.mediaCount - 1) {
++root.currentFile
root.playMedia()
}
}
onCurrentFileRemoved: {
mediaPlayer.stop()
if (root.currentFile < playlistInfo.mediaCount - 1) {
root.playMedia()
} else if (playlistInfo.mediaCount) {
--root.currentFile
root.playMedia()
}
}
}
SettingsInfo {
id: settingsInfo
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: seeker.opacity ? seeker.top : playbackControl.top
anchors.topMargin: 10
anchors.rightMargin: 5
mediaPlayer: mediaPlayer
selectedAudioTrack: mediaPlayer.activeAudioTrack
selectedVideoTrack: mediaPlayer.activeVideoTrack
selectedSubtitleTrack: mediaPlayer.activeSubtitleTrack
visible: false
}
ParallelAnimation {
id: hideControls
NumberAnimation {
targets: [playbackControl, seeker, background, shadow]
property: "opacity"
to: 0
duration: 1000
easing.type: Easing.InOutQuad
}
NumberAnimation {
target: playbackControl
property: "anchors.bottomMargin"
to: -playbackControl.height - seeker.height
duration: 1000
easing.type: Easing.InOutQuad
}
}
ParallelAnimation {
id: showControls
NumberAnimation {
targets: [playbackControl, seeker, shadow]
property: "opacity"
to: 1
duration: 1000
easing.type: Easing.InOutQuad
}
NumberAnimation {
target: background
property: "opacity"
to: 0.5
duration: 1000
easing.type: Easing.InOutQuad
}
NumberAnimation {
target: playbackControl
property: "anchors.bottomMargin"
to: 0
duration: 1000
easing.type: Easing.InOutQuad
}
}
Component.onCompleted: {
if (source.toString().length > 0)
openFile(source)
else
mediaPlayer.play()
}
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls.Fusion
import Config
Item {
id: root
function clear() {
elements.clear()
}
function read(metadata) {
if (metadata) {
for (var key of metadata.keys()) {
if (metadata.stringValue(key)) {
elements.append({
name: metadata.metaDataKeyToString(key),
value: metadata.stringValue(key)
})
}
}
}
}
ListModel {
id: elements
}
Item {
anchors.fill: parent
anchors.margins: 15
ListView {
id: metadataList
anchors.fill: parent
model: elements
delegate: RowLayout {
id: row
width: metadataList.width
required property string name
required property string value
Label {
text: row.name + ":"
font.pixelSize: 16
color: Config.secondaryColor
Layout.preferredWidth: root.width / 2
}
Label {
text: row.value
font.pixelSize: 16
wrapMode: Text.WrapAnywhere
color: Config.secondaryColor
Layout.fillWidth: true
}
}
}
Label {
visible: !elements.count
font.pixelSize: 16
text: qsTr("No metadata present")
anchors.centerIn: parent
color: Config.secondaryColor
}
}
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Controls.Fusion
import QtQuick.Dialogs
import QtCore
import Config
Item {
id: root
implicitHeight: menuBar.height
signal fileOpened(path: url)
property alias openFileMenu: fileDialog
property alias openUrlPopup: urlPopup
property list<string> nameFilters
property int selectedNameFilter
FileDialog {
id: fileDialog
currentFolder: StandardPaths.standardLocations(StandardPaths.MoviesLocation)[0]
nameFilters: root.nameFilters
selectedNameFilter.index: root.selectedNameFilter
title: qsTr("Please choose a file")
onAccepted: root.fileOpened(fileDialog.selectedFile)
}
UrlPopup {
id: urlPopup
onPathChanged: root.fileOpened(urlPopup.path)
}
MenuBar {
id: menuBar
visible: !Config.isMobileTarget
anchors.left: root.left
leftPadding: 10
topPadding: 10
palette.base: Config.mainColor
palette.text: Config.secondaryColor
palette.highlightedText: Config.highlightColor
palette.window: "transparent"
palette.highlight: Config.mainColor
Menu {
title: qsTr("&File")
palette.base: Config.mainColor
palette.text: Config.secondaryColor
palette.highlight: Config.highlightColor
MenuItem {
text: qsTr("Open &File")
onTriggered: fileDialog.open()
}
MenuItem {
text: qsTr("Open &URL")
onTriggered: urlPopup.open()
}
}
}
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls.Fusion
import QtQuick.Dialogs
import QtQuick.Layouts
import QtCore
import MediaControls
import Config
import io.qt.filenameprovider
Rectangle {
id: root
implicitWidth: 380
color: Config.mainColor
border.color: "lightgrey"
radius: 10
property int currentIndex: -1
property bool isShuffled: false
property alias mediaCount: files.count
signal playlistUpdated()
signal currentFileRemoved()
function getSource() {
if (isShuffled && mediaCount > 1) {
let randomIndex = Math.floor(Math.random() * mediaCount)
while (randomIndex == currentIndex) {
randomIndex = Math.floor(Math.random() * mediaCount)
}
currentIndex = randomIndex
}
return files.get(currentIndex).path
}
function addFiles(index, selectedFiles) {
selectedFiles.forEach(function (file){
const url = new URL(file)
files.insert(index,
{
path: url,
isMovie: isMovie(FileNameProvider.getFileName(url.toString()))
})
})
playlistUpdated()
}
function addFile(index, selectedFile) {
if (index > mediaCount || index < 0) {
index = 0
currentIndex = 0
}
files.insert(index,
{
path: selectedFile,
isMovie: isMovie(FileNameProvider.getFileName(selectedFile.toString()))
})
}
function isMovie(path) {
const paths = path.split('.')
const extension = paths[paths.length - 1]
const musicFormats = ["mp3", "wav", "aac"]
for (const format of musicFormats) {
if (format === extension) {
return false
}
}
return true
}
MouseArea {
anchors.fill: root
preventStealing: true
}
FileDialog {
id: folderView
title: qsTr("Add files to playlist")
currentFolder: StandardPaths.standardLocations(StandardPaths.MoviesLocation)[0]
fileMode: FileDialog.OpenFiles
onAccepted: {
root.addFiles(files.count, folderView.selectedFiles)
close()
}
}
ListModel {
id: files
}
Item {
id: playlist
anchors.fill: root
anchors.margins: 30
RowLayout {
id: header
width: playlist.width
Label {
font.bold: true
font.pixelSize: 20
text: qsTr("Playlist")
color: Config.secondaryColor
Layout.fillWidth: true
}
CustomButton {
icon.source: ControlImages.iconSource("Add_file")
onClicked: folderView.open()
}
}
ListView {
id: listView
model: files
anchors.fill: playlist
anchors.topMargin: header.height + 30
spacing: 20
delegate: RowLayout {
id: row
width: listView.width
spacing: 15
required property string path
required property int index
required property bool isMovie
Image {
id: mediaIcon
states: [
State {
name: "activeMovie"
when: root.currentIndex === row.index && row.isMovie
PropertyChanges {
mediaIcon.source: Images.iconSource("Movie_Active", false)
}
},
State {
name: "inactiveMovie"
when: root.currentIndex !== row.index && row.isMovie
PropertyChanges {
mediaIcon.source: Images.iconSource("Movie_Icon")
}
},
State {
name: "activeMusic"
when: root.currentIndex === row.index && !row.isMovie
PropertyChanges {
mediaIcon.source: Images.iconSource("Music_Active", false)
}
},
State {
name: "inactiveMusic"
when: root.currentIndex !== row.index && !row.isMovie
PropertyChanges {
mediaIcon.source: Images.iconSource("Music_Icon")
}
}
]
}
Label {
Layout.fillWidth: true
elide: Text.ElideRight
font.bold: root.currentIndex === row.index
color: root.currentIndex === row.index ? Config.highlightColor : Config.secondaryColor
font.pixelSize: 18
text: {
return FileNameProvider.getFileName(row.path)
}
}
CustomButton {
icon.source: ControlImages.iconSource("Trash_Icon")
onClicked: {
const removedIndex = row.index
files.remove(row.index)
if (root.currentIndex === removedIndex) {
root.currentFileRemoved()
} else if (root.currentIndex > removedIndex) {
--root.currentIndex
}
}
}
}
remove: Transition {
NumberAnimation {
property: "opacity"
from: 1.0
to: 0.0
duration: 400
}
}
add: Transition {
NumberAnimation {
property: "opacity"
from: 0.0
to: 1.0
duration: 400
}
NumberAnimation {
property: "scale"
from: 0.5
to: 1.0
duration: 400
}
}
displaced: Transition {
NumberAnimation {
properties: "y"
duration: 600
easing.type: Easing.OutBounce
}
}
}
}
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls.Fusion
import QtMultimedia
import Config
Rectangle {
id: root
implicitWidth: 380
color: Config.mainColor
border.color: "lightgrey"
radius: 10
property alias tracksInfo: tracksInfo
property alias metadataInfo: metadataInfo
required property MediaPlayer mediaPlayer
required property int selectedAudioTrack
required property int selectedVideoTrack
required property int selectedSubtitleTrack
MouseArea {
anchors.fill: root
preventStealing: true
}
TabBar {
id: bar
width: root.width
contentHeight: 60
Repeater {
model: [qsTr("Metadata"), qsTr("Tracks"), qsTr("Theme")]
TabButton {
id: tab
required property int index
required property string modelData
property color shadowColor: bar.currentIndex === index ? Config.highlightColor : "black"
property color textColor: bar.currentIndex === index ? Config.highlightColor : Config.secondaryColor
background: Rectangle {
opacity: 0.15
gradient: Gradient {
GradientStop { position: 0.0; color: "transparent" }
GradientStop { position: 0.5; color: "transparent" }
GradientStop { position: 1.0; color: tab.shadowColor }
}
}
contentItem: Label {
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
text: tab.modelData
font.pixelSize: 20
color: tab.textColor
}
}
}
}
StackLayout {
width: root.width
anchors.top: bar.bottom
anchors.bottom: root.bottom
currentIndex: bar.currentIndex
MetadataInfo { id: metadataInfo }
TracksInfo {
id: tracksInfo
mediaPlayer: root.mediaPlayer
selectedAudioTrack: root.selectedAudioTrack
selectedVideoTrack: root.selectedVideoTrack
selectedSubtitleTrack: root.selectedSubtitleTrack
}
ThemeInfo { id: themeInfo }
}
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls.Fusion
import MediaControls
import Config
Item {
id: root
Item {
anchors.fill: parent
Column {
padding: 15
spacing: 20
ButtonGroup { id: group }
CustomRadioButton {
checked: Config.Theme.Light === Config.activeTheme
text: qsTr("Light theme")
ButtonGroup.group: group
onClicked: Config.activeTheme = Config.Theme.Light
}
CustomRadioButton {
checked: Config.Theme.Dark === Config.activeTheme
text: qsTr("Dark theme")
ButtonGroup.group: group
onClicked: Config.activeTheme = Config.Theme.Dark
}
}
}
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Controls.Fusion
import QtQuick.Layouts
import Config
Menu {
id: menuPopup
padding: 0
verticalPadding: 15
property alias openUrlMenuItem: openUrlMenuItem
property alias openFileMenuItem: openFileMenuItem
background: Rectangle {
color: Config.mainColor
radius: 15
border.color: Config.highlightColor
}
component MenuItemLabel: Label {
font.pixelSize: 24
color: Config.highlightColor
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
topPadding: 4
bottomPadding: 4
}
component CustomMenuItem: MenuItem {
id: menuItem
property bool bold
text: qsTr("File")
contentItem: MenuItemLabel {
text: menuItem.text
font.bold: menuItem.bold
}
background: Rectangle {
color: menuItem.pressed ? Config.highlightColor : "transparent"
opacity: 0.25
}
}
MenuItemLabel {
text: qsTr("Load media file from:")
color: Config.secondaryColor
bottomPadding: 12
width: parent.width
}
Rectangle {
width: parent.width
implicitHeight: 1
color: Config.highlightColor
opacity: 0.25
}
CustomMenuItem {
id: openFileMenuItem
text: qsTr("File")
bold: true
}
Rectangle {
width: parent.width
implicitHeight: 1
color: Config.highlightColor
opacity: 0.25
}
CustomMenuItem {
id: openUrlMenuItem
text: qsTr("URL")
bold: true
}
Rectangle {
implicitHeight: 1
color: Config.highlightColor
Layout.fillWidth: true
}
CustomMenuItem {
id: cancelButtonBackground
text: qsTr("Cancel")
}
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtMultimedia
Item {
id: root
required property int selectedAudioTrack
required property int selectedVideoTrack
required property int selectedSubtitleTrack
required property MediaPlayer mediaPlayer
Flickable {
anchors.fill: parent
contentWidth: column.implicitWidth
contentHeight: column.implicitHeight
boundsBehavior: Flickable.DragAndOvershootBounds
flickableDirection: Flickable.VerticalFlick
clip: true
Column {
id: column
anchors.fill: parent
clip: true
padding: 15
spacing: 20
TracksOptions {
id: audioTracks
headerText: qsTr("Audio Tracks")
selectedTrack: root.selectedAudioTrack
metaData: root.mediaPlayer.audioTracks
onSelectedTrackChanged: root.mediaPlayer.activeAudioTrack = audioTracks.selectedTrack
}
TracksOptions {
id: videoTracks
headerText: qsTr("Video Tracks")
selectedTrack: root.selectedVideoTrack
metaData: root.mediaPlayer.videoTracks
onSelectedTrackChanged: root.mediaPlayer.activeVideoTrack = videoTracks.selectedTrack
}
TracksOptions {
id: subtitleTracks
headerText: qsTr("Subtitle Tracks")
selectedTrack: root.selectedSubtitleTrack
metaData: root.mediaPlayer.subtitleTracks
onSelectedTrackChanged: root.mediaPlayer.activeSubtitleTrack = subtitleTracks.selectedTrack
}
}
}
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls.Fusion
import QtMultimedia
import MediaControls
import Config
Item {
id: root
implicitWidth: 380
implicitHeight: childrenRect.height
required property int selectedTrack
required property list<mediaMetaData> metaData
property string headerText: ""
function readTracks(metadataList : list<mediaMetaData>) {
const LanguageKey = 6
elements.clear()
if (!metadataList || !metadataList.length)
return
elements.append({
language: "No Track",
trackNumber: -1
})
metadataList.forEach(function (metadata, index) {
const language = metadata.stringValue(LanguageKey)
const label = language ? language : "Track " + (index + 1)
elements.append({
language: label,
trackNumber: index
})
});
}
ListModel { id: elements }
ButtonGroup { id: group }
Column {
spacing: 16
Label {
id: header
text: elements.count ? qsTr(root.headerText) : qsTr("No " + root.headerText + " present")
font.pixelSize: 18
font.bold: true
color: Config.secondaryColor
}
Column {
id: trackList
spacing: 18
Repeater {
model: elements
delegate: CustomRadioButton {
checked: trackNumber === root.selectedTrack
text: language
required property int trackNumber
required property string language
ButtonGroup.group: group
onClicked: root.selectedTrack = trackNumber
}
}
}
}
onMetaDataChanged: readTracks(root.metaData)
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Controls.Fusion
import QtQuick.Layouts
import MediaControls
import Config
Popup {
id: urlPopup
anchors.centerIn: Overlay.overlay
padding: 30
width: 500
height: column.height + 60
property url path: ""
readonly property color borderColor: urlText.text ? (!errorMsg.visible ? Config.highlightColor : "red") : Config.secondaryColor
background: Rectangle {
color: Config.mainColor
opacity: 0.9
radius: 15
border.color: "grey"
}
function setUrl(urlPath: url) {
path = urlPath
urlPopup.close()
}
function validateUrl(urlText: string) : bool {
const urlPattern = /^([a-z]+:){1,2}\/\/.+/
return urlPattern.test(urlText)
}
Column {
id: column
spacing: 20
Label {
text: qsTr("Load from URL")
font.pixelSize: 18
anchors.horizontalCenter: parent.horizontalCenter
color: Config.secondaryColor
}
ColumnLayout {
spacing: 0
TextField {
id: urlText
leftPadding: 15
verticalAlignment: TextInput.AlignVCenter
font.pixelSize: 16
placeholderText: qsTr("URL:")
placeholderTextColor: Config.secondaryColor
color: Config.secondaryColor
text: "https://download.qt.io/learning/videos/media-player-example/Qt_LogoMergeEffect.mp4"
Layout.preferredHeight: 40
Layout.preferredWidth: 440
background: Rectangle {
color: Config.mainColor
border.color: urlPopup.borderColor
}
}
Rectangle {
id: errorMsg
visible: false
color: "#FF3A3A"
Layout.minimumHeight: 40
Layout.minimumWidth: 130
Layout.alignment: Qt.AlignLeft
Row {
anchors.centerIn: parent
spacing: 10
Image {
source: Images.iconSource("Warning_Icon", false)
}
Label {
text: qsTr("Wrong URL")
font.pixelSize: 16
color: "white"
}
}
onVisibleChanged: showError.start()
NumberAnimation {
id: showError
target: errorMsg
properties: "opacity"
from: 0
to: 1
duration: 1000
}
}
}
RowLayout {
spacing: 20
anchors.horizontalCenter: parent.horizontalCenter
CustomButton {
icon.source: ControlImages.iconSource("Cancel_Button", false)
onClicked: {
urlText.text = ""
urlPopup.close()
}
}
CustomButton {
icon.source: ControlImages.iconSource("Load_Button", false)
enabled: urlText.text
opacity: urlText.text ? 1 : 0.5
onClicked: {
if (urlPopup.validateUrl(urlText.text)) {
urlPopup.setUrl(new URL(urlText.text))
} else {
errorMsg.visible = true
}
}
}
}
}
onOpened: urlPopup.forceActiveFocus()
onClosed: {
urlText.text = ""
errorMsg.visible = false
}
}
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import os
import sys
from argparse import ArgumentParser, RawTextHelpFormatter
from pathlib import Path
from PySide6.QtCore import QDir, QFileInfo, QObject, QUrl, Slot
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine, QmlElement, QmlSingleton
from PySide6.QtMultimedia import QMediaFormat
_opt_verbose = False
QML_IMPORT_NAME = "io.qt.filenameprovider"
QML_IMPORT_MAJOR_VERSION = 1
@QmlElement
@QmlSingleton
class FileNameProvider(QObject):
def __init__(self, p=None):
super().__init__(p)
@Slot(str, result=str)
def getFileName(self, p):
return QFileInfo(p).fileName()
def nameFilters():
"""Create a tuple of name filters/preferred index for populating the
open file dialog."""
result = []
preferredFilter = ""
formats = QMediaFormat().supportedFileFormats(QMediaFormat.Decode)
for m, format in enumerate(formats):
mediaFormat = QMediaFormat(format)
mimeType = mediaFormat.mimeType()
if mimeType.isValid():
filter = QMediaFormat.fileFormatDescription(format) + " ("
for i, suffix in enumerate(mimeType.suffixes()):
if i:
filter += ' '
filter += "*." + suffix
filter += ')'
result.append(filter)
if mimeType.name() == "video/mp4":
preferredFilter = filter
result.sort()
preferred = result.index(preferredFilter) if preferredFilter else 0
return (result, preferred)
if __name__ == "__main__":
argument_parser = ArgumentParser(description="Media Player",
formatter_class=RawTextHelpFormatter)
argument_parser.add_argument("-v", "--verbose", action="store_true",
help="Generate more output")
argument_parser.add_argument("url", help="The URL to open", nargs='?',
type=str)
options = argument_parser.parse_args()
_opt_verbose = options.verbose
if _opt_verbose:
os.environ["QML_IMPORT_TRACE"] = "1"
source = QUrl.fromUserInput(options.url, QDir.currentPath()) if options.url else QUrl()
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
app_dir = Path(__file__).parent
app_dir_url = QUrl.fromLocalFile(os.fspath(app_dir))
engine.addImportPath(os.fspath(app_dir))
nameFilterList, selectedNameFilter = nameFilters()
engine.setInitialProperties({
"source": source,
"nameFilters": nameFilterList,
"selectedNameFilter": selectedNameFilter})
engine.loadFromModule("MediaPlayer", "Main")
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec())