diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index cbb2728..5d71973 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -15,7 +15,7 @@ jobs:
- name: Install build dependencies
run: |
sudo apt-get update
- sudo apt-get install meson cmake ninja-build qt6-base-dev qt6-declarative-dev qt6-multimedia-dev
+ sudo apt-get install meson cmake ninja-build qt6-base-dev qt6-declarative-dev qt6-multimedia-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
- name: Build application using Meson Build System
run: |
meson setup build-meson --buildtype=release
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 5b9402d..302fdbb 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -9,6 +9,8 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTORCC ON)
find_package(Qt6 REQUIRED COMPONENTS Core Qml Quick Gui DBus Multimedia)
+find_package(PkgConfig REQUIRED)
+pkg_check_modules(gstreamer REQUIRED IMPORTED_TARGET gstreamer-1.0 gstreamer-pbutils-1.0)
list(APPEND qtphy_sources
resources/resources.qrc
@@ -17,6 +19,8 @@ list(APPEND qtphy_sources
src/device_info.cpp
src/rauc.hpp
src/rauc.cpp
+ src/multimedia_formats.hpp
+ src/multimedia_formats.cpp
)
list(APPEND qtphy_libraries
@@ -26,18 +30,14 @@ list(APPEND qtphy_libraries
Qt6::Gui
Qt6::DBus
Qt6::Multimedia
+ PkgConfig::gstreamer
)
if(QML_SINK)
- find_package(PkgConfig REQUIRED)
- pkg_check_modules(gstreamer REQUIRED IMPORTED_TARGET gstreamer-1.0)
list(APPEND qtphy_sources
src/multimedia_qmlsink.hpp
src/multimedia_qmlsink.cpp
)
- list(APPEND qtphy_libraries
- PkgConfig::gstreamer
- )
add_definitions(-DQML_SINK)
endif()
diff --git a/meson.build b/meson.build
index 985d943..29761a0 100644
--- a/meson.build
+++ b/meson.build
@@ -7,22 +7,22 @@ project(
qt6 = import('qt6')
qt6_dep = dependency('qt6', modules : ['Core', 'Qml', 'Quick', 'Gui', 'DBus', 'Multimedia'])
-exec_dep = [qt6_dep]
+exec_dep = [qt6_dep, dependency('gstreamer-1.0'), dependency('gstreamer-pbutils-1.0')]
headers = [
'src/device_info.hpp',
- 'src/rauc.hpp'
+ 'src/rauc.hpp',
+ 'src/multimedia_formats.hpp'
]
src = [
'src/main.cpp',
'src/device_info.cpp',
- 'src/rauc.cpp'
+ 'src/rauc.cpp',
+ 'src/multimedia_formats.cpp'
]
qmlsink_option = get_option('qmlsink')
if qmlsink_option.enabled()
- gst_dep = dependency('gstreamer-1.0')
- exec_dep += [gst_dep]
add_project_arguments('-DQML_SINK', language : 'cpp')
headers += ['src/multimedia_qmlsink.hpp']
src += ['src/multimedia_qmlsink.cpp']
diff --git a/qtphy.pro b/qtphy.pro
index 9a67b3c..189fb4c 100644
--- a/qtphy.pro
+++ b/qtphy.pro
@@ -3,17 +3,21 @@ TARGET = qtphy
QT += qml quick dbus
+CONFIG += link_pkgconfig
+PKGCONFIG += gstreamer-1.0 gstreamer-pbutils-1.0
+
SOURCES += \
src/main.cpp \
src/device_info.cpp \
- src/rauc.cpp
+ src/rauc.cpp \
+ src/multimedia_formats.cpp
HEADERS += \
src/device_info.hpp \
- src/rauc.hpp
+ src/rauc.hpp \
+ src/multimedia_formats.hpp
qmlsink {
- PKGCONFIG = gstreamer-1.0
SOURCES += src/multimedia_qmlsink.cpp
HEADERS += src/multimedia_qmlsink.hpp
}
diff --git a/resources/controls/PhyConvertDialog.qml b/resources/controls/PhyConvertDialog.qml
new file mode 100644
index 0000000..b4cb920
--- /dev/null
+++ b/resources/controls/PhyConvertDialog.qml
@@ -0,0 +1,366 @@
+import QtQuick 2.0
+import QtQuick.Controls 2.0
+import QtQuick.Layouts 1.0
+import PhyTheme 1.0
+
+Rectangle {
+ id: convertDialog
+ color: PhyTheme.white
+ property string file
+
+ Component {
+ id: codecConvertDelegate
+
+ Item {
+ width: codecSelectorBox.currentText !== "REMOVE" ? ListView.view.width : 0
+ height: codecSelectorBox.currentText !== "REMOVE" ? codecSelectorBox.implicitHeight: 0
+ visible: codecSelectorBox.currentText !== "REMOVE"
+
+ RowLayout {
+ anchors.fill: parent
+ spacing: PhyTheme.marginSmall
+ Label {
+ text: type + " Stream " + subIndex + ":"
+ Layout.alignment: Qt.AlignLeft
+ Layout.leftMargin: PhyTheme.marginRegular
+ Layout.fillWidth: true
+ Layout.horizontalStretchFactor: 2
+ }
+ Label {
+ text: modelData
+ }
+ Label {
+ text: "convert to"
+ horizontalAlignment: Text.AlignHCenter
+ Layout.fillWidth: true
+ Layout.horizontalStretchFactor: 3
+ }
+ ComboBox {
+ id: resolutionSelector
+ visible: type === "Video"
+ model: ["", "960 x 540", "1280 x 720", "1600 x 900", "1920 x 1080"]
+ onCurrentValueChanged: parent.updateModelValue()
+ Layout.alignment: Qt.AlignRight
+ }
+ ComboBox {
+ id: codecSelectorBox
+ textRole: "text"
+ valueRole: "value"
+ displayText: multimediaFormats.format(currentValue, prefix)
+ model: [{"text":"-","value":"-"}]
+ Layout.alignment: Qt.AlignRight
+ onCurrentValueChanged: parent.updateModelValue()
+ Component.onCompleted: {
+ loadCodecModel()
+ containerFormatSelectorBox.onCurrentValueChanged.connect(loadCodecModel)
+ containerDataSelectorBox.onCurrentValueChanged.connect(loadCodecModel)
+ codecSelectorView.onActiveVideoStreamsChanged.connect(loadCodecModel)
+ codecSelectorView.onActiveAudioStreamsChanged.connect(loadCodecModel)
+ }
+
+ function loadCodecModel() {
+ var copyData = {"text": currentText, "value": currentValue}
+ var newModel = multimediaFormats.getEncodeCodecs(false, false, prefix, containerDataSelectorBox.currentValue ?
+ containerFormatSelectorBox.currentValue + ", " + containerDataSelectorBox.currentValue :
+ containerFormatSelectorBox.currentValue)
+ .sort(multimediaFormats.compareEncodeCodecs)
+ .map(codec => ({"text": multimediaFormats.formatWithDeco(codec, true, prefix), "value": codec}))
+ if (newModel.length === 0)
+ newModel = [{"text":"","value":""}]
+ if (currentValue) {
+ if (typeof multimediaGST !== "undefined") {
+ if (type === "Video" && codecSelectorView.activeVideoStreams > 1)
+ newModel.push({"text":"REMOVE","value":""})
+ else if (type === "Audio" && codecSelectorView.activeAudioStreams > 1)
+ newModel.push({"text":"REMOVE","value":""})
+ } else {
+ if ((codecSelectorView.activeVideoStreams + codecSelectorView.activeAudioStreams) > 1)
+ newModel.push({"text":"REMOVE","value":""})
+ }
+ } else {
+ newModel.push({"text":"REMOVE","value":""})
+ }
+ model = newModel
+ var copyIndex = 0
+ if (copyData.text === "REMOVE")
+ copyIndex = find("REMOVE")
+ else if (copyData.value)
+ copyIndex = indexOfValue(copyData.value)
+ currentIndex = copyIndex > 0 ? copyIndex : 0
+ }
+ }
+
+ function updateModelValue() {
+ if (modelValue && !codecSelectorBox.currentValue) {
+ if (type === "Video")
+ codecSelectorView.activeVideoStreams -= 1
+ else if (type === "Audio")
+ codecSelectorView.activeAudioStreams -= 1
+ } else if (!modelValue && codecSelectorBox.currentValue) {
+ if (type === "Video")
+ codecSelectorView.activeVideoStreams += 1
+ else if (type === "Audio")
+ codecSelectorView.activeAudioStreams += 1
+ }
+ if (resolutionSelector.visible && codecSelectorBox.currentValue && resolutionSelector.currentText) {
+ var resolution = resolutionSelector.currentText.split("x")
+ modelValue = codecSelectorBox.currentValue + ",width=" + resolution[0].trim() + ",height=" + resolution[1].trim()
+ } else {
+ modelValue = codecSelectorBox.currentValue
+ }
+ }
+ }
+ }
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: 0
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: PhyTheme.marginRegular
+ Layout.margins: PhyTheme.marginSmall
+
+ Button {
+ text: "Cancel"
+ flat: true
+ onClicked: convertDialogLoader.active = false
+ }
+ Label {
+ Layout.fillWidth: true
+ text: file.replace("file://", "")
+ elide: Text.ElideLeft
+ Layout.leftMargin: PhyTheme.marginRegular
+ Layout.rightMargin: PhyTheme.marginRegular
+ }
+ Button {
+ text: "Convert"
+ flat: true
+ onClicked: {
+ var destinationFile = (fileNameField.text ?? fileNameField.placeholderText) + extensionSelectorBox.currentValue
+ var containerFormat = containerDataSelectorBox.currentValue ?
+ containerFormatSelectorBox.currentValue + ", " + containerDataSelectorBox.currentValue :
+ containerFormatSelectorBox.currentValue
+ var selectedVideoCodecs = []
+ var selectedAudioCodecs = []
+ for (var i = 0; i < codecSelectorView.count; i++) {
+ if (fileCodecsModel.get(i).type === "Video") {
+ selectedVideoCodecs.push(fileCodecsModel.get(i).modelValue)
+ } else if (fileCodecsModel.get(i).type === "Audio") {
+ selectedAudioCodecs.push(fileCodecsModel.get(i).modelValue)
+ }
+ }
+ multimediaFormats.convertFile(file, destinationFile, containerFormat, selectedVideoCodecs, selectedAudioCodecs)
+ }
+ }
+ }
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: PhyTheme.marginSmall
+ Label {
+ text: "Destination:"
+ Layout.leftMargin: 2 * PhyTheme.marginRegular
+ Layout.fillWidth: true
+ }
+ TextField {
+ id: fileNameField
+ Layout.fillWidth: true
+ text: file.replace(new RegExp(".+\\/([^\\/]+)\\.[^.]+$"), "$1") + "_converted"
+ placeholderText: "File Name"
+ }
+ ComboBox {
+ id: extensionSelectorBox
+ model: multimediaFormats.getExtensions(containerFormatSelectorBox.currentValue)
+ .map(extension => "." + extension)
+ Layout.rightMargin: PhyTheme.marginRegular
+ Layout.alignment: Qt.AlignRight
+ }
+ }
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: PhyTheme.marginSmall
+ Label {
+ text: "Container Format:"
+ Layout.leftMargin: 2 * PhyTheme.marginRegular
+ Layout.rightMargin: PhyTheme.marginRegular
+ }
+ ComboBox {
+ id: containerFormatSelectorBox
+ textRole: "text"
+ valueRole: "value"
+ model: multimediaFormats.getContainerFormats()
+ .filter(format => format.startsWith("video/"))
+ .filter(format =>
+ typeof multimediaGST === "undefined" ||
+ (multimediaFormats.getEncodeCodecs(false, false, "video/", format).length > 0 &&
+ multimediaFormats.getEncodeCodecs(false, false, "audio/", format).length > 0))
+ .map(format => format.includes(", ") ? format.slice(0, format.indexOf(", ")) : format)
+ .filter((format, index, formats) => index === formats.indexOf(format) && multimediaFormats.getExtensions(format).length > 0)
+ .map(format => ({"text": multimediaFormats.format(format), "value": format}))
+ Layout.rightMargin: PhyTheme.marginRegular
+ }
+ Label {
+ text: "Data:"
+ Layout.rightMargin: PhyTheme.marginRegular
+ }
+ ComboBox {
+ id: containerDataSelectorBox
+ model: multimediaFormats.getContainerFormats()
+ .filter(format => format === containerFormatSelectorBox.currentValue || format.startsWith(containerFormatSelectorBox.currentValue + ", "))
+ .filter(format =>
+ typeof multimediaGST === "undefined" ||
+ (multimediaFormats.getEncodeCodecs(false, false, "video/", format).length > 0 &&
+ multimediaFormats.getEncodeCodecs(false, false, "audio/", format).length > 0))
+ .map(format => format.includes(", ") ? format.slice(format.indexOf(", ") + 2) : "")
+ delegate: ItemDelegate {
+ required property string modelData
+
+ text: modelData
+ width: containerDataSelectorBox.width
+ contentItem: Text {
+ text: parent.text
+ font: containerDataSelectorBox.font
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ elide: Text.ElideNone
+ wrapMode: Text.WordWrap
+ }
+ }
+ Layout.rightMargin: PhyTheme.marginRegular
+ Layout.fillWidth: true
+ }
+ }
+ ListView {
+ id: codecSelectorView
+ property int activeVideoStreams
+ property int activeAudioStreams
+ clip: true
+ boundsBehavior: Flickable.StopAtBounds
+ model: updateFileCodecModel(file)
+ delegate: codecConvertDelegate
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ Layout.margins: PhyTheme.marginRegular
+ ListModel {
+ id: fileCodecsModel
+ }
+ function updateFileCodecModel(currentFile) {
+ fileCodecsModel.clear()
+ var codecs = multimediaFormats.getFileVideoCodec(currentFile)
+ activeVideoStreams = codecs.length
+ for (var i = 0; i < codecs.length; i++) {
+ fileCodecsModel.append({
+ "type": "Video",
+ "prefix": "video/",
+ "subIndex": i,
+ "modelData": multimediaFormats.format(codecs[i], "video/"),
+ "modelValue": "-"
+ });
+ }
+ codecs = multimediaFormats.getFileAudioCodec(currentFile)
+ activeAudioStreams = codecs.length
+ for (var i = 0; i < codecs.length; i++) {
+ fileCodecsModel.append({
+ "type": "Audio",
+ "prefix": "audio/",
+ "subIndex": i,
+ "modelData": multimediaFormats.format(codecs[i], "audio/"),
+ "modelValue": "-"
+ });
+ }
+ return fileCodecsModel
+ }
+ }
+ }
+
+ Rectangle {
+ id: convertProgress
+ anchors.centerIn: parent
+ visible: multimediaFormats.converting
+ width: parent.width / 2
+ height: parent.height / 2
+ color: PhyTheme.white
+
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.margins: PhyTheme.marginSmall
+ Label {
+ text: "Converting..."
+ Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
+ }
+ Label {
+ id: progressLabel
+ text: formatTime(progressBar.value) + " / " + formatTime(progressBar.to)
+ Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
+
+ function formatTime(nanoseconds) {
+ nanoseconds /= 1000000
+ if (nanoseconds >= 3600000) {
+ return new Date(nanoseconds).toLocaleTimeString(Qt.locale(), "hh:" + "mm:" + "ss:" + "zzz")
+ } else {
+ return new Date(nanoseconds).toLocaleTimeString(Qt.locale(), "mm:" + "ss:" + "zzz")
+ }
+ }
+ }
+ ProgressBar {
+ id: progressBar
+ indeterminate: progressBar.value < 0 || progressBar.to < 0
+ Layout.fillWidth: true
+ }
+ }
+
+ Timer {
+ interval: 100
+ repeat: true
+ running: convertProgress.visible
+ triggeredOnStart: true
+ onTriggered: {
+ progressBar.value = multimediaFormats.getConvertPosition()
+ progressBar.to = multimediaFormats.getConvertDuration()
+ }
+ }
+ }
+
+ Rectangle {
+ id: convertError
+ anchors.centerIn: parent
+ visible: false
+ width: parent.width / 2
+ height: parent.height / 2
+ color: PhyTheme.white
+
+ Component.onCompleted: multimediaFormats.conversionError.connect(onError)
+ Component.onDestruction: multimediaFormats.conversionError.disconnect(onError)
+
+ function onError(message) {
+ convertError.visible = true;
+ errorLabel.text = message;
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.margins: PhyTheme.marginSmall
+ Label {
+ text: "An error occurred:"
+ Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
+ }
+ Label {
+ id: errorLabel
+ wrapMode: Text.WordWrap
+ horizontalAlignment: Text.AlignHCenter
+ Layout.alignment: Qt.AlignVCenter
+ Layout.fillWidth: true
+ }
+ Button {
+ id: errorButton
+ text: "Ok"
+ Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
+ onClicked: {
+ convertError.visible = false
+ errorLabel.text = ""
+ }
+ }
+ }
+ }
+}
diff --git a/resources/controls/PhyFileDialog.qml b/resources/controls/PhyFileDialog.qml
index 0cd59ef..d0a2380 100644
--- a/resources/controls/PhyFileDialog.qml
+++ b/resources/controls/PhyFileDialog.qml
@@ -30,9 +30,24 @@ Rectangle {
id: fileDelegate
Item {
+ id: fileItem
+ property int discoverResult
width: listView.width
height: labelFileName.implicitHeight
+ Component.onCompleted: {
+ if (folderListModel.isFolder(index) || !fileUrl) {
+ discoverResult = 0
+ } else {
+ discoverResult = multimediaFormats.getFileDiscoverResult(fileUrl)
+ if (typeof multimediaGST !== "undefined" && discoverResult === 0) {
+ if (!multimediaFormats.getFileVideoCodec(fileUrl).length || !multimediaFormats.getFileAudioCodec(fileUrl).length)
+ discoverResult = 5
+ }
+ }
+ enabled = discoverResult === 0
+ }
+
RowLayout {
anchors.fill: parent
spacing: PhyTheme.marginSmall
@@ -48,9 +63,28 @@ Rectangle {
elide: Text.ElideMiddle
Layout.fillWidth: true
}
+ Label {
+ Component.onCompleted: {
+ if (folderListModel.isFolder(index) || !fileUrl)
+ return
+ if (fileItem.discoverResult === 0)
+ text = multimediaFormats.formatList(multimediaFormats.getFileVideoCodec(fileUrl)).join(", ")
+ else if (fileItem.discoverResult === 2)
+ text = "unknown"
+ else if (fileItem.discoverResult === 5)
+ text = "unsupported"
+ else
+ text = "failed"
+ }
+ Layout.rightMargin: PhyTheme.marginSmall
+ }
Label {
text: fileSize + " B"
Layout.rightMargin: PhyTheme.marginSmall
+ leftPadding: labelHeaderSize.leftPadding - contentWidth + labelHeaderSize.contentWidth
+ onContentWidthChanged: () => {
+ labelHeaderSize.leftPadding = Math.max(labelHeaderSize.leftPadding, contentWidth - labelHeaderSize.contentWidth)
+ }
}
}
@@ -85,6 +119,7 @@ Rectangle {
}
ColumnLayout {
+ visible: !convertDialogLoader.active
anchors.fill: dialog
spacing: 0
@@ -105,10 +140,21 @@ Rectangle {
Layout.leftMargin: PhyTheme.marginRegular
Layout.rightMargin: PhyTheme.marginRegular
}
+ Button {
+ text: "Convert"
+ flat: true
+ onClicked: {
+ if (listView.currentIndex === -1)
+ return
+ convertDialogLoader.active = true
+ }
+ }
Button {
text: "Open"
flat: true
onClicked: {
+ if (listView.currentIndex === -1)
+ return
dialog.selectedFile = dialog.currentFile
dialog.visible = false
}
@@ -130,6 +176,11 @@ Rectangle {
Layout.leftMargin: PhyTheme.marginSmall
}
Label {
+ text: "Video Codec"
+ Layout.rightMargin: PhyTheme.marginSmall
+ }
+ Label {
+ id: labelHeaderSize
text: "Size"
Layout.rightMargin: PhyTheme.marginSmall
}
@@ -141,6 +192,7 @@ Rectangle {
Layout.fillHeight: true
Layout.fillWidth: true
clip: true
+ currentIndex: -1
boundsBehavior: Flickable.StopAtBounds
model: folderListModel
delegate: fileDelegate
@@ -149,4 +201,12 @@ Rectangle {
}
}
}
+
+ Loader {
+ id: convertDialogLoader
+ active: false
+ source: "PhyConvertDialog.qml"
+ anchors.fill: parent
+ onLoaded: item.file = dialog.currentFile
+ }
}
diff --git a/resources/controls/PhyToolBar.qml b/resources/controls/PhyToolBar.qml
index c806ff9..b0a7357 100644
--- a/resources/controls/PhyToolBar.qml
+++ b/resources/controls/PhyToolBar.qml
@@ -13,23 +13,28 @@ ToolBar {
property string subTitle: ""
property alias buttonBack: buttonBack
property alias buttonMenu: buttonMenu
+ property alias infoMenu: infoMenu
RowLayout {
anchors.fill: parent
- ToolButton {
- id: buttonBack
- text: PhyTheme.iconFont.arrowLeft
- font.family: icons.font.family
- flat: true
- leftPadding: PhyTheme.marginBig
- rightPadding: PhyTheme.marginBig
- topPadding: PhyTheme.marginRegular
- bottomPadding: PhyTheme.marginRegular
+ RowLayout {
+ id: toolBarLeft
+ Layout.fillWidth: false
+ Layout.minimumWidth: toolBarRight.width
+ ToolButton {
+ id: buttonBack
+ text: PhyTheme.iconFont.arrowLeft
+ font.family: icons.font.family
+ flat: true
+ leftPadding: PhyTheme.marginBig
+ rightPadding: PhyTheme.marginBig
+ topPadding: PhyTheme.marginRegular
+ bottomPadding: PhyTheme.marginRegular
+ }
}
ColumnLayout {
- Layout.alignment: Qt.AlignVCenter
-
+ Layout.alignment: Qt.AlignCenter
Label {
text: "" + title + ""
elide: Text.ElideRight
@@ -41,21 +46,38 @@ ToolBar {
visible: text !== ""
elide: Text.ElideLeft
scale: 0.8
+ horizontalAlignment: Text.AlignHCenter
Layout.fillWidth: true
- Layout.alignment: Qt.AlignHCenter
}
}
- ToolButton {
- id: buttonMenu
+ RowLayout {
+ id: toolBarRight
Layout.fillWidth: false
- text: PhyTheme.iconFont.list
- font.family: icons.font.family
- flat: true
- visible: false
- leftPadding: PhyTheme.marginBig
- rightPadding: PhyTheme.marginBig
- topPadding: PhyTheme.marginRegular
- bottomPadding: PhyTheme.marginRegular
+ Layout.minimumWidth: toolBarLeft.width
+ ToolButton {
+ id: infoMenu
+ Layout.fillWidth: false
+ text: PhyTheme.iconFont.info
+ font.family: icons.font.family
+ flat: true
+ visible: false
+ leftPadding: PhyTheme.marginBig
+ rightPadding: PhyTheme.marginBig
+ topPadding: PhyTheme.marginRegular
+ bottomPadding: PhyTheme.marginRegular
+ }
+ ToolButton {
+ id: buttonMenu
+ Layout.fillWidth: false
+ text: PhyTheme.iconFont.list
+ font.family: icons.font.family
+ flat: true
+ visible: false
+ leftPadding: PhyTheme.marginBig
+ rightPadding: PhyTheme.marginBig
+ topPadding: PhyTheme.marginRegular
+ bottomPadding: PhyTheme.marginRegular
+ }
}
}
}
diff --git a/resources/pages/MultimediaInfo.qml b/resources/pages/MultimediaInfo.qml
new file mode 100644
index 0000000..b2d669e
--- /dev/null
+++ b/resources/pages/MultimediaInfo.qml
@@ -0,0 +1,95 @@
+/*
+ * SPDX-License-Identifier: MIT
+ * Copyright (c) 2021 PHYTEC Messtechnik GmbH
+ */
+
+import QtQuick 2.0
+import QtQuick.Controls 2.0
+import QtQuick.Layouts 1.0
+import Phytec.DeviceInfo 1.0
+import PhyTheme 1.0
+import "../controls"
+
+Page {
+ id: infoPage
+ readonly property var videoCodecs: {
+ "hwDecode": multimediaFormats.getDecodeCodecs(true, false, "video/"),
+ "hwEncode": multimediaFormats.getEncodeCodecs(true, false, "video/"),
+ "swDecode": multimediaFormats.getDecodeCodecs(false, true, "video/"),
+ "swEncode": multimediaFormats.getEncodeCodecs(false, true, "video/"),
+ }
+ readonly property var audioCodecs: {
+ "hwDecode": multimediaFormats.getDecodeCodecs(true, false, "audio/"),
+ "hwEncode": multimediaFormats.getEncodeCodecs(true, false, "audio/"),
+ "swDecode": multimediaFormats.getDecodeCodecs(false, true, "audio/"),
+ "swEncode": multimediaFormats.getEncodeCodecs(false, true, "audio/"),
+ }
+
+ header: PhyToolBar {
+ title: "Multimedia Information"
+ buttonBack.onClicked: stack.pop()
+ buttonMenu.visible: false
+ }
+
+ Flickable {
+ id: scrollView
+ anchors.fill: parent
+ contentWidth: content.width
+ contentHeight: content.height
+
+ ColumnLayout {
+ id: content
+ width: scrollView.width
+
+ GridLayout {
+ columns: 2
+ columnSpacing: PhyTheme.marginBig
+ rowSpacing: PhyTheme.marginSmall
+ Layout.margins: PhyTheme.marginRegular
+ Layout.fillWidth: true
+ Layout.alignment: Qt.AlignTop | Qt.AlignLeft
+
+ Row {
+ spacing: PhyTheme.marginRegular
+ Label {
+ text: " " + PhyTheme.iconFont.cpu + " "
+ font.family: icons.font.family
+ color: PhyTheme.white
+ background: Rectangle { color: PhyTheme.teal2 }
+ }
+ Label { text: "
Hardware
" }
+ }
+ Label {}
+ Label { text: "Video Decode Codecs"; color: PhyTheme.gray3 }
+ Label { text: multimediaFormats.formatList(infoPage.videoCodecs["hwDecode"]).join(", "); wrapMode: Text.WordWrap; Layout.fillWidth: true }
+ Label { text: "Video Encode Codecs"; color: PhyTheme.gray3 }
+ Label { text: multimediaFormats.formatList(infoPage.videoCodecs["hwEncode"]).join(", "); wrapMode: Text.WordWrap; Layout.fillWidth: true }
+ Label { text: "Audio Decode Codecs"; color: PhyTheme.gray3 }
+ Label { text: multimediaFormats.formatList(infoPage.audioCodecs["hwDecode"], "audio/").join(", "); wrapMode: Text.WordWrap; Layout.fillWidth: true }
+ Label { text: "Audio Encode Codecs"; color: PhyTheme.gray3 }
+ Label { text: multimediaFormats.formatList(infoPage.audioCodecs["hwEncode"], "audio/").join(", "); wrapMode: Text.WordWrap; Layout.fillWidth: true }
+ Row {
+
+ Layout.topMargin: 2 * PhyTheme.marginBig
+ spacing: PhyTheme.marginRegular
+ Label {
+ text: " " + PhyTheme.iconFont.code + " "
+ font.family: icons.font.family
+ color: PhyTheme.white
+ background: Rectangle { color: PhyTheme.teal2 }
+ }
+ Label { text: "Software
" }
+ }
+ Label {}
+ Label { text: "Video Decode Codecs"; color: PhyTheme.gray3 }
+ Label { text: multimediaFormats.formatList(infoPage.videoCodecs["swDecode"]).join(", "); wrapMode: Text.WordWrap; Layout.fillWidth: true }
+ Label { text: "Video Encode Codecs"; color: PhyTheme.gray3 }
+ Label { text: multimediaFormats.formatList(infoPage.videoCodecs["swEncode"]).join(", "); wrapMode: Text.WordWrap; Layout.fillWidth: true }
+ Label { text: "Audio Decode Codecs"; color: PhyTheme.gray3 }
+ Label { text: multimediaFormats.formatList(infoPage.audioCodecs["swDecode"], "audio/").join(", "); wrapMode: Text.WordWrap; Layout.fillWidth: true }
+ Label { text: "Audio Encode Codecs"; color: PhyTheme.gray3 }
+ Label { text: multimediaFormats.formatList(infoPage.audioCodecs["swEncode"], "audio/").join(", "); wrapMode: Text.WordWrap; Layout.fillWidth: true }
+ }
+ }
+ }
+}
diff --git a/resources/pages/multimedia.qml b/resources/pages/multimedia.qml
index 01f5d26..674b28f 100644
--- a/resources/pages/multimedia.qml
+++ b/resources/pages/multimedia.qml
@@ -19,6 +19,15 @@ Page {
video.pause()
stack.pop()
}
+ infoMenu {
+ text: PhyTheme.iconFont.info
+ font.family: icons.font.family
+ onClicked: {
+ multimediaInfo.visible = true
+ stack.push(multimediaInfo)
+ }
+ visible: true
+ }
buttonMenu {
text: PhyTheme.iconFont.folderOpen
font.family: icons.font.family
@@ -77,5 +86,22 @@ Page {
id: fileDialog
selectedFile: "file:///usr/share/qtphy/videos/caminandes_3_llamigos_720p_vp9.webm"
nameFilters: ["*.webm", "*.mp4"]
+
+ Component.onCompleted: {
+ multimediaFormats.getContainerFormats([], true)
+ .filter(format => format.startsWith("video/"))
+ .map(format => format.includes(", ") ? format.slice(0, format.indexOf(", ")) : format)
+ .filter((format, index, formats) => index === formats.indexOf(format) && multimediaFormats.getExtensions(format).length > 0)
+ .forEach(format => {
+ fileDialog.nameFilters = fileDialog.nameFilters.concat(multimediaFormats.getExtensions(format).map(extension => "*." + extension))
+ }
+ )
+ fileDialog.nameFilters = [...new Set(fileDialog.nameFilters)]
+ }
+ }
+
+ MultimediaInfo {
+ id: multimediaInfo
+ visible: false
}
}
diff --git a/resources/pages/multimedia_qmlsink.qml b/resources/pages/multimedia_qmlsink.qml
index b1d2d51..a7d040e 100644
--- a/resources/pages/multimedia_qmlsink.qml
+++ b/resources/pages/multimedia_qmlsink.qml
@@ -21,6 +21,15 @@ Page {
multimediaGST.pause()
stack.pop()
}
+ infoMenu {
+ text: PhyTheme.iconFont.info
+ font.family: icons.font.family
+ onClicked: {
+ multimediaInfo.visible = true
+ stack.push(multimediaInfo)
+ }
+ visible: true
+ }
buttonMenu {
text: PhyTheme.iconFont.folderOpen
font.family: icons.font.family
@@ -123,5 +132,22 @@ Page {
nameFilters: ["*.webm", "*.mp4"]
onSelectedFileChanged:
multimediaGST.setupNewPipeline(fileDialog.selectedFile)
+
+ Component.onCompleted: {
+ multimediaFormats.getContainerFormats([], true)
+ .filter(format => format.startsWith("video/"))
+ .map(format => format.includes(", ") ? format.slice(0, format.indexOf(", ")) : format)
+ .filter((format, index, formats) => index === formats.indexOf(format) && multimediaFormats.getExtensions(format).length > 0)
+ .forEach(format => {
+ fileDialog.nameFilters = fileDialog.nameFilters.concat(multimediaFormats.getExtensions(format).map(extension => "*." + extension))
+ }
+ )
+ fileDialog.nameFilters = [...new Set(fileDialog.nameFilters)]
+ }
+ }
+
+ MultimediaInfo {
+ id: multimediaInfo
+ visible: false
}
}
diff --git a/resources/resources.qrc b/resources/resources.qrc
index d2c9457..892b9d8 100644
--- a/resources/resources.qrc
+++ b/resources/resources.qrc
@@ -6,6 +6,7 @@
PhyStyle/Label.qml
PhyStyle/ToolBar.qml
PhyStyle/ToolButton.qml
+ controls/PhyConvertDialog.qml
controls/PhyFileDialog.qml
controls/PhyToolBar.qml
fonts/MaterialIcons-Regular.ttf
@@ -21,5 +22,6 @@
themes/PhyTheme/PhyTheme.qml
themes/PhyTheme/qmldir
pages/multimedia_qmlsink.qml
+ pages/MultimediaInfo.qml
diff --git a/resources/themes/PhyTheme/PhyTheme.qml b/resources/themes/PhyTheme/PhyTheme.qml
index 38f9e9c..7a40a6c 100644
--- a/resources/themes/PhyTheme/PhyTheme.qml
+++ b/resources/themes/PhyTheme/PhyTheme.qml
@@ -41,7 +41,7 @@ QtObject {
readonly property string dotsThreeVertical: "\ue5d4"
readonly property string code: "\ue86f"
readonly property string cpu: "\ue322"
- readonly property string file: "\ue66d"
+ readonly property string file: "\ue24d"
readonly property string folder: "\ue2c7"
readonly property string folderOpen: "\ue2c8"
readonly property string frameCorners: "\ue3c2"
@@ -56,5 +56,6 @@ QtObject {
readonly property string stop: "\ue047"
readonly property string skipBack: "\ue045"
readonly property string skipForward: "\ue044"
+ readonly property string info: "\ue88e"
}
}
diff --git a/src/main.cpp b/src/main.cpp
index c0ff120..8df7774 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -12,6 +12,7 @@
#include
#include "device_info.hpp"
#include "rauc.hpp"
+#include "multimedia_formats.hpp"
#ifdef QML_SINK
#include "multimedia_qmlsink.hpp"
@@ -87,6 +88,9 @@ int main(int argc, char *argv[])
engine.rootContext()->setContextProperty("multimediaGST", multimediaGST);
#endif
+ MultimediaFormats *multimediaFormats = new MultimediaFormats(&app, argc, argv);
+ engine.rootContext()->setContextProperty("multimediaFormats", multimediaFormats);
+
engine.load(QUrl(QStringLiteral("qrc:///main.qml")));
#ifdef QML_SINK
diff --git a/src/multimedia_formats.cpp b/src/multimedia_formats.cpp
new file mode 100644
index 0000000..68f9d73
--- /dev/null
+++ b/src/multimedia_formats.cpp
@@ -0,0 +1,547 @@
+#include "multimedia_formats.hpp"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+MultimediaFormats::MultimediaFormats(QObject *parent, int argc, char *argv[])
+ : QObject(parent), convData() {
+ gst_init (&argc, &argv);
+
+ findCodecs();
+}
+
+void MultimediaFormats::findCodecs() {
+ //Find all encodable codecs
+ GList *factories = gst_element_factory_list_get_elements(GST_ELEMENT_FACTORY_TYPE_ENCODER, GST_RANK_NONE);
+ for (GList *l = factories; l != NULL; l = l->next) {
+ GstElementFactory *factory = GST_ELEMENT_FACTORY(l->data);
+ const gchar *name = gst_plugin_feature_get_name(factory);
+ guint rank = gst_plugin_feature_get_rank(GST_PLUGIN_FEATURE(factory));
+ const GList *pads = gst_element_factory_get_static_pad_templates(factory);
+ bool hardware = g_str_has_prefix(name, "vpu") || rank > 256
+ || gst_element_factory_list_is_type(factory, GST_ELEMENT_FACTORY_TYPE_HARDWARE)
+ || QString(gst_element_factory_get_metadata(factory, GST_ELEMENT_METADATA_KLASS)).contains(GST_ELEMENT_FACTORY_KLASS_HARDWARE);
+ while (pads) {
+ GstStaticPadTemplate *padtemplate = (GstStaticPadTemplate *) pads->data;
+ if (padtemplate->direction == GST_PAD_SRC) {
+ GstCaps *caps = gst_static_pad_template_get_caps(padtemplate);
+ for (guint i = 0; i < gst_caps_get_size(caps); i++) {
+ GstStructure *structure = gst_caps_get_structure(caps, i);
+ QString codecName = QString(gst_structure_get_name(structure));
+ if (codecName.endsWith("/x-raw") || codecName.startsWith("unknown/"))
+ continue;
+ CodecData &codecData = codecs[codecName];
+ codecData.encode = true;
+ if (codecData.encodeRank < rank)
+ codecData.encodeRank = rank;
+ codecData.hardwareEncode |= hardware;
+ }
+ gst_caps_unref(caps);
+ }
+ pads = pads->next;
+ }
+ }
+ gst_plugin_feature_list_free(factories);
+ //Find all decodable codecs
+ factories = gst_element_factory_list_get_elements(GST_ELEMENT_FACTORY_TYPE_DECODER, GST_RANK_NONE);
+ for (GList *l = factories; l != NULL; l = l->next) {
+ GstElementFactory *factory = GST_ELEMENT_FACTORY(l->data);
+ const gchar *name = gst_plugin_feature_get_name(factory);
+ guint rank = gst_plugin_feature_get_rank(GST_PLUGIN_FEATURE(factory));
+ const GList *pads = gst_element_factory_get_static_pad_templates(factory);
+ bool hardware = g_str_has_prefix(name, "vpu") || rank > 256
+ || gst_element_factory_list_is_type(factory, GST_ELEMENT_FACTORY_TYPE_HARDWARE)
+ || QString(gst_element_factory_get_metadata(factory, GST_ELEMENT_METADATA_KLASS)).contains(GST_ELEMENT_FACTORY_KLASS_HARDWARE);
+ while (pads) {
+ GstStaticPadTemplate *padtemplate = (GstStaticPadTemplate *) pads->data;
+ if (padtemplate->direction == GST_PAD_SINK) {
+ GstCaps *caps = gst_static_pad_template_get_caps(padtemplate);
+ for (guint i = 0; i < gst_caps_get_size(caps); i++) {
+ GstStructure *structure = gst_caps_get_structure(caps, i);
+ QString codecName = QString(gst_structure_get_name(structure));
+ if (codecName.endsWith("/x-raw") || codecName.startsWith("unknown/"))
+ continue;
+ CodecData &codecData = codecs[codecName];
+ codecData.decode = true;
+ if (codecData.decodeRank < rank)
+ codecData.decodeRank = rank;
+ codecData.hardwareDecode |= hardware;
+ }
+ gst_caps_unref(caps);
+ }
+ pads = pads->next;
+ }
+ }
+ gst_plugin_feature_list_free(factories);
+ //Find all container formats for encoding and their supported codecs
+ factories = gst_element_factory_list_get_elements(GST_ELEMENT_FACTORY_TYPE_MUXER, GST_RANK_NONE);
+ for (GList *l = factories; l != NULL; l = l->next) {
+ GstElementFactory *factory = GST_ELEMENT_FACTORY(l->data);
+ bool hasVideo = false;
+ QSet avaiableCodecs;
+ QSet containerFormats;
+ const GList *pads = gst_element_factory_get_static_pad_templates(factory);
+ while (pads) {
+ GstStaticPadTemplate *padtemplate = (GstStaticPadTemplate *) pads->data;
+ if (padtemplate->direction == GST_PAD_SINK) {
+ GstCaps *caps = gst_static_pad_template_get_caps(padtemplate);
+ for (guint i = 0; i < gst_caps_get_size(caps); i++) {
+ GstStructure *structure = gst_caps_get_structure(caps, i);
+ QString codecName = QString(gst_structure_get_name(structure));
+ if (codecs.value(codecName).encode) {
+ avaiableCodecs.insert(codecName);
+ if (codecName.startsWith("video/")) {
+ hasVideo = true;
+ }
+ }
+ }
+ gst_caps_unref(caps);
+ } else if (padtemplate->direction == GST_PAD_SRC) {
+ GstCaps *caps = gst_static_pad_template_get_caps(padtemplate);
+ for (guint i = 0; i < gst_caps_get_size(caps); i++) {
+ GstStructure *structure = gst_caps_get_structure(caps, i);
+ gchar *structureStr = gst_structure_to_string(structure);
+ containerFormats.insert(QString(structureStr).section(';', 0, 0));
+ g_free(structureStr);
+ }
+ gst_caps_unref(caps);
+ }
+ pads = pads->next;
+ }
+ if (!avaiableCodecs.empty() && hasVideo) {
+ for (const auto& format : containerFormats) {
+ containers[format].encodeCodecs = avaiableCodecs;
+ }
+ }
+ }
+ gst_plugin_feature_list_free(factories);
+ //Find all container formats for decoding and their supported codecs
+ factories = gst_element_factory_list_get_elements(GST_ELEMENT_FACTORY_TYPE_DEMUXER, GST_RANK_NONE);
+ for (GList *l = factories; l != NULL; l = l->next) {
+ GstElementFactory *factory = GST_ELEMENT_FACTORY(l->data);
+ bool hasVideo = false;
+ QSet avaiableCodecs;
+ QSet containerFormats;
+ const GList *pads = gst_element_factory_get_static_pad_templates(factory);
+ while (pads) {
+ GstStaticPadTemplate *padtemplate = (GstStaticPadTemplate *) pads->data;
+ if (padtemplate->direction == GST_PAD_SRC) {
+ GstCaps *caps = gst_static_pad_template_get_caps(padtemplate);
+ for (guint i = 0; i < gst_caps_get_size(caps); i++) {
+ GstStructure *structure = gst_caps_get_structure(caps, i);
+ QString codecName = QString(gst_structure_get_name(structure));
+ if (codecs.value(codecName).encode) {
+ avaiableCodecs.insert(codecName);
+ if (codecName.startsWith("video/")) {
+ hasVideo = true;
+ }
+ }
+ }
+ gst_caps_unref(caps);
+ } else if (padtemplate->direction == GST_PAD_SINK) {
+ GstCaps *caps = gst_static_pad_template_get_caps(padtemplate);
+ for (guint i = 0; i < gst_caps_get_size(caps); i++) {
+ GstStructure *structure = gst_caps_get_structure(caps, i);
+ gchar *structureStr = gst_structure_to_string(structure);
+ containerFormats.insert(QString(structureStr).section(';', 0, 0));
+ g_free(structureStr);
+ }
+ gst_caps_unref(caps);
+ }
+ pads = pads->next;
+ }
+ if (!avaiableCodecs.empty() && hasVideo) {
+ for (const auto& format : containerFormats) {
+ containers[format].decodeCodecs = avaiableCodecs;
+ }
+ }
+ }
+ gst_plugin_feature_list_free(factories);
+}
+
+void MultimediaFormats::cleanConvPipeline() {
+ if (!convData.pipeline) return;
+ gst_element_set_state(convData.pipeline, GST_STATE_NULL);
+ gst_object_unref(convData.pipeline);
+ convData.pipeline = NULL;
+ convData.source = NULL;
+ convData.decoder = NULL;
+ convData.encoder = NULL;
+ convData.sink = NULL;
+ emit convertingChanged();
+}
+
+MultimediaFormats::~MultimediaFormats() {
+ cleanConvPipeline();
+ gst_deinit();
+}
+
+void MultimediaFormats::convertFile(QString sourceUri, QString destinationUri, QString containerFormat, QStringList videoCodecs, QStringList audioCodecs) {
+ cleanConvPipeline();
+ convData.pipeline = gst_pipeline_new("convert-pipeline");
+ convData.source = gst_element_factory_make("filesrc", "source");
+ convData.decoder = gst_element_factory_make("decodebin", "decoder");
+ convData.encoder = gst_element_factory_make("encodebin", "encoder");
+ convData.sink = gst_element_factory_make("filesink", "sink");
+ convData.videoCodecs = videoCodecs;
+ convData.audioCodecs = audioCodecs;
+
+ if (!convData.pipeline || !convData.source || !convData.decoder || !convData.encoder || !convData.sink) {
+ qCritical() << "Not all elements for conversion pipeline could be created";
+ emit conversionError("Not all elements for conversion pipeline could be created");
+ cleanConvPipeline();
+ return;
+ }
+
+ // Select the format of the container (e.g. mpeg/matroska) from the parameter
+ GstCaps *caps = gst_caps_from_string(containerFormat.toStdString().c_str());
+ GstEncodingContainerProfile *containerProfile = gst_encoding_container_profile_new("container", NULL, caps, NULL);
+ gst_caps_unref(caps);
+
+ // Select video codecs for encoding from parameter
+ for (const auto &codec : videoCodecs) {
+ if (codec.isEmpty())
+ continue;
+ caps = gst_caps_from_string(codec.toStdString().c_str());
+ GstEncodingVideoProfile *videoProfile = gst_encoding_video_profile_new(caps, NULL, NULL, 1);
+ gst_encoding_container_profile_add_profile(containerProfile, (GstEncodingProfile *) videoProfile);
+ gst_caps_unref(caps);
+ }
+
+ // Select audio codecs for encoding from parameter
+ for (const auto &codec : audioCodecs) {
+ if (codec.isEmpty())
+ continue;
+ caps = gst_caps_from_string(codec.toStdString().c_str());
+ GstEncodingAudioProfile *audioProfile = gst_encoding_audio_profile_new(caps, NULL, NULL, 1);
+ gst_encoding_container_profile_add_profile(containerProfile, (GstEncodingProfile *) audioProfile);
+ gst_caps_unref(caps);
+ }
+
+ g_object_set(convData.encoder, "profile", containerProfile, NULL);
+ g_object_set(convData.source, "location", sourceUri.replace("file://", "").toStdString().c_str(), NULL);
+ g_object_set(convData.sink, "location", destinationUri.replace("file://", "").toStdString().c_str(), NULL);
+
+ // Build the pipeline
+ gst_bin_add_many(GST_BIN(convData.pipeline), convData.source, convData.decoder, convData.encoder, convData.sink, NULL);
+ // Link source to decoder, encoder to sink
+ if (!gst_element_link(convData.source, convData.decoder) || !gst_element_link(convData.encoder, convData.sink)) {
+ qCritical() << "Could not link all elements in conversion pipeline";
+ emit conversionError("Could not link all elements in conversion pipeline");
+ cleanConvPipeline();
+ return;
+ }
+ // Dynamically link decoder and encoder
+ g_signal_connect(convData.decoder, "pad-added", G_CALLBACK((+[](GstElement *src, GstPad *pad, MultimediaFormats *multimediaFormats) {
+ GstCaps* caps = gst_pad_get_current_caps(pad);
+ GstStructure* structure = gst_caps_get_structure(caps, 0);
+ const gchar *name = gst_structure_get_name(structure);
+ gst_caps_unref(caps);
+
+ if ((g_str_has_prefix(name, "video") && multimediaFormats->convData.videoCodecs.takeLast().isEmpty())
+ || (g_str_has_prefix(name, "audio") && multimediaFormats->convData.audioCodecs.takeLast().isEmpty())) {
+ return;
+ }
+ GstPad *sinkPad = gst_element_get_compatible_pad(multimediaFormats->convData.encoder, pad, NULL);
+
+ if (sinkPad) {
+ gst_pad_link(pad, sinkPad);
+ gst_object_unref(sinkPad);
+ } else {
+ qCritical() << "Failed to get matching pad for pad with name:" << name;
+ emit multimediaFormats->conversionError("Could not dynamically link all encoder pads to decoder");
+ g_idle_add(G_SOURCE_FUNC(+[](MultimediaFormats *multimediaFormats) {
+ multimediaFormats->cleanConvPipeline();
+ return G_SOURCE_REMOVE;
+ }), multimediaFormats);
+ }
+ })), this);
+
+ // Run the pipeline
+ GstStateChangeReturn ret = gst_element_set_state(convData.pipeline, GST_STATE_PLAYING);
+ if (ret == GST_STATE_CHANGE_FAILURE) {
+ qCritical() << "Conversion pipeline doesn't want to pause";
+ emit conversionError("Conversion pipeline doesn't want to pause");
+ cleanConvPipeline();
+ return;
+ }
+
+ // Listen for EOS or error from the pipeline
+ GstBus *bus = gst_element_get_bus(convData.pipeline);
+ gst_bus_add_watch(bus, +[](GstBus* bus, GstMessage* message, gpointer data) -> gboolean {
+ auto *multimediaFormats = static_cast(data);
+ if (GST_MESSAGE_TYPE(message) == GST_MESSAGE_EOS) {
+ multimediaFormats->cleanConvPipeline();
+ return G_SOURCE_REMOVE;
+ } else if (GST_MESSAGE_TYPE(message) == GST_MESSAGE_ERROR) {
+ GError* error;
+ gchar* debug;
+ gst_message_parse_error(message, &error, &debug);
+ qCritical() << "Conversion pipeline exited with error:" << error->message;
+ emit multimediaFormats->conversionError(QString("Conversion pipeline exited with error: ") + error->message);
+ g_error_free(error);
+ g_free(debug);
+ multimediaFormats->cleanConvPipeline();
+ return G_SOURCE_REMOVE;
+ }
+ return G_SOURCE_CONTINUE;
+ }, this);
+ gst_object_unref(bus);
+
+ emit convertingChanged();
+}
+
+bool MultimediaFormats::isConverting() {
+ return convData.pipeline;
+}
+
+gint64 MultimediaFormats::getConvertDuration() {
+ gint64 duration;
+ if (convData.pipeline && gst_element_query_duration(convData.pipeline, GST_FORMAT_TIME, &duration))
+ return duration;
+ return -1;
+}
+
+gint64 MultimediaFormats::getConvertPosition() {
+ gint64 position;
+ if (convData.pipeline && gst_element_query_position(convData.pipeline, GST_FORMAT_TIME, &position))
+ return position;
+ return -1;
+}
+
+bool MultimediaFormats::isHardwareCodec(QString codec, bool encode) {
+ if (encode) {
+ return codecs.value(codec).hardwareEncode;
+ } else {
+ return codecs.value(codec).hardwareDecode;
+ }
+}
+
+QString MultimediaFormats::format(QString codec, QString prefix) {
+ return codec.replace(QRegularExpression("^" + prefix + "(x-)?"), "");
+}
+
+QString MultimediaFormats::formatWithDeco(QString codec, bool encode, QString prefix) {
+ if (isHardwareCodec(codec, encode)) {
+ return format(codec, prefix);
+ } else {
+ return "" + format(codec, prefix) + "";
+ }
+}
+
+QStringList MultimediaFormats::formatList(QStringList codecs, QString prefix) {
+ return codecs.replaceInStrings(QRegularExpression("^" + prefix + "(x-)?"), "");
+}
+
+int MultimediaFormats::compareEncodeCodecs(QString codec1, QString codec2) {
+ int hardware = (2 * isHardwareCodec(codec2, true) + isHardwareCodec(codec2, false)) - (2 * isHardwareCodec(codec1, true) + isHardwareCodec(codec1, false));
+ if (hardware != 0)
+ return hardware;
+ return getCodecRank(codec2, true) - getCodecRank(codec1, true);
+}
+
+guint MultimediaFormats::getCodecRank(QString codec, bool encode) {
+ if (encode) {
+ return codecs.value(codec).encodeRank;
+ } else {
+ return codecs.value(codec).decodeRank;
+ }
+}
+
+int MultimediaFormats::getFileDiscoverResult(QString uri) {
+ GError *err = NULL;
+ GstDiscoverer *discoverer = gst_discoverer_new(GST_SECOND, &err);
+ GstDiscovererInfo *info = gst_discoverer_discover_uri(discoverer, uri.toStdString().c_str(), &err);
+ int result = gst_discoverer_info_get_result(info);
+ if (err) {
+ g_error_free(err);
+ }
+ if (info) {
+ gst_discoverer_info_unref(info);
+ }
+ g_object_unref(discoverer);
+ return result;
+}
+
+QString MultimediaFormats::getFileFormat(QString uri) {
+ GError *err = NULL;
+ GstDiscoverer *discoverer = gst_discoverer_new(GST_SECOND, &err);
+ GstDiscovererInfo *info = gst_discoverer_discover_uri(discoverer, uri.toStdString().c_str(), &err);
+ QString format;
+ if (info) {
+ GstDiscovererStreamInfo* streamInfo = gst_discoverer_info_get_stream_info(info);
+ if (streamInfo) {
+ GstCaps* caps = gst_discoverer_stream_info_get_caps(streamInfo);
+ if (caps) {
+ gchar* format_name = gst_caps_to_string(caps);
+ format = QString(format_name);
+ g_free(format_name);
+ gst_caps_unref(caps);
+ }
+ }
+ gst_discoverer_info_unref(info);
+ }
+ if (err) {
+ g_error_free(err);
+ }
+ g_object_unref(discoverer);
+ return format;
+}
+
+QStringList MultimediaFormats::getFileVideoCodec(QString uri) {
+ GError *err = NULL;
+ GstDiscoverer *discoverer = gst_discoverer_new(GST_SECOND, &err);
+ GstDiscovererInfo *info = gst_discoverer_discover_uri(discoverer, uri.toStdString().c_str(), &err);
+ QStringList codecs;
+ if (info) {
+ GList *videoStreams = gst_discoverer_info_get_video_streams(info);
+ for (GList *l = videoStreams; l != NULL; l = l->next) {
+ GstDiscovererStreamInfo *streamInfo = (GstDiscovererStreamInfo *)l->data;
+ GstCaps *caps = gst_discoverer_stream_info_get_caps(streamInfo);
+ if (!caps)
+ continue;
+ for (guint i = 0; i < gst_caps_get_size(caps); i++) {
+ GstStructure *structure = gst_caps_get_structure(caps, i);
+ codecs.append(QString(gst_structure_get_name(structure)));
+ }
+ gst_caps_unref(caps);
+ }
+ gst_discoverer_stream_info_list_free(videoStreams);
+ gst_discoverer_info_unref(info);
+ }
+ if (err) {
+ g_error_free(err);
+ }
+ g_object_unref(discoverer);
+ return codecs;
+}
+
+QStringList MultimediaFormats::getFileAudioCodec(QString uri) {
+ GError *err = NULL;
+ GstDiscoverer *discoverer = gst_discoverer_new(GST_SECOND, &err);
+ GstDiscovererInfo *info = gst_discoverer_discover_uri(discoverer, uri.toStdString().c_str(), &err);
+ QStringList codecs;
+ if (info) {
+ GList *audioStreams = gst_discoverer_info_get_audio_streams(info);
+ for (GList *l = audioStreams; l != NULL; l = l->next) {
+ GstDiscovererStreamInfo *streamInfo = (GstDiscovererStreamInfo *)l->data;
+ GstCaps *caps = gst_discoverer_stream_info_get_caps(streamInfo);
+ if (!caps)
+ continue;
+ for (guint i = 0; i < gst_caps_get_size(caps); i++) {
+ GstStructure *structure = gst_caps_get_structure(caps, i);
+ codecs.append(QString(gst_structure_get_name(structure)));
+ }
+ gst_caps_unref(caps);
+ }
+ gst_discoverer_stream_info_list_free(audioStreams);
+ gst_discoverer_info_unref(info);
+ }
+ if (err) {
+ g_error_free(err);
+ }
+ g_object_unref(discoverer);
+ return codecs;
+}
+
+QStringList MultimediaFormats::getEncodeCodecs(bool onlyHardware, bool onlySoftware, QString prefix, QString containerFormat) {
+ QStringList selectedCodecs;
+ if (containerFormat.isNull()) {
+ for (auto [codec, data] : codecs.asKeyValueRange()) {
+ if (data.encode && !(onlyHardware && !data.hardwareEncode)
+ && !(onlySoftware && data.hardwareEncode) && codec.startsWith(prefix)) {
+ selectedCodecs.append(codec);
+ }
+ }
+ } else {
+ for (const auto &codec : containers.value(containerFormat).encodeCodecs) {
+ if (!codecs.contains(codec))
+ continue;
+ CodecData data = codecs.value(codec);
+ if (data.encode && !(onlyHardware && !data.hardwareEncode)
+ && !(onlySoftware && data.hardwareEncode) && codec.startsWith(prefix)) {
+ selectedCodecs.append(codec);
+ }
+ }
+ }
+ return selectedCodecs;
+}
+
+QStringList MultimediaFormats::getDecodeCodecs(bool onlyHardware, bool onlySoftware, QString prefix, QString containerFormat) {
+ QStringList selectedCodecs;
+ if (containerFormat.isNull()) {
+ for (auto [codec, data] : codecs.asKeyValueRange()) {
+ if (data.decode && !(onlyHardware && !data.hardwareDecode)
+ && !(onlySoftware && data.hardwareDecode) && codec.startsWith(prefix)) {
+ selectedCodecs.append(codec);
+ }
+ }
+ } else {
+ for (const auto &codec : containers.value(containerFormat).decodeCodecs) {
+ if (!codecs.contains(codec))
+ continue;
+ CodecData data = codecs.value(codec);
+ if (data.decode && !(onlyHardware && !data.hardwareDecode)
+ && !(onlySoftware && data.hardwareDecode) && codec.startsWith(prefix)) {
+ selectedCodecs.append(codec);
+ }
+ }
+ }
+ return selectedCodecs;
+}
+
+QStringList MultimediaFormats::getContainerFormats(QStringList codecs, bool all, bool encode) {
+ QStringList containerFormats;
+ if (all) {
+ for (auto [key, value] : containers.asKeyValueRange()) {
+ containerFormats.append(key);
+ for (const auto &codec : codecs) {
+ if (!value.encodeCodecs.contains(codec) && !value.decodeCodecs.contains(codec)) {
+ containerFormats.removeLast();
+ break;
+ }
+ }
+ }
+ } else {
+ for (auto [key, value] : containers.asKeyValueRange()) {
+ if ((encode && value.encodeCodecs.isEmpty())
+ || (!encode && value.decodeCodecs.isEmpty()))
+ continue;
+ containerFormats.append(key);
+ for (const auto &codec : codecs) {
+ if ((encode && !value.encodeCodecs.contains(codec))
+ || (!encode && !value.decodeCodecs.contains(codec))) {
+ containerFormats.removeLast();
+ break;
+ }
+ }
+ }
+ }
+ return containerFormats;
+}
+
+QStringList MultimediaFormats::getExtensions(QString containerFormat) {
+ QSet extensionList;
+ GstCaps* caps = gst_caps_from_string(containerFormat.toStdString().c_str());
+ GList *factories = gst_type_find_factory_get_list();
+ for (GList* l = factories; l != NULL; l = l->next) {
+ GstTypeFindFactory* factory = GST_TYPE_FIND_FACTORY(l->data);
+ GstCaps* f_caps = gst_type_find_factory_get_caps(factory);
+
+ if (f_caps && gst_caps_can_intersect(caps, f_caps)) {
+ const gchar* const* extensions = gst_type_find_factory_get_extensions(factory);
+ if (extensions) {
+ for (int j = 0; extensions[j] != NULL; j++) {
+ extensionList.insert(extensions[j]);
+ }
+ }
+ }
+ }
+ gst_caps_unref(caps);
+ gst_plugin_feature_list_free(factories);
+ return extensionList.values();
+}
diff --git a/src/multimedia_formats.hpp b/src/multimedia_formats.hpp
new file mode 100644
index 0000000..91dd446
--- /dev/null
+++ b/src/multimedia_formats.hpp
@@ -0,0 +1,77 @@
+#ifndef MULTIMEDIA_FORMATS_HPP
+#define MULTIMEDIA_FORMATS_HPP
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+struct ConversionData {
+ GstElement *pipeline;
+ GstElement *source;
+ GstElement *decoder;
+ GstElement *encoder;
+ GstElement *sink;
+ QStringList videoCodecs;
+ QStringList audioCodecs;
+};
+
+struct CodecData {
+ guint encodeRank;
+ guint decodeRank;
+ bool hardwareEncode;
+ bool hardwareDecode;
+ bool encode;
+ bool decode;
+};
+
+struct ContainerData {
+ QSet encodeCodecs;
+ QSet decodeCodecs;
+};
+
+class MultimediaFormats : public QObject {
+ Q_OBJECT
+ Q_PROPERTY(bool converting READ isConverting NOTIFY convertingChanged)
+
+private:
+ ConversionData convData;
+ QMap codecs;
+ QMap containers;
+ void findCodecs();
+ void cleanConvPipeline();
+
+public:
+ explicit MultimediaFormats(QObject *parent = nullptr, int argc = 0, char *argv[] = nullptr);
+ ~MultimediaFormats();
+ bool isConverting();
+
+public slots:
+ void convertFile(QString sourceUri, QString destinationUri, QString containerFormat, QStringList videoCodecs, QStringList audioCodecs);
+ gint64 getConvertDuration();
+ gint64 getConvertPosition();
+ bool isHardwareCodec(QString codec, bool encode);
+ QString format(QString codec, QString prefix = "video/");
+ QString formatWithDeco(QString codec, bool encode, QString prefix = "video/");
+ QStringList formatList(QStringList codecs, QString prefix = "video/");
+ int compareEncodeCodecs(QString codec1, QString codec2);
+ guint getCodecRank(QString codec, bool encode);
+ int getFileDiscoverResult(QString uri);
+ QString getFileFormat(QString uri);
+ QStringList getFileVideoCodec(QString uri);
+ QStringList getFileAudioCodec(QString uri);
+ QStringList getEncodeCodecs(bool onlyHardware = false, bool onlySoftware = false,
+ QString prefix = QString(), QString containerFormat = QString());
+ QStringList getDecodeCodecs(bool onlyHardware = false, bool onlySoftware = false,
+ QString prefix = QString(), QString containerFormat = QString());
+ QStringList getContainerFormats(QStringList codecs = QStringList(), bool all = false, bool encode = true);
+ QStringList getExtensions(QString containerFormat);
+
+signals:
+ void convertingChanged();
+ void conversionError(QString message);
+};
+
+#endif // MULTIMEDIA_FORMATS_HPP