diff options
author | Matt Ranostay <matt.ranostay@konsulko.com> | 2017-04-03 22:35:00 -0700 |
---|---|---|
committer | Matt Ranostay <matt.ranostay@konsulko.com> | 2017-04-18 11:52:40 -0700 |
commit | ba7c74937dfbe12ab2ef2419c934a3fc6b51c711 (patch) | |
tree | b2d3156533b207c6a52e96732b4f4c33a260fc6e /app | |
parent | 3aeb5e52f454b972a423d5e4a359e0e02adec248 (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.qml | 120 | ||||
-rw-r--r-- | app/dbus.cpp | 112 | ||||
-rw-r--r-- | app/dbus.h | 18 | ||||
-rw-r--r-- | app/main.cpp | 6 |
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(); +} @@ -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(); |