summaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
authorMatt Ranostay <matt.ranostay@konsulko.com>2017-04-03 22:35:00 -0700
committerMatt Ranostay <matt.ranostay@konsulko.com>2017-04-18 11:52:40 -0700
commitba7c74937dfbe12ab2ef2419c934a3fc6b51c711 (patch)
treeb2d3156533b207c6a52e96732b4f4c33a260fc6e /app
parent3aeb5e52f454b972a423d5e4a359e0e02adec248 (diff)
bluetooth: add a2dp metadata and avrcp controls
Add initial support for Bluetooth A2DP streams, and AVRCP player controls, and metadata. Bug-AGL: SPEC-486 SPEC-524 SPEC-525 Change-Id: Iac3095c517f07d7e65bf0bd5639d85bab2de7451 Signed-off-by: Matt Ranostay <matt.ranostay@konsulko.com>
Diffstat (limited to 'app')
-rw-r--r--app/MediaPlayer.qml120
-rw-r--r--app/dbus.cpp112
-rw-r--r--app/dbus.h18
-rw-r--r--app/main.cpp6
4 files changed, 237 insertions, 19 deletions
diff --git a/app/MediaPlayer.qml b/app/MediaPlayer.qml
index dae74a0..9019a3f 100644
--- a/app/MediaPlayer.qml
+++ b/app/MediaPlayer.qml
@@ -24,30 +24,63 @@ import MediaPlayer 1.0
ApplicationWindow {
id: root
- function clearMetadata() {
- title.text = ''
- artist.text = ''
- duration.text = player.time2str(0)
- albumart.visible = false
+ Item {
+ id: bluetooth
+ property bool connected: false
+ property string state
+
+ property string artist
+ property string title
+ property int duration: 0
+ property int position: 0
+
+ function disableBluetooth() {
+ bluetooth.artist = ''
+ bluetooth.title = ''
+ bluetooth.duration = 0
+ bluetooth.position = 0
+ bluetooth.connected = false
+ }
}
Connections {
target: dbus
+
onProcessPlaylistUpdate: {
playlist.clear()
playlist.addItems(mediaFiles)
playlistmodel.setSource(playlist)
- playlistview.visible = true
- albumart.visible = true
+ playlistview.visible = bluetooth.connected == false
}
onProcessPlaylistHide: {
- player.stop()
playlistview.visible = false
- clearMetadata()
+ player.stop()
}
+ onProcessPlaylistShow: {
+ playlistview.visible = true
+ bluetooth.disableBluetooth()
+ }
+
+ onDisplayBluetoothMetadata: {
+ if (avrcp_artist)
+ bluetooth.artist = avrcp_artist
+ if (avrcp_title)
+ bluetooth.title = avrcp_title
+ bluetooth.duration = avrcp_duration
+ }
+
+ onUpdatePlayerStatus: {
+ bluetooth.connected = true
+ bluetooth.state = status
+ }
+
+ onUpdatePosition: {
+ slider.value = current_position
+ bluetooth.position = current_position
+ }
}
MediaPlayer {
@@ -55,12 +88,25 @@ ApplicationWindow {
audioRole: MediaPlayer.MusicRole
autoLoad: true
playlist: playlist
+
+ property bool is_bluetooth: false
function time2str(value) {
return Qt.formatTime(new Date(value), 'mm:ss')
}
onPositionChanged: slider.value = player.position
}
+ Timer {
+ id: timer
+ interval: 250
+ running: (bluetooth.connected && bluetooth.state == "playing")
+ repeat: true
+ onTriggered: {
+ bluetooth.position = dbus.getCurrentPosition()
+ slider.value = bluetooth.position
+ }
+ }
+
Playlist {
id: playlist
playbackMode: random.checked ? Playlist.Random : loop.checked ? Playlist.Loop : Playlist.Sequential
@@ -86,6 +132,7 @@ ApplicationWindow {
height: sourceSize.height * width / sourceSize.width
fillMode: Image.PreserveAspectCrop
source: player.metaData.coverArtImage ? player.metaData.coverArtImage : ''
+ visible: bluetooth.connected == false
}
Item {
@@ -109,11 +156,13 @@ ApplicationWindow {
spacing: 20
ToggleButton {
id: random
+ visible: bluetooth.connected == false
offImage: './images/AGL_MediaPlayer_Shuffle_Inactive.svg'
onImage: './images/AGL_MediaPlayer_Shuffle_Active.svg'
}
ToggleButton {
id: loop
+ visible: bluetooth.connected == false
offImage: './images/AGL_MediaPlayer_Loop_Inactive.svg'
onImage: './images/AGL_MediaPlayer_Loop_Active.svg'
}
@@ -123,14 +172,13 @@ ApplicationWindow {
Label {
id: title
Layout.alignment: Layout.Center
- text: player.metaData.title ? player.metaData.title : ''
+ text: bluetooth.title ? bluetooth.title : (player.metaData.title ? player.metaData.title : '')
horizontalAlignment: Label.AlignHCenter
verticalAlignment: Label.AlignVCenter
}
Label {
- id: artist
Layout.alignment: Layout.Center
- text: player.metaData.contributingArtist ? player.metaData.contributingArtist : ''
+ text: bluetooth.artist ? bluetooth.artist : (player.metaData.contributingArtist ? player.metaData.contributingArtist : '')
horizontalAlignment: Label.AlignHCenter
verticalAlignment: Label.AlignVCenter
font.pixelSize: title.font.pixelSize * 0.6
@@ -140,20 +188,27 @@ ApplicationWindow {
Slider {
id: slider
Layout.fillWidth: true
- to: player.duration
+ to: bluetooth.connected ? bluetooth.duration : player.duration
+ enabled: bluetooth.connected == false
+ function getPosition() {
+ if (bluetooth.connected && bluetooth.position) {
+ return player.time2str(bluetooth.position)
+ }
+ return player.time2str(player.position)
+ }
Label {
id: position
anchors.left: parent.left
anchors.bottom: parent.top
font.pixelSize: 32
- text: player.time2str(player.position)
+ text: slider.getPosition()
}
Label {
id: duration
anchors.right: parent.right
anchors.bottom: parent.top
font.pixelSize: 32
- text: player.time2str(player.duration)
+ text: bluetooth.connected ? player.time2str(bluetooth.duration) : player.time2str(player.duration)
}
onPressedChanged: player.seek(value)
}
@@ -167,13 +222,26 @@ ApplicationWindow {
// }
Item { Layout.fillWidth: true }
ImageButton {
+ id: previous
offImage: './images/AGL_MediaPlayer_BackArrow.svg'
- onClicked: playlist.previous()
+ onClicked: {
+ if (bluetooth.connected) {
+ dbus.processQMLEvent("Previous")
+ } else {
+ playlist.previous()
+ }
+ }
}
ImageButton {
id: play
offImage: './images/AGL_MediaPlayer_Player_Play.svg'
- onClicked: player.play()
+ onClicked: {
+ if (bluetooth.connected) {
+ dbus.processQMLEvent("Play")
+ } else {
+ player.play()
+ }
+ }
states: [
State {
when: player.playbackState === MediaPlayer.PlayingState
@@ -182,12 +250,28 @@ ApplicationWindow {
offImage: './images/AGL_MediaPlayer_Player_Pause.svg'
onClicked: player.pause()
}
+ },
+ State {
+ when: bluetooth.connected && bluetooth.state == "playing"
+ PropertyChanges {
+ target: play
+ offImage: './images/AGL_MediaPlayer_Player_Pause.svg'
+ onClicked: dbus.processQMLEvent("Pause")
+ }
}
+
]
}
ImageButton {
+ id: forward
offImage: './images/AGL_MediaPlayer_ForwardArrow.svg'
- onClicked: playlist.next()
+ onClicked: {
+ if (bluetooth.connected) {
+ dbus.processQMLEvent("Next")
+ } else {
+ playlist.next()
+ }
+ }
}
Item { Layout.fillWidth: true }
diff --git a/app/dbus.cpp b/app/dbus.cpp
index c0bfcea..4c307a7 100644
--- a/app/dbus.cpp
+++ b/app/dbus.cpp
@@ -59,3 +59,115 @@ void DbusService::lmsUpdate(const QString&, const QVariantMap&, const QStringLis
{
}
#endif
+
+/*
+ * Bluetooth
+ */
+
+void DbusService::setBluezPath(const QString& path)
+{
+ this->bluezPath = path;
+}
+
+QString DbusService::getBluezPath() const
+{
+ return this->bluezPath;
+}
+
+bool DbusService::enableBluetooth()
+{
+ QDBusConnection system_bus = QDBusConnection::systemBus();
+ QString interface = "org.freedesktop.DBus.ObjectManager";
+ bool ret;
+
+ if (!system_bus.isConnected())
+ return false;
+
+ ret = system_bus.connect(QString("org.bluez"), QString("/"), interface, "InterfacesAdded", this, SLOT(newBluetoothDevice(QDBusObjectPath,QVariantMap)));
+
+ if (!ret)
+ return false;
+
+ ret = system_bus.connect(QString("org.bluez"), QString("/"), interface, "InterfacesRemoved", this, SLOT(removeBluetoothDevice(QDBusObjectPath,QStringList)));
+
+ /*
+ * Unregister InterfacesAdded on error condition
+ */
+ if (!ret)
+ system_bus.disconnect(QString("org.bluez"), QString("/"), interface, "InterfacesAdded", this, SLOT(newBluetoothDevice(QString,QVariantMap)));
+
+ return ret;
+}
+
+bool DbusService::checkIfPlayer(const QString& path) const
+{
+ QRegExp regex("^.*/player\\d$");
+ if (regex.exactMatch(path))
+ return true;
+
+ return false;
+}
+
+void DbusService::newBluetoothDevice(const QDBusObjectPath& item, const QVariantMap&)
+{
+ QString path = item.path();
+ if (!checkIfPlayer(path))
+ return;
+
+ if (!getBluezPath().isEmpty()) {
+ qWarning() << "Another Bluetooth Player already connected";
+ return;
+ }
+
+ emit processPlaylistHide();
+
+ QDBusConnection system_bus = QDBusConnection::systemBus();
+ system_bus.connect(QString("org.bluez"), path, "org.freedesktop.DBus.Properties", "PropertiesChanged", this, SLOT(processBluetoothEvent(QString,QVariantMap,QStringList)));
+
+ setBluezPath(path);
+}
+
+void DbusService::removeBluetoothDevice(const QDBusObjectPath& item, const QStringList&)
+{
+ QString path = item.path();
+ if (!checkIfPlayer(path))
+ return;
+
+ if (getBluezPath().isEmpty()) {
+ qWarning() << "No Bluetooth Player connected";
+ return;
+ }
+
+ QDBusConnection system_bus = QDBusConnection::systemBus();
+ system_bus.disconnect(QString("org.bluez"), path, "org.freedesktop.DBus.Properties", "PropertiesChanged", this, SLOT(processBluetoothEvent(QString,QVariantMap,QStringList)));
+
+ setBluezPath(QString());
+ emit processPlaylistShow();
+}
+
+void DbusService::processBluetoothEvent(const QString&, const QVariantMap& map, const QStringList&)
+{
+ if (map.contains("Track")) {
+ QVariantMap track;
+ map["Track"].value<QDBusArgument>() >> track;
+ emit displayBluetoothMetadata(track["Artist"].toString(), track["Title"].toString(), track["Duration"].toInt());
+ }
+
+ if (map.contains("Position"))
+ emit updatePosition(map["Position"].toInt());
+
+ if (map.contains("Status"))
+ emit updatePlayerStatus(map["Status"].toString());
+}
+
+void DbusService::processQMLEvent(const QString& state)
+{
+ QDBusInterface interface("org.bluez", getBluezPath(), "org.bluez.MediaPlayer1", QDBusConnection::systemBus());
+ interface.call(state);
+}
+
+long DbusService::getCurrentPosition()
+{
+ QDBusInterface interface("org.bluez", getBluezPath(), "org.bluez.MediaPlayer1", QDBusConnection::systemBus());
+ return interface.property("Position").toInt();
+}
diff --git a/app/dbus.h b/app/dbus.h
index d533143..b470166 100644
--- a/app/dbus.h
+++ b/app/dbus.h
@@ -33,14 +33,32 @@ class DbusService : public QObject {
Q_OBJECT
public:
explicit DbusService(QObject *parent = 0);
+
bool enableLMS();
+ bool enableBluetooth();
+ Q_INVOKABLE void processQMLEvent(const QString&);
+ Q_INVOKABLE long getCurrentPosition();
+
+private:
+ void setBluezPath(const QString& path);
+ QString getBluezPath() const;
+ bool checkIfPlayer(const QString& path) const;
+ QString bluezPath;
signals:
void processPlaylistUpdate(const QVariantList& mediaFiles);
void processPlaylistHide();
+ void processPlaylistShow();
+
+ void displayBluetoothMetadata(const QString& avrcp_artist, const QString& avrcp_title, const int avrcp_duration);
+ void updatePosition(const int current_position);
+ void updatePlayerStatus(const QString status);
private slots:
void lmsUpdate(const QString&, const QVariantMap&, const QStringList&);
+ void newBluetoothDevice(const QDBusObjectPath&, const QVariantMap&);
+ void removeBluetoothDevice(const QDBusObjectPath&, const QStringList&);
+ void processBluetoothEvent(const QString&, const QVariantMap&, const QStringList&);
};
#endif
diff --git a/app/main.cpp b/app/main.cpp
index e8d4082..dd84f59 100644
--- a/app/main.cpp
+++ b/app/main.cpp
@@ -87,12 +87,16 @@ int main(int argc, char *argv[])
QQmlContext *context = engine.rootContext();
context->setContextProperty("mediaFiles", mediaFiles);
-#if defined(HAVE_DBUS) && defined(HAVE_LIGHTMEDIASCANNER)
+#if defined(HAVE_DBUS)
DbusService dbus_service;
context->setContextProperty("dbus", &dbus_service);
+#if defined(HAVE_LIGHTMEDIASCANNER)
if (!dbus_service.enableLMS())
qWarning() << "Cannot run enableLMS";
#endif
+ if (!dbus_service.enableBluetooth())
+ qWarning() << "Cannot run enableBluetooth";
+#endif
engine.load(QUrl(QStringLiteral("qrc:/MediaPlayer.qml")));
return app.exec();