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.

Media Player Screenshot

Download this example

// 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"

    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

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: "#41CD52"
        opacity: 0.2
        border.color: "#41CD52"
        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: "#41CD52"
        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

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: "#41CD52"
        }
    }
}
// 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" : "#41CD52"
            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: "#41CD52"
        palette.window: "transparent"
        palette.highlight: Config.mainColor

        Menu {
            title: qsTr("&File")
            palette.text: Config.secondaryColor
            palette.window: Config.mainColor
            palette.highlightedText: "#41CD52"

            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

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) {
            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(url.toString())
                })
        })
        playlistUpdated()
    }

    function addFile(index, selectedFile) {
        if (index > mediaCount || index < 0) {
            index = 0
            currentIndex = 0
        }
        files.insert(index,
            {
                path: selectedFile,
                isMovie: isMovie(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 ? "#41CD52" : Config.secondaryColor
                    font.pixelSize: 18
                    text: {
                        const paths = row.path.split('/')
                        return paths[paths.length - 1]
                    }
                }

                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 ? "#41CD52" : "black"
                property color textColor:  bar.currentIndex === index ? "#41CD52" : 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: "#41CD52"
    }

    component MenuItemLabel: Label {
        font.pixelSize: 24
        color: "#41CD52"
        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 ? "#41CD52" : "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: "#41CD52"
        opacity: 0.25
    }

    CustomMenuItem {
        id: openFileMenuItem
        text: qsTr("File")
        bold: true
    }

    Rectangle {
        width: parent.width
        implicitHeight: 1
        color: "#41CD52"
        opacity: 0.25
    }

    CustomMenuItem {
        id: openUrlMenuItem
        text: qsTr("URL")
        bold: true
    }

    Rectangle {
        implicitHeight: 1
        color: "#41CD52"

        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 ? "#41CD52" : "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) {
        const urlPattern = /^((http)|(https)|(rtp)|(rtsp)|(udp)):\/\//
        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, QUrl
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtMultimedia import QMediaFormat

_opt_verbose = False


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())