summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorScott Murray <scott.murray@konsulko.com>2022-02-28 13:04:45 -0500
committerScott Murray <scott.murray@konsulko.com>2022-02-28 13:04:45 -0500
commit1332cc7d0a618ee88b4d19813340665332d406ca (patch)
treefe500276505160f5fd8ba8acb46334633ed0788a
parent0de8ac83e6a190d5fc124587d1f9f0a7f0198ce3 (diff)
Add Bluetooth media control supportmarlin_12.93.0marlin/12.93.012.93.0
Rework to expose Bluetooth AVRCP media control in the Bluetooth class, and use that support to implement a Bluetooth backend in the Mediaplayer class. This replaces the scheme that existed with the agl-service-mediaplayer binding where it abstracted away the Bluetooth support itself. However, care has been take to make sure that the exposed API to users of libqtappfw-mediaplayer has not changed. Bug-AGL: SPEC-4231 Signed-off-by: Scott Murray <scott.murray@konsulko.com> Change-Id: I76b4f75621ce0121364eea3259b074bf3067ee88
-rw-r--r--bluetooth/bluetooth.cpp195
-rw-r--r--bluetooth/bluetooth.h106
-rw-r--r--bluetooth/bluetootheventhandler.cpp142
-rw-r--r--bluetooth/bluetootheventhandler.h43
-rw-r--r--mediaplayer/CMakeLists.txt4
-rw-r--r--mediaplayer/MediaplayerBackend.cpp25
-rw-r--r--mediaplayer/MediaplayerBackend.h50
-rw-r--r--mediaplayer/MediaplayerBluezBackend.cpp149
-rw-r--r--mediaplayer/MediaplayerBluezBackend.h70
-rw-r--r--mediaplayer/MediaplayerMpdBackend.cpp139
-rw-r--r--mediaplayer/MediaplayerMpdBackend.h22
-rw-r--r--mediaplayer/MpdEventHandler.cpp16
-rw-r--r--mediaplayer/MpdEventHandler.h3
-rw-r--r--mediaplayer/mediaplayer.cpp138
-rw-r--r--mediaplayer/mediaplayer.h111
15 files changed, 1062 insertions, 151 deletions
diff --git a/bluetooth/bluetooth.cpp b/bluetooth/bluetooth.cpp
index c58b64b..5c7fd31 100644
--- a/bluetooth/bluetooth.cpp
+++ b/bluetooth/bluetooth.cpp
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018-2021 Konsulko Group
+ * Copyright (C) 2018-2022 Konsulko Group
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,10 +23,17 @@
#include "bluetootheventhandler.h"
-Bluetooth::Bluetooth (bool register_agent, QQmlContext *context, QObject * parent) :
+Bluetooth::Bluetooth(bool register_agent,
+ QQmlContext *context,
+ bool handle_media,
+ QObject * parent) :
QObject(parent),
m_context(context),
- m_agent(register_agent)
+ m_agent(register_agent),
+ m_handle_media(handle_media),
+ m_connected(false),
+ m_connected_device(""),
+ m_media_connected(false)
{
m_bluetooth = new BluetoothModel();
BluetoothModelFilter *m_model = new BluetoothModelFilter();
@@ -39,7 +46,7 @@ Bluetooth::Bluetooth (bool register_agent, QQmlContext *context, QObject * paren
m_model->setFilterFixedString("false");
context->setContextProperty("BluetoothDiscoveryModel", m_model);
- m_event_handler = new BluetoothEventHandler(this, register_agent);
+ m_event_handler = new BluetoothEventHandler(this, register_agent, handle_media);
uuids.insert("a2dp", "0000110a-0000-1000-8000-00805f9b34fb");
uuids.insert("avrcp", "0000110e-0000-1000-8000-00805f9b34fb");
@@ -61,12 +68,19 @@ void Bluetooth::setDiscoverable(bool state)
m_discoverable = state;
- emit discoverableChanged();
+ emit discoverableChanged(state);
}
void Bluetooth::start()
{
- bluez_init(m_agent, m_agent, m_event_handler->init_cb, m_event_handler);
+ // NOTE: An explicit "start" is somewhat required at present as calling
+ // bluez_init in the constructor means state update signals get
+ // emitted before the QML in an app seems able to receive them.
+ // A slightly better alternative might be something like a
+ // "refresh" function, or documenting that apps must explicitly
+ // call getters of relevant values on start.
+
+ bluez_init(m_agent, m_agent, m_event_handler->init_cb, m_event_handler);
}
void Bluetooth::discovery_command(bool state)
@@ -153,7 +167,7 @@ void Bluetooth::disconnect(QString device)
bluez_device_disconnect(device_cstr, NULL);
}
-void Bluetooth::send_confirmation(int pincode)
+void Bluetooth::send_confirmation(const int pincode)
{
QString pincode_str;
pincode_str.setNum(pincode);
@@ -163,7 +177,75 @@ void Bluetooth::send_confirmation(int pincode)
bluez_confirm_pairing(pincode_cstr);
}
-void Bluetooth::init_adapter_state(QString adapter)
+void Bluetooth::media_control(MediaAction action)
+{
+ QString action_name;
+ bool action_allowed = true;
+ bluez_media_control_t bluez_action;
+ switch (action) {
+ case Connect:
+ bluez_action = BLUEZ_MEDIA_CONTROL_CONNECT;
+ action_name = "Connect";
+ action_allowed = !m_media_connected;
+ break;
+ case Disconnect:
+ bluez_action = BLUEZ_MEDIA_CONTROL_DISCONNECT;
+ action_name = "Disconnect";
+ action_allowed = m_media_connected;
+ break;
+ case Play:
+ bluez_action = BLUEZ_MEDIA_CONTROL_PLAY;
+ action_name = "Play";
+ break;
+ case Pause:
+ bluez_action = BLUEZ_MEDIA_CONTROL_PAUSE;
+ action_name = "Pause";
+ break;
+ case Stop:
+ bluez_action = BLUEZ_MEDIA_CONTROL_STOP;
+ action_name = "Stop";
+ break;
+ case Next:
+ bluez_action = BLUEZ_MEDIA_CONTROL_NEXT;
+ action_name = "Next";
+ break;
+ case Previous:
+ bluez_action = BLUEZ_MEDIA_CONTROL_PREVIOUS;
+ action_name = "Previous";
+ break;
+ case FastForward:
+ bluez_action = BLUEZ_MEDIA_CONTROL_FASTFORWARD;
+ action_name = "Fastforward";
+ break;
+ case Rewind:
+ bluez_action = BLUEZ_MEDIA_CONTROL_REWIND;
+ action_name = "Rewind";
+ break;
+ case Loop:
+ // Not implemented, but potentially possible with bluez-glib addition
+ default:
+ break;
+ }
+#ifdef BLUETOOTH_EVENT_DEBUG
+ qDebug() << "Bluetooth::media_control: enter, action = " << action_name;
+ qDebug() << "Bluetooth::media_control: m_connected = " << m_connected
+ << ", m_media_connected = " << m_media_connected;
+#endif
+
+ if (!(m_connected && action_allowed)) {
+ qDebug() << "Bluetooth::media_control: not connected or invalid action!";
+ return;
+ }
+
+ QByteArray device_ba = m_connected_device.toLocal8Bit();
+ const char *device_cstr = device_ba.data();
+ bluez_device_avrcp_controls(device_cstr, bluez_action);
+}
+
+
+// Private
+
+void Bluetooth::init_adapter_state(const QString &adapter)
{
// Get initial power state
GVariant *reply = NULL;
@@ -171,7 +253,7 @@ void Bluetooth::init_adapter_state(QString adapter)
if (rc && reply) {
GVariantDict *props_dict = g_variant_dict_new(reply);
gboolean powered = FALSE;
- if (g_variant_dict_lookup(props_dict, "Powered", "b", &powered)) {
+ if (g_variant_dict_lookup(props_dict, "Powered", "b", &powered)) {
if (m_power != powered) {
m_power = powered;
emit powerChanged(m_power);
@@ -179,10 +261,16 @@ void Bluetooth::init_adapter_state(QString adapter)
}
g_variant_dict_unref(props_dict);
g_variant_unref(reply);
- }
+ }
// Get initial device list
refresh_device_list();
+
+ // Do a refresh of the media state to handle the situation where
+ // a client app has been started after a phone has been connected
+ // (and thus misses seeing the related events go by).
+ if (m_handle_media)
+ refresh_media_state();
}
void Bluetooth::refresh_device_list(void)
@@ -202,8 +290,12 @@ void Bluetooth::refresh_device_list(void)
GVariant *var = NULL;
while (g_variant_iter_next(array, "{&sv}", &key, &var)) {
BluetoothDevice *device = m_bluetooth->updateDeviceProperties(nullptr, key, var);
- if (device)
+ if (device) {
m_bluetooth->addDevice(device);
+ if (device->connected()) {
+ update_connected_state(device->id(), true);
+ }
+ }
g_variant_unref(var);
}
@@ -211,6 +303,42 @@ void Bluetooth::refresh_device_list(void)
g_variant_unref(reply);
}
+void Bluetooth::refresh_media_state()
+{
+ if (!(m_handle_media && m_connected && m_connected_device.count()))
+ return;
+
+ QByteArray device_ba = m_connected_device.toLocal8Bit();
+ const char *device_cstr = device_ba.data();
+
+ GVariant *reply = NULL;
+ if (!bluez_get_media_control_properties(device_cstr, &reply))
+ return;
+
+ GVariantDict *props_dict = g_variant_dict_new(reply);
+ if (!props_dict) {
+ g_variant_unref(reply);
+ return;
+ }
+
+ gboolean connected = FALSE;
+ if (g_variant_dict_lookup(props_dict, "Connected", "b", &connected)) {
+ update_media_connected_state(connected);
+
+ GVariant *player_reply = NULL;
+ if(bluez_get_media_player_properties(device_cstr, &player_reply)) {
+ QVariantMap tmp;
+ m_event_handler->parse_media_player_properties(player_reply, tmp);
+ if (!tmp.empty())
+ update_media_properties(tmp);
+
+ g_variant_unref(player_reply);
+ }
+ }
+ g_variant_dict_unref(props_dict);
+ g_variant_unref(reply);
+}
+
void Bluetooth::set_discovery_filter(void)
{
QList<QString> values = uuids.values();
@@ -233,7 +361,7 @@ void Bluetooth::set_discovery_filter(void)
g_free(transport);
}
-void Bluetooth::update_adapter_power(bool powered)
+void Bluetooth::update_adapter_power(const bool powered)
{
if (!powered)
m_bluetooth->removeAllDevices();
@@ -251,11 +379,50 @@ void Bluetooth::update_adapter_power(bool powered)
bool discoverable = m_discoverable;
m_discoverable = false;
if (discoverable != m_discoverable)
- emit discoverableChanged();
+ emit discoverableChanged(false);
+ }
+}
+
+void Bluetooth::update_connected_state(const QString &device, const bool connected)
+{
+#ifdef BLUETOOTH_EVENT_DEBUG
+ qDebug() << "Bluetooth::update_connected_state: device = " << device
+ << ", connected = " << connected;
+#endif
+ if (m_connected != connected) {
+ if (!m_connected) {
+ // Connecting
+ m_connected = true;
+ m_connected_device = device;
+ emit connectedChanged(true);
+ } else if (m_connected_device == device) {
+ // Disconnecting
+ m_connected = false;
+ m_connected_device = "";
+ emit connectedChanged(false);
+ } else {
+ qDebug() << "Bluetooth::update_connected_state: ignoring " << device;
+ }
}
}
-void Bluetooth::request_confirmation(int pincode)
+void Bluetooth::update_media_connected_state(const bool connected)
+{
+#ifdef BLUETOOTH_EVENT_DEBUG
+ qDebug() << "Bluetooth::update_media_connected_state: connected = " << connected;
+#endif
+ if (m_media_connected != connected) {
+ m_media_connected = connected;
+ emit mediaConnectedChanged(connected);
+ }
+}
+
+void Bluetooth::update_media_properties(const QVariantMap &metadata)
+{
+ emit mediaPropertiesChanged(metadata);
+}
+
+void Bluetooth::request_confirmation(const int pincode)
{
QString pincode_str;
pincode_str.setNum(pincode);
diff --git a/bluetooth/bluetooth.h b/bluetooth/bluetooth.h
index c7ef55c..09fb4d9 100644
--- a/bluetooth/bluetooth.h
+++ b/bluetooth/bluetooth.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018-2021 Konsulko Group
+ * Copyright (C) 2018-2022 Konsulko Group
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -26,64 +26,90 @@ class BluetoothEventHandler;
class Bluetooth : public QObject
{
- Q_OBJECT
- Q_PROPERTY(bool power READ power WRITE setPower NOTIFY powerChanged)
- Q_PROPERTY(bool discoverable READ discoverable WRITE setDiscoverable NOTIFY discoverableChanged)
+ Q_OBJECT
+ Q_PROPERTY(bool power READ power WRITE setPower NOTIFY powerChanged)
+ Q_PROPERTY(bool discoverable READ discoverable WRITE setDiscoverable NOTIFY discoverableChanged)
+ Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged)
+ Q_PROPERTY(bool mediaConnected READ mediaConnected NOTIFY mediaConnectedChanged)
- public:
- explicit Bluetooth(bool register_agent, QQmlContext *context, QObject * parent = Q_NULLPTR);
- virtual ~Bluetooth();
+public:
+ explicit Bluetooth(bool register_agent,
+ QQmlContext *context,
+ bool handle_media = false,
+ QObject *parent = Q_NULLPTR);
+ virtual ~Bluetooth();
- void setPower(bool);
- void setDiscoverable(bool);
+ void setPower(bool);
+ void setDiscoverable(bool);
- Q_INVOKABLE void start(void);
+ Q_INVOKABLE void start(void);
- Q_INVOKABLE void start_discovery(void);
- Q_INVOKABLE void stop_discovery(void);
+ Q_INVOKABLE void start_discovery(void);
+ Q_INVOKABLE void stop_discovery(void);
- Q_INVOKABLE void remove_device(QString device);
- Q_INVOKABLE void pair(QString device);
- Q_INVOKABLE void cancel_pair(void);
+ Q_INVOKABLE void remove_device(QString device);
+ Q_INVOKABLE void pair(QString device);
+ Q_INVOKABLE void cancel_pair(void);
- Q_INVOKABLE void connect(QString device, QString uuid);
- Q_INVOKABLE void connect(QString device);
+ Q_INVOKABLE void connect(QString device, QString uuid);
+ Q_INVOKABLE void connect(QString device);
- Q_INVOKABLE void disconnect(QString device, QString uuid);
- Q_INVOKABLE void disconnect(QString device);
+ Q_INVOKABLE void disconnect(QString device, QString uuid);
+ Q_INVOKABLE void disconnect(QString device);
- Q_INVOKABLE void send_confirmation(int pincode);
+ Q_INVOKABLE void send_confirmation(int pincode);
- bool power() const { return m_power; };
- bool discoverable() const { return m_discoverable; };
+ enum MediaAction {
+ Connect, Disconnect,
+ Play, Pause, Stop, Next, Previous, FastForward, Rewind, Loop
+ };
+ Q_ENUM(MediaAction)
- signals:
- void powerChanged(bool state);
- void discoverableChanged();
+ Q_INVOKABLE void media_control(MediaAction action);
+ Q_INVOKABLE void refresh_media_state();
- //void connectionEvent(QJsonObject data);
+ bool power() const { return m_power; };
+ bool discoverable() const { return m_discoverable; };
+ bool connected() const { return m_connected; };
+ bool mediaConnected() const { return m_media_connected; };
+
+signals:
+ void powerChanged(bool state);
+ void discoverableChanged(bool state);
+ void connectedChanged(bool state);
+ void mediaConnectedChanged(bool state);
+
+ void mediaPropertiesChanged(QVariantMap metadata);
void requestConfirmationEvent(QString pincode);
- private:
- QQmlContext *m_context;
- BluetoothModel *m_bluetooth;
- BluetoothEventHandler *m_event_handler;
+private:
+ QQmlContext *m_context;
+ BluetoothModel *m_bluetooth;
+ BluetoothEventHandler *m_event_handler;
bool m_agent;
+ bool m_handle_media;
- void init_adapter_state(QString);
+ void init_adapter_state(const QString &adapter);
void refresh_device_list(void);
- void set_discovery_filter(void);
- void discovery_command(bool);
- void update_adapter_power(bool);
- void request_confirmation(int);
+ void set_discovery_filter(void);
+ void discovery_command(const bool);
+ void update_adapter_power(const bool powered);
+ void update_connected_state(const QString &device, const bool connected);
+ void update_media_connected_state(const bool connected);
+ void update_media_properties(const QVariantMap &metadata);
+ void request_confirmation(const int pincode);
+
+ QString process_uuid(QString uuid) { if (uuid.length() == 36) return uuid; return uuids.value(uuid); };
- QString process_uuid(QString uuid) { if (uuid.length() == 36) return uuid; return uuids.value(uuid); };
+ // values
+ bool m_power;
+ bool m_discoverable;
+ bool m_connected;
+ bool m_media_connected;
- // values
- bool m_power;
- bool m_discoverable;
+ QString m_connected_device;
- QMap<QString, QString> uuids;
+ QMap<QString, QString> uuids;
friend class BluetoothEventHandler;
};
diff --git a/bluetooth/bluetootheventhandler.cpp b/bluetooth/bluetootheventhandler.cpp
index 0acd5f5..a0154bb 100644
--- a/bluetooth/bluetootheventhandler.cpp
+++ b/bluetooth/bluetootheventhandler.cpp
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 Konsulko Group
+ * Copyright (C) 2021,2022 Konsulko Group
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,20 +16,27 @@
#include <QDebug>
#include <bluez-glib.h>
-
#include "bluetootheventhandler.h"
#include "bluetooth.h"
#include "bluetoothmodel.h"
+#undef BLUETOOTH_EVENT_DEBUG
+
-BluetoothEventHandler::BluetoothEventHandler (Bluetooth *parent, bool register_agent) :
+BluetoothEventHandler::BluetoothEventHandler(Bluetooth *parent, bool register_agent, bool handle_media) :
m_parent(parent),
m_agent(register_agent)
{
bluez_add_adapter_event_callback(adapter_event_cb, this);
bluez_add_device_event_callback(device_event_cb, this);
+
if (register_agent)
bluez_add_agent_event_callback(agent_event_cb, this);
+
+ if (handle_media) {
+ bluez_add_media_control_event_callback(media_control_event_cb, this);
+ bluez_add_media_player_event_callback(media_player_event_cb, this);
+ }
}
BluetoothEventHandler::~BluetoothEventHandler()
@@ -38,6 +45,10 @@ BluetoothEventHandler::~BluetoothEventHandler()
void BluetoothEventHandler::handle_init_event(gchar *adapter, gboolean status)
{
+#ifdef BLUETOOTH_EVENT_DEBUG
+ qDebug() << "BluetoothEventHandler::handle_init_event: enter, status = " << (int) status;
+#endif
+
if (status)
m_parent->init_adapter_state(QString(adapter));
else
@@ -48,6 +59,15 @@ void BluetoothEventHandler::handle_adapter_event(gchar *adapter,
bluez_event_t event,
GVariant *properties)
{
+#ifdef BLUETOOTH_EVENT_DEBUG
+ qDebug() << "BluetoothEventHandler::handle_adapter_event: enter";
+ gchar *p = properties ? g_variant_print(properties, TRUE) : g_strdup("(null)");
+ qDebug() << "BluetoothEventHandler::handle_adapter_event: adapter = "
+ << adapter << ", event = " << (int) event;
+ qDebug() << "BluetoothEventHandler::handle_adapter_event: properties = " << p;
+ g_free(p);
+#endif
+
if (!adapter || event != BLUEZ_EVENT_CHANGE)
return;
@@ -71,6 +91,15 @@ void BluetoothEventHandler::handle_device_event(gchar *adapter,
bluez_event_t event,
GVariant *properties)
{
+#ifdef BLUETOOTH_EVENT_DEBUG
+ qDebug() << "BluetoothEventHandler::handle_device_event: enter";
+ gchar *p = properties ? g_variant_print(properties, TRUE) : g_strdup("(null)");
+ qDebug() << "BluetoothEventHandler::handle_device_event: adapter = "
+ << adapter << ", device = " << device << ", event = " << (int) event;
+ qDebug() << "BluetoothEventHandler::handle_device_event: properties = " << p;
+ g_free(p);
+#endif
+
if (!device)
return;
@@ -91,6 +120,72 @@ void BluetoothEventHandler::handle_device_event(gchar *adapter,
// device not previously in model
m_parent->m_bluetooth->addDevice(new_device);
}
+
+ // Update parent's connected state
+ // NOTE: Currently not worrying about multiple devices being connected
+ GVariantDict *props_dict = g_variant_dict_new(properties);
+ if (props_dict) {
+ gboolean connected = FALSE;
+ if (g_variant_dict_lookup(props_dict, "Connected", "b", &connected)) {
+ m_parent->update_connected_state(QString(device), connected);
+ }
+ g_variant_dict_unref(props_dict);
+ }
+}
+
+void BluetoothEventHandler::handle_media_control_event(gchar *adapter,
+ gchar *device,
+ bluez_event_t event,
+ GVariant *properties)
+{
+#ifdef BLUETOOTH_EVENT_DEBUG
+ qDebug() << "BluetoothEventHandler::handle_media_control_event: enter";
+ gchar *p = properties ? g_variant_print(properties, TRUE) : g_strdup("(null)");
+ qDebug() << "BluetoothEventHandler::handle_media_control_event: adapter = "
+ << adapter << ", device = " << device << ", event = " << (int) event;
+ qDebug() << "BluetoothEventHandler::handle_media_control_event: properties = " << p;
+ g_free(p);
+#endif
+
+ if (event != BLUEZ_EVENT_CHANGE)
+ return;
+
+ // Update parent's media connected state
+ // NOTE: Currently not worrying about multiple devices being connected
+ GVariantDict *props_dict = g_variant_dict_new(properties);
+ if (props_dict) {
+ gboolean connected = FALSE;
+ if (g_variant_dict_lookup(props_dict, "Connected", "b", &connected)) {
+ m_parent->update_media_connected_state(connected);
+ }
+ g_variant_dict_unref(props_dict);
+ }
+
+}
+
+void BluetoothEventHandler::handle_media_player_event(gchar *adapter,
+ gchar *device,
+ gchar *player,
+ bluez_event_t event,
+ GVariant *properties)
+{
+#ifdef BLUETOOTH_EVENT_DEBUG
+ qDebug() << "BluetoothEventHandler::handle_media_player_event: enter";
+ gchar *p = properties ? g_variant_print(properties, TRUE) : g_strdup("(null)");
+ qDebug() << "BluetoothEventHandler::handle_media_player_event: adapter = "
+ << adapter << ", device = " << device << ", player = " << player
+ << ", event = " << (int) event;
+ qDebug() << "BluetoothEventHandler::handle_media_player_event: properties = " << p;
+ g_free(p);
+#endif
+
+ if (event != BLUEZ_EVENT_REMOVE) {
+ QVariantMap tmp;
+ parse_media_player_properties(properties, tmp);
+ if (!tmp.empty()) {
+ m_parent->update_media_properties(tmp);
+ }
+ }
}
void BluetoothEventHandler::handle_agent_event(gchar *device,
@@ -119,3 +214,44 @@ void BluetoothEventHandler::handle_pair_event(gchar *device, gboolean status)
qDebug() << "pairing failed";
}
+void BluetoothEventHandler::parse_media_player_properties(GVariant *properties, QVariantMap &metadata)
+{
+ GVariantDict *props_dict = g_variant_dict_new(properties);
+ if (!props_dict)
+ return;
+
+ GVariant *v = NULL;
+ if (g_variant_dict_lookup(props_dict, "Track", "@a{sv}", &v)) {
+ GVariantDict *track_dict = g_variant_dict_new(v);
+ QVariantMap track;
+ gchar *p = NULL;
+
+ if (g_variant_dict_lookup(track_dict, "Title", "&s", &p))
+ track.insert(QString("title"), QVariant(QString(p)));
+
+ if (g_variant_dict_lookup(track_dict, "Artist", "&s", &p))
+ track.insert(QString("artist"), QVariant(QString(p)));
+
+ if (g_variant_dict_lookup(track_dict, "Album", "&s", &p))
+ track.insert(QString("album"), QVariant(QString(p)));
+
+ unsigned int duration = 0;
+ if (g_variant_dict_lookup(track_dict, "Duration", "u", &duration))
+ track.insert(QString("duration"), QVariant(duration));
+
+ g_variant_dict_unref(track_dict);
+
+ metadata.insert("track", track);
+ }
+
+ unsigned int position = 0;
+ if (g_variant_dict_lookup(props_dict, "Position", "u", &position)) {
+ metadata.insert("position", QVariant(position));
+ }
+
+ gchar *p = NULL;
+ if (g_variant_dict_lookup(props_dict, "Status", "&s", &p))
+ metadata.insert(QString("status"), QVariant(QString(p)));
+
+ g_variant_dict_unref(props_dict);
+}
diff --git a/bluetooth/bluetootheventhandler.h b/bluetooth/bluetootheventhandler.h
index ff970f5..c4dd67a 100644
--- a/bluetooth/bluetootheventhandler.h
+++ b/bluetooth/bluetootheventhandler.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 Konsulko Group
+ * Copyright (C) 2021,2022 Konsulko Group
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -22,7 +22,7 @@ class Bluetooth;
class BluetoothEventHandler
{
public:
- explicit BluetoothEventHandler(Bluetooth *parent, bool register_agent);
+ explicit BluetoothEventHandler(Bluetooth *parent, bool register_agent, bool handle_media = false);
virtual ~BluetoothEventHandler();
static void init_cb(gchar *adapter, gboolean status, gpointer user_data) {
@@ -40,6 +40,10 @@ class BluetoothEventHandler
((BluetoothEventHandler*) user_data)->handle_pair_event(device, status);
}
+ // Helper function callable by parent (instead of vice versa), since Bluetooth
+ // object should not expose glib types.
+ void parse_media_player_properties(GVariant *properties, QVariantMap &metadata);
+
private:
Bluetooth *m_parent;
bool m_agent;
@@ -59,11 +63,46 @@ class BluetoothEventHandler
if (user_data)
((BluetoothEventHandler*) user_data)->handle_agent_event(device, event, properties);
}
+
+ static void media_control_event_cb(gchar *adapter,
+ gchar *device,
+ bluez_event_t event,
+ GVariant *properties,
+ gpointer user_data) {
+ if (user_data)
+ ((BluetoothEventHandler*) user_data)->handle_media_control_event(adapter,
+ device,
+ event,
+ properties);
+ }
+
+ static void media_player_event_cb(gchar *adapter,
+ gchar *device,
+ gchar *player,
+ bluez_event_t event,
+ GVariant *properties,
+ gpointer user_data) {
+ if (user_data)
+ ((BluetoothEventHandler*) user_data)->handle_media_player_event(adapter,
+ device,
+ player,
+ event,
+ properties);
+ }
void handle_init_event(gchar *adapter, gboolean status);
void handle_adapter_event(gchar *adapter, bluez_event_t event, GVariant *properties);
void handle_device_event(gchar *adapter, gchar *device, bluez_event_t event, GVariant *properties);
void handle_agent_event(gchar *device, bluez_agent_event_t event, GVariant *properties);
+ void handle_media_control_event(gchar *adapter,
+ gchar *device,
+ bluez_event_t event,
+ GVariant *properties);
+ void handle_media_player_event(gchar *adapter,
+ gchar *device,
+ gchar *player,
+ bluez_event_t event,
+ GVariant *properties);
void handle_connect_event(gchar *device, gboolean status);
void handle_pair_event(gchar *device, gboolean status);
};
diff --git a/mediaplayer/CMakeLists.txt b/mediaplayer/CMakeLists.txt
index 3f77b8d..35dda69 100644
--- a/mediaplayer/CMakeLists.txt
+++ b/mediaplayer/CMakeLists.txt
@@ -5,13 +5,15 @@ install(FILES ${CMAKE_CURRENT_BINARY_DIR}/qtappfw-mediaplayer.pc
add_library(qtappfw-mediaplayer SHARED
mediaplayer.cpp
+ MediaplayerBackend.cpp
MediaplayerMpdBackend.cpp
+ MediaplayerBluezBackend.cpp
MpdEventHandler.cpp)
target_include_directories(qtappfw-mediaplayer PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}")
target_include_directories(qtappfw-mediaplayer PUBLIC "${CMAKE_INSTALL_INCLUDEDIR}")
-target_link_libraries(qtappfw-mediaplayer Qt5::Qml PkgConfig::libmpdclient)
+target_link_libraries(qtappfw-mediaplayer qtappfw-bt Qt5::Qml PkgConfig::libmpdclient)
set_target_properties(qtappfw-mediaplayer PROPERTIES
VERSION ${PROJECT_VERSION}
SOVERSION 1
diff --git a/mediaplayer/MediaplayerBackend.cpp b/mediaplayer/MediaplayerBackend.cpp
new file mode 100644
index 0000000..19480ca
--- /dev/null
+++ b/mediaplayer/MediaplayerBackend.cpp
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2022 Konsulko Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "MediaplayerBackend.h"
+
+// Stub to make library build self-contained, with header-only it seems the
+// moc generated bits don't get pulled in or something along those lines.
+
+MediaplayerBackend::MediaplayerBackend(Mediaplayer *player, QObject * parent) :
+ QObject(parent), m_player(player)
+{
+}
diff --git a/mediaplayer/MediaplayerBackend.h b/mediaplayer/MediaplayerBackend.h
new file mode 100644
index 0000000..31b0874
--- /dev/null
+++ b/mediaplayer/MediaplayerBackend.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2022 Konsulko Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef MEDIAPLAYER_BACKEND_H
+#define MEDIAPLAYER_BACKEND_H
+
+#include <QObject>
+#include <QtQml/QQmlContext>
+#include "mediaplayer.h"
+
+class MediaplayerBackend : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit MediaplayerBackend(Mediaplayer *player, QObject * parent = Q_NULLPTR);
+ virtual ~MediaplayerBackend() {};
+
+ virtual void start() = 0;
+ virtual void refresh_metadata() = 0;
+
+ virtual void play() = 0;
+ virtual void pause() = 0;
+ virtual void previous() = 0;
+ virtual void next() = 0;
+ virtual void seek(int) = 0;
+ virtual void fastforward(int) = 0;
+ virtual void rewind(int) = 0;
+ virtual void picktrack(int) = 0;
+ virtual void volume(int) = 0;
+ virtual void loop(QString) = 0;
+
+private:
+ Mediaplayer *m_player;
+};
+
+#endif // MEDIAPLAYER_MPD_BACKEND_H
diff --git a/mediaplayer/MediaplayerBluezBackend.cpp b/mediaplayer/MediaplayerBluezBackend.cpp
new file mode 100644
index 0000000..db189ac
--- /dev/null
+++ b/mediaplayer/MediaplayerBluezBackend.cpp
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2022 Konsulko Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <QDebug>
+#include "MediaplayerBluezBackend.h"
+#include "mediaplayer.h"
+
+
+MediaplayerBluezBackend::MediaplayerBluezBackend(Mediaplayer *player, QQmlContext *context, QObject *parent) :
+ MediaplayerBackend(player, parent)
+{
+ m_bluetooth = new Bluetooth(false, context, true);
+ if (!m_bluetooth)
+ qFatal("Could not create Bluetooth");
+
+ // Connect up metadata updates
+ connect(m_bluetooth,
+ &Bluetooth::mediaConnectedChanged,
+ player,
+ &Mediaplayer::updateBluetoothMediaConnected);
+#if 0
+ connect(m_bluetooth,
+ &Bluetooth::mediaPropertiesChanged,
+ player,
+ &Mediaplayer::updateBluetoothMetadata);
+#else
+ // Proxy Bluetooth metadata updates so we can cache the
+ // latest update locally to allow for quick refreshes.
+ connect(m_bluetooth,
+ &Bluetooth::mediaPropertiesChanged,
+ this,
+ &MediaplayerBluezBackend::updateMetadata);
+ connect(this,
+ &MediaplayerBluezBackend::metadataUpdate,
+ player,
+ &Mediaplayer::updateBluetoothMetadata);
+#endif
+}
+
+MediaplayerBluezBackend::~MediaplayerBluezBackend()
+{
+ delete m_bluetooth;
+}
+
+void MediaplayerBluezBackend::start()
+{
+ m_bluetooth->start();
+}
+
+void MediaplayerBluezBackend::refresh_metadata()
+{
+ // Try to avoid driving a D-Bus request if we have a cached update
+ if (m_cached_metadata.isEmpty())
+ m_bluetooth->refresh_media_state();
+ else
+ emit metadataUpdate(m_cached_metadata);
+}
+
+// Slots
+
+void MediaplayerBluezBackend::updateMetadata(QVariantMap metadata)
+{
+ m_cached_metadata = metadata;
+ emit metadataUpdate(metadata);
+}
+
+// Control methods
+
+void MediaplayerBluezBackend::play()
+{
+ m_bluetooth->media_control(Bluetooth::MediaAction::Play);
+}
+
+void MediaplayerBluezBackend::pause()
+{
+ m_bluetooth->media_control(Bluetooth::MediaAction::Pause);
+}
+
+void MediaplayerBluezBackend::previous()
+{
+ m_bluetooth->media_control(Bluetooth::MediaAction::Previous);
+}
+
+void MediaplayerBluezBackend::next()
+{
+ m_bluetooth->media_control(Bluetooth::MediaAction::Next);
+}
+
+void MediaplayerBluezBackend::seek(int milliseconds)
+{
+ // Not implemented, currently not needed by demo app
+ // It is not quite obvious how this is implemented with the AVRCP
+ // commands not taking a position/offset.
+}
+
+// Relative to current position
+void MediaplayerBluezBackend::fastforward(int milliseconds)
+{
+ // Not implemented, currently not needed by demo app
+ // It is not quite obvious how this is implemented with the AVRCP
+ // commands not taking a position/offset.
+}
+
+// Relative to current position
+void MediaplayerBluezBackend::rewind(int milliseconds)
+{
+ // Not implemented, currently not needed by demo app
+ // It is not quite obvious how this is implemented with the AVRCP
+ // commands not taking a position/offset.
+}
+
+void MediaplayerBluezBackend::picktrack(int track)
+{
+ // Not implemented
+}
+
+void MediaplayerBluezBackend::volume(int volume)
+{
+ // Not implemented
+}
+
+void MediaplayerBluezBackend::loop(QString state)
+{
+ // Not implemented, but potentially possible by setting player property
+ // with bluez-glib addition
+}
+
+void MediaplayerBluezBackend::connect_media()
+{
+ m_bluetooth->media_control(Bluetooth::MediaAction::Connect);
+}
+
+void MediaplayerBluezBackend::disconnect_media()
+{
+ m_bluetooth->media_control(Bluetooth::MediaAction::Disconnect);
+}
diff --git a/mediaplayer/MediaplayerBluezBackend.h b/mediaplayer/MediaplayerBluezBackend.h
new file mode 100644
index 0000000..e37f35f
--- /dev/null
+++ b/mediaplayer/MediaplayerBluezBackend.h
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2022 Konsulko Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef MEDIAPLAYER_BLUEZ_BACKEND_H
+#define MEDIAPLAYER_BLUEZ_BACKEND_H
+
+#include <QObject>
+#include <QtQml/QQmlContext>
+#include <QThread>
+#include <QTimer>
+#include <QMutex>
+
+#include "MediaplayerBackend.h"
+#include "mediaplayer.h"
+#include "bluetooth.h"
+
+class MediaplayerBluezBackend : public MediaplayerBackend
+{
+ Q_OBJECT
+
+public:
+ explicit MediaplayerBluezBackend(Mediaplayer *player, QQmlContext *context, QObject * parent = Q_NULLPTR);
+ virtual ~MediaplayerBluezBackend();
+
+ void start();
+ void refresh_metadata();
+
+ void play();
+ void pause();
+ void previous();
+ void next();
+ void seek(int);
+ void fastforward(int);
+ void rewind(int);
+ void picktrack(int);
+ void volume(int);
+ void loop(QString);
+
+ // Bluetooth specific
+ void connect_media();
+ void disconnect_media();
+
+signals:
+ void metadataUpdate(QVariantMap metadata);
+
+private slots:
+ void updateMetadata(QVariantMap metadata);
+
+private:
+ Mediaplayer *m_player;
+ Bluetooth *m_bluetooth;
+
+ // Cached metadata to simplify refresh requests (e.g. on source switch)
+ QVariantMap m_cached_metadata;
+};
+
+#endif // MEDIAPLAYER_BLUEZ_BACKEND_H
diff --git a/mediaplayer/MediaplayerMpdBackend.cpp b/mediaplayer/MediaplayerMpdBackend.cpp
index c42d18a..5cc9dce 100644
--- a/mediaplayer/MediaplayerMpdBackend.cpp
+++ b/mediaplayer/MediaplayerMpdBackend.cpp
@@ -29,8 +29,7 @@
#define MPD_CONNECTION_TIMEOUT 60000
MediaplayerMpdBackend::MediaplayerMpdBackend(Mediaplayer *player, QQmlContext *context, QObject *parent) :
- QObject(parent),
- m_player(player)
+ MediaplayerBackend(player, parent)
{
struct mpd_connection *conn = mpd_connection_new(NULL, 0, MPD_CONNECTION_TIMEOUT);
if (!conn) {
@@ -49,36 +48,53 @@ MediaplayerMpdBackend::MediaplayerMpdBackend(Mediaplayer *player, QQmlContext *c
m_song_pos_timer = new QTimer(this);
connect(m_song_pos_timer, &QTimer::timeout, this, &MediaplayerMpdBackend::songPositionTimeout);
+ // Connect signal to push the timer-driven position updates to the player
+ connect(this,
+ &MediaplayerMpdBackend::positionMetadataUpdate,
+ player,
+ &Mediaplayer::updateLocalMetadata);
+
+ // Create thread to handle polling for MPD events
MpdEventHandler *handler = new MpdEventHandler();
handler->moveToThread(&m_handlerThread);
connect(&m_handlerThread, &QThread::finished, handler, &QObject::deleteLater);
- connect(this, &MediaplayerMpdBackend::start, handler, &MpdEventHandler::handleEvents);
+ connect(this,
+ &MediaplayerMpdBackend::startHandler,
+ handler,
+ &MpdEventHandler::handleEvents);
// Connect playback state updates from the backend handler thread
// so our view of the state is kept in sync with MPD.
connect(handler,
&MpdEventHandler::playbackStateUpdate,
this,
- &MediaplayerMpdBackend::updatePlaybackState);
-
- // Connect updates from backend handler thread to parent.
- // Note that we should not have to explicitly specify the
- // Qt::QueuedConnection option here since the handler is constructed
- // by adding a worker object to a QThread and it should be automatic
- // in that case.
+ &MediaplayerMpdBackend::updatePlaybackState,
+ Qt::QueuedConnection);
+
+ // Connect updates from backend handler thread.
+ // For now, playlist updates go directly to the parent to keep its
+ // model in sync. This would have to change if there's ever another
+ // backend with playlist support, or improving abstraction is desired.
+ // Current song metadata is directed to a slot here for caching before
+ // sending to the parent, as having the backends be where that is
+ // done seems inherently more logical.
connect(handler,
&MpdEventHandler::playlistUpdate,
player,
- &Mediaplayer::updatePlaylist);
+ &Mediaplayer::updateLocalPlaylist);
connect(handler,
&MpdEventHandler::metadataUpdate,
+ this,
+ &MediaplayerMpdBackend::updateMetadata);
+ connect(this,
+ &MediaplayerMpdBackend::metadataUpdate,
player,
- &Mediaplayer::updateMetadata);
+ &Mediaplayer::updateLocalMetadata);
m_handlerThread.start();
// Start event handler worker loop
- emit start();
+ emit startHandler();
}
MediaplayerMpdBackend::~MediaplayerMpdBackend()
@@ -92,6 +108,62 @@ MediaplayerMpdBackend::~MediaplayerMpdBackend()
mpd_connection_free(m_mpd_conn);
}
+void MediaplayerMpdBackend::start()
+{
+ // An explicit refresh of the playlist is likely necessary
+ // here to handle starting in situations where MPD has been
+ // running before we were (e.g. restarting an app after a
+ // crash). At present the timing seems to be such that we
+ // do not have to worry about signals being sent before the
+ // app is ready, but this could be an issue down the road.
+}
+
+
+void MediaplayerMpdBackend::refresh_metadata()
+{
+ if (m_cached_metadata.isEmpty()) {
+ // This can happen if the app starts up with Bluetooth
+ // connected and then the source is switched. Provide
+ // empty metadata to clear up the app's state.
+ //
+ // Getting the metadata for the first entry in the playlist
+ // to provide it here intersects with how to handle hitting
+ // the end of the playlist and MPD stopping, handling that
+ // is complicated enough that it is being left as future
+ // development.
+
+ QVariantMap track;
+ track.insert("title", "");
+ track.insert("artist", "");
+ track.insert("album", "");
+ track.insert("duration", 0);
+ m_cached_metadata["track"] = track;
+ m_cached_metadata["position"] = 0;
+ m_cached_metadata["status"] = "stopped";
+ }
+
+ // The demo app currently ignores other information in an update with
+ // album art, which is a historical artifact of it arriving in a second
+ // update. Until that assumption is perhaps changed, to avoid having to
+ // complicate things wrt caching, we recreate this behavior using the
+ // metadata we have if it contains album art.
+ if (m_cached_metadata.contains("track")) {
+ QVariantMap tmp = m_cached_metadata;
+ QVariantMap track = tmp.value("track").toMap();
+ if (track.contains("image")) {
+ track.remove("image");
+ tmp["track"] = track;
+
+ // Send this as the initial no art update
+ emit metadataUpdate(tmp);
+ }
+ }
+
+ emit metadataUpdate(m_cached_metadata);
+}
+
+// Slots
+
void MediaplayerMpdBackend::connectionKeepaliveTimeout(void)
{
m_mpd_conn_mutex.lock();
@@ -128,7 +200,8 @@ void MediaplayerMpdBackend::songPositionTimeout(void)
m_song_pos_ms += 250;
QVariantMap metadata;
metadata["position"] = m_song_pos_ms;
- m_player->updateMetadata(metadata);
+ m_cached_metadata["position"] = m_song_pos_ms;
+ emit positionMetadataUpdate(metadata);
}
m_state_mutex.unlock();
@@ -147,7 +220,6 @@ void MediaplayerMpdBackend::updatePlaybackState(int queue_pos, int song_pos_ms,
} else {
// Stop position timer
m_song_pos_timer->stop();
- //m_song_pos_ms = 0;
}
}
m_playing = state;
@@ -155,6 +227,43 @@ void MediaplayerMpdBackend::updatePlaybackState(int queue_pos, int song_pos_ms,
m_state_mutex.unlock();
}
+void MediaplayerMpdBackend::updateMetadata(QVariantMap metadata)
+{
+ // Update our cached metadata to allow fast refreshes upon request
+ // without having to make an out of band query to MPD.
+ //
+ // Updates from the event handler thread that contain track
+ // information are assumed to be complete with the exception of the
+ // album art, which may be included in a follow up update if it is
+ // present.
+
+ if (metadata.contains("status")) {
+ QString status = metadata.value("status").toString();
+ if (status == "stopped") {
+ // There's likely no track information as chances are
+ // this is from hitting the end of the playlist, so
+ // clear things out.
+ // If there actually is track metadata, it'll be
+ // handled by the logic below.
+ m_cached_metadata.clear();
+ }
+ m_cached_metadata["status"] = metadata["status"];
+ }
+
+ if (metadata.contains("track")) {
+ QVariantMap track = metadata.value("track").toMap();
+ m_cached_metadata["track"] = track;
+ }
+
+ // After playback starts, position updates will come from our local
+ // timer, but take the initial value if present.
+ if (metadata.contains("position"))
+ m_cached_metadata["position"] = metadata["position"];
+
+ // Send update up to front end
+ emit metadataUpdate(metadata);
+}
+
// Control methods
void MediaplayerMpdBackend::play()
diff --git a/mediaplayer/MediaplayerMpdBackend.h b/mediaplayer/MediaplayerMpdBackend.h
index b181fda..4b8d74c 100644
--- a/mediaplayer/MediaplayerMpdBackend.h
+++ b/mediaplayer/MediaplayerMpdBackend.h
@@ -23,9 +23,10 @@
#include <QTimer>
#include <QMutex>
#include <mpd/client.h>
+#include "MediaplayerBackend.h"
#include "mediaplayer.h"
-class MediaplayerMpdBackend : public QObject
+class MediaplayerMpdBackend : public MediaplayerBackend
{
Q_OBJECT
@@ -33,6 +34,9 @@ public:
explicit MediaplayerMpdBackend(Mediaplayer *player, QQmlContext *context, QObject * parent = Q_NULLPTR);
virtual ~MediaplayerMpdBackend();
+ void start();
+ void refresh_metadata();
+
void play();
void pause();
void previous();
@@ -45,7 +49,15 @@ public:
void loop(QString);
signals:
- void start(void);
+ void startHandler(void);
+ void metadataUpdate(QVariantMap metadata);
+ void positionMetadataUpdate(QVariantMap metadata);
+
+private slots:
+ void connectionKeepaliveTimeout(void);
+ void songPositionTimeout(void);
+ void updatePlaybackState(int queue_pos, int song_pos_ms, bool state);
+ void updateMetadata(QVariantMap metadata);
private:
Mediaplayer *m_player;
@@ -63,10 +75,8 @@ private:
QThread m_handlerThread;
-private slots:
- void connectionKeepaliveTimeout(void);
- void songPositionTimeout(void);
- void updatePlaybackState(int queue_pos, int song_pos_ms, bool state);
+ // Cached metadata to simplify refresh requests (e.g. on source switch)
+ QVariantMap m_cached_metadata;
};
#endif // MEDIAPLAYER_MPD_BACKEND_H
diff --git a/mediaplayer/MpdEventHandler.cpp b/mediaplayer/MpdEventHandler.cpp
index 40d2999..56a4612 100644
--- a/mediaplayer/MpdEventHandler.cpp
+++ b/mediaplayer/MpdEventHandler.cpp
@@ -15,6 +15,7 @@
*/
#include <QDebug>
+#include <QFileInfo>
#include "MpdEventHandler.h"
MpdEventHandler::MpdEventHandler(QObject *parent) :
@@ -101,6 +102,13 @@ void MpdEventHandler::handleQueueEvent(void)
QString genre(mpd_song_get_tag(song, MPD_TAG_GENRE, 0));
QString uri(mpd_song_get_uri(song));
int pos = mpd_song_get_pos(song);
+
+ if (title.isEmpty()) {
+ // If there's no tag, use the filename
+ QFileInfo fi(uri);
+ title = fi.fileName();
+ }
+
//qDebug() << "Queue[" << pos << "]: " << artist << " - " << title << " / " << album << ", genre " << genre;
QVariantMap track;
@@ -138,6 +146,13 @@ void MpdEventHandler::handlePlayerEvent(void)
QString genre(mpd_song_get_tag(song, MPD_TAG_GENRE, 0));
pos = mpd_song_get_pos(song);
uri = mpd_song_get_uri(song);
+
+ if (title.isEmpty()) {
+ // If there's no tag, use the filename
+ QFileInfo fi(uri);
+ title = fi.fileName();
+ }
+
//qDebug() << "Current song[" << pos << "]: " << artist << " - " << title << " / " << album << ", genre " << genre;
track["title"] = title;
@@ -147,6 +162,7 @@ void MpdEventHandler::handlePlayerEvent(void)
track["index"] = pos;
track["duration"] = mpd_song_get_duration_ms(song);
track["path"] = uri;
+
mpd_song_free(song);
metadata["track"] = track;
diff --git a/mediaplayer/MpdEventHandler.h b/mediaplayer/MpdEventHandler.h
index fcd6155..d51c373 100644
--- a/mediaplayer/MpdEventHandler.h
+++ b/mediaplayer/MpdEventHandler.h
@@ -18,9 +18,6 @@
#define MPD_EVENT_HANDLER_H
#include <QObject>
-//#include <QThread>
-//#include <QTimer>
-//#include <QMutex>
#include <mpd/client.h>
// Use a 60s timeout on our MPD connection
diff --git a/mediaplayer/mediaplayer.cpp b/mediaplayer/mediaplayer.cpp
index f6a9cbd..f686fbb 100644
--- a/mediaplayer/mediaplayer.cpp
+++ b/mediaplayer/mediaplayer.cpp
@@ -15,10 +15,11 @@
*/
#include <QDebug>
-#include <QJsonObject>
+#include <QMutexLocker>
#include "mediaplayer.h"
#include "MediaplayerMpdBackend.h"
+#include "MediaplayerBluezBackend.h"
Playlist::Playlist(QVariantMap &item)
@@ -34,24 +35,37 @@ Playlist::Playlist(QVariantMap &item)
Playlist::~Playlist() {}
-Mediaplayer::Mediaplayer (QQmlContext *context, QObject * parent) :
+Mediaplayer::Mediaplayer(QQmlContext *context, QObject * parent) :
QObject(parent)
{
m_context = context;
m_context->setContextProperty("MediaplayerModel", QVariant::fromValue(m_playlist));
- m_backend = new MediaplayerMpdBackend(this, context);
- if (!m_backend)
- qFatal("Could not create MediaPlayerBackend");
+ m_mpd_backend = new MediaplayerMpdBackend(this, context);
+ if (!m_mpd_backend)
+ qFatal("Could not create MediaplayerMpdBackend");
+ m_backend = m_mpd_backend;
+
+ m_bluez_backend = new MediaplayerBluezBackend(this, context);
+ if (!m_bluez_backend)
+ qFatal("Could not create MediaplayerBluezBackend");
}
Mediaplayer::~Mediaplayer()
{
+ delete m_mpd_backend;
+ delete m_bluez_backend;
+}
+
+void Mediaplayer::start()
+{
+ m_mpd_backend->start();
+ m_bluez_backend->start();
}
// Qt UI Context
-void Mediaplayer::updatePlaylist(QVariantMap playlist)
+void Mediaplayer::updateLocalPlaylist(QVariantMap playlist)
{
QVariantList list = playlist["list"].toList();
@@ -82,30 +96,44 @@ void Mediaplayer::updatePlaylist(QVariantMap playlist)
m_context->setContextProperty("MediaplayerModel", QVariant::fromValue(m_playlist));
}
-void Mediaplayer::updateMetadata(QVariantMap metadata)
+void Mediaplayer::updateLocalMetadata(QVariantMap metadata)
{
- if (metadata.contains("track")) {
- QVariantMap track = metadata.value("track").toMap();
+ if (!m_bt_connected)
+ updateMetadata(metadata);
+}
- if (track.contains("image")) {
- m_context->setContextProperty("AlbumArt",
- QVariant::fromValue(track.value("image")));
- }
-
- if (!track.contains("artist")) {
- track.insert("artist", "");
- metadata["track"] = track;
- }
-
- if (!track.contains("album")) {
- track.insert("album", "");
- metadata["track"] = track;
+void Mediaplayer::updateBluetoothMetadata(QVariantMap metadata)
+{
+ if (m_bt_connected)
+ updateMetadata(metadata);
+}
+
+void Mediaplayer::updateBluetoothMediaConnected(const bool connected)
+{
+ if (m_bt_connected != connected) {
+ QMutexLocker locker(&m_backend_mutex);
+ if (connected) {
+ qDebug() << "Mediaplayer::updateBluetoothMediaConnected: switching to BlueZ backend";
+ m_backend = m_bluez_backend;
+ m_bt_connected = connected;
+ m_bluez_backend->refresh_metadata();
+ } else {
+ qDebug() << "Mediaplayer::updateBluetoothMediaConnected: switching to MPD backend";
+ m_backend = m_mpd_backend;
+ m_bt_connected = connected;
+ m_mpd_backend->refresh_metadata();
}
}
- emit metadataChanged(metadata);
+ m_bt_operation_mutex.lock();
+ if (m_bt_operation) {
+ m_bt_operation = false;
+ }
+ // else externally driven event
+ m_bt_operation_mutex.unlock();
}
+
// Control methods
// For backwards compatibility
@@ -122,60 +150,124 @@ void Mediaplayer::connect()
void Mediaplayer::disconnectBluetooth()
{
+ m_bt_operation_mutex.lock();
+ if (m_bt_operation) {
+ m_bt_operation_mutex.unlock();
+ qDebug() << "Bluetooth media connection operation in progress, ignoring";
+ return;
+ }
+ m_bt_operation_mutex.unlock();
+
// Disconnect from Bluetooth media
+ if (m_bt_connected) {
+ // Explicitly pausing before disconnecting does not seem to be required
+ m_bluez_backend->disconnect_media();
+ }
}
void Mediaplayer::connectBluetooth()
{
+ m_bt_operation_mutex.lock();
+ if (m_bt_operation) {
+ m_bt_operation_mutex.unlock();
+ qDebug() << "Bluetooth media connection operation in progress, ignoring";
+ return;
+ }
+ m_bt_operation_mutex.unlock();
+
// Connect to Bluetooth media
+ if (!m_bt_connected) {
+ m_mpd_backend->pause();
+ m_bluez_backend->connect_media();
+ }
}
void Mediaplayer::play()
{
+ QMutexLocker locker(&m_backend_mutex);
m_backend->play();
}
void Mediaplayer::pause()
{
+ QMutexLocker locker(&m_backend_mutex);
m_backend->pause();
}
void Mediaplayer::previous()
{
+ QMutexLocker locker(&m_backend_mutex);
m_backend->previous();
}
void Mediaplayer::next()
{
+ QMutexLocker locker(&m_backend_mutex);
m_backend->next();
}
void Mediaplayer::seek(int milliseconds)
{
+ QMutexLocker locker(&m_backend_mutex);
m_backend->seek(milliseconds);
}
void Mediaplayer::fastforward(int milliseconds)
{
+ QMutexLocker locker(&m_backend_mutex);
m_backend->fastforward(milliseconds);
}
void Mediaplayer::rewind(int milliseconds)
{
+ QMutexLocker locker(&m_backend_mutex);
m_backend->rewind(milliseconds);
}
void Mediaplayer::picktrack(int track)
{
+ QMutexLocker locker(&m_backend_mutex);
m_backend->picktrack(track);
}
void Mediaplayer::volume(int volume)
{
+ QMutexLocker locker(&m_backend_mutex);
m_backend->volume(volume);
}
void Mediaplayer::loop(QString state)
{
+ QMutexLocker locker(&m_backend_mutex);
m_backend->loop(state);
}
+
+// Private
+
+// Common metadata helper
+void Mediaplayer::updateMetadata(QVariantMap &metadata)
+{
+ if (metadata.contains("track")) {
+ QVariantMap track = metadata.value("track").toMap();
+
+ if (track.contains("image")) {
+ m_context->setContextProperty("AlbumArt",
+ QVariant::fromValue(track.value("image")));
+ }
+
+ if (!track.contains("artist")) {
+ track.insert("artist", "");
+ metadata["track"] = track;
+ }
+
+ if (!track.contains("album")) {
+ track.insert("album", "");
+ metadata["track"] = track;
+ }
+ }
+
+ // Insert current Bluetooth status
+ metadata["connected"] = m_bt_connected;
+
+ emit metadataChanged(metadata);
+}
diff --git a/mediaplayer/mediaplayer.h b/mediaplayer/mediaplayer.h
index 9afe6c6..e274d75 100644
--- a/mediaplayer/mediaplayer.h
+++ b/mediaplayer/mediaplayer.h
@@ -18,6 +18,7 @@
#define MEDIAPLAYER_H
#include <QObject>
+#include <QMutex>
#include <QtQml/QQmlContext>
#include <QtQml/QQmlListProperty>
@@ -36,71 +37,93 @@ class Playlist : public QObject
Q_PROPERTY(QString genre READ genre NOTIFY genreChanged)
public:
- explicit Playlist(QVariantMap& item);
- virtual ~Playlist();
+ explicit Playlist(QVariantMap& item);
+ virtual ~Playlist();
- bool operator<(Playlist& c) { return ((this->m_index < c.m_index)); };
- int index() { return m_index; };
- int duration() { return m_duration; };
- QString path() { return m_path; };
+ bool operator<(Playlist& c) { return ((this->m_index < c.m_index)); };
+ int index() { return m_index; };
+ int duration() { return m_duration; };
+ QString path() { return m_path; };
- // METADATA FIELDS
- QString title() { return m_title; };
- QString album() { return m_album; };
- QString artist() { return m_artist; };
- QString genre() { return m_genre; };
+ // METADATA FIELDS
+ QString title() { return m_title; };
+ QString album() { return m_album; };
+ QString artist() { return m_artist; };
+ QString genre() { return m_genre; };
signals:
- void indexChanged();
- void durationChanged();
- void pathChanged();
- void titleChanged();
- void albumChanged();
- void artistChanged();
- void genreChanged();
+ void indexChanged();
+ void durationChanged();
+ void pathChanged();
+ void titleChanged();
+ void albumChanged();
+ void artistChanged();
+ void genreChanged();
private:
- int m_index, m_duration;
- QString m_path, m_title, m_album, m_artist, m_genre;
+ int m_index, m_duration;
+ QString m_path, m_title, m_album, m_artist, m_genre;
};
+class MediaplayerBackend;
class MediaplayerMpdBackend;
+class MediaplayerBluezBackend;
class Mediaplayer : public QObject
{
Q_OBJECT
public:
- explicit Mediaplayer(QQmlContext *context, QObject * parent = Q_NULLPTR);
- virtual ~Mediaplayer();
-
- // controls
- Q_INVOKABLE void connect();
- Q_INVOKABLE void disconnect();
- Q_INVOKABLE void connectBluetooth();
- Q_INVOKABLE void disconnectBluetooth();
- Q_INVOKABLE void play();
- Q_INVOKABLE void pause();
- Q_INVOKABLE void previous();
- Q_INVOKABLE void next();
- Q_INVOKABLE void seek(int);
- Q_INVOKABLE void fastforward(int);
- Q_INVOKABLE void rewind(int);
- Q_INVOKABLE void picktrack(int);
- Q_INVOKABLE void volume(int);
- Q_INVOKABLE void loop(QString);
+ explicit Mediaplayer(QQmlContext *context, QObject * parent = Q_NULLPTR);
+ virtual ~Mediaplayer();
+
+ // NOTE: Start in the sense of the user is ready to receive events
+ Q_INVOKABLE void start(void);
+
+ // controls
+ Q_INVOKABLE void connect();
+ Q_INVOKABLE void disconnect();
+ Q_INVOKABLE void connectBluetooth();
+ Q_INVOKABLE void disconnectBluetooth();
+ Q_INVOKABLE void play();
+ Q_INVOKABLE void pause();
+ Q_INVOKABLE void previous();
+ Q_INVOKABLE void next();
+ Q_INVOKABLE void seek(int);
+ Q_INVOKABLE void fastforward(int);
+ Q_INVOKABLE void rewind(int);
+ Q_INVOKABLE void picktrack(int);
+ Q_INVOKABLE void volume(int);
+ Q_INVOKABLE void loop(QString);
public slots:
- void updatePlaylist(QVariantMap playlist);
- void updateMetadata(QVariantMap metadata);
+ void updateLocalPlaylist(QVariantMap playlist);
+ void updateLocalMetadata(QVariantMap metadata);
+ void updateBluetoothMetadata(QVariantMap metadata);
+ void updateBluetoothMediaConnected(const bool connected);
signals:
- void metadataChanged(QVariantMap metadata);
+ void metadataChanged(QVariantMap metadata);
private:
- QQmlContext *m_context;
- QList<QObject *> m_playlist;
- MediaplayerMpdBackend *m_backend;
+ void updateMetadata(QVariantMap &metadata);
+
+ QQmlContext *m_context;
+ QList<QObject *> m_playlist;
+
+ MediaplayerBackend *m_backend;
+ MediaplayerMpdBackend *m_mpd_backend;
+ MediaplayerBluezBackend *m_bluez_backend;
+ QMutex m_backend_mutex;
+ bool m_bt_connected = false;
+
+ // Need to track if a Bluetooth connect/disconnect is in progress
+ // to serialize things. This avoids potential issues from multiple
+ // inputs from the UI, which are easy to trigger with the current
+ // demo app not having debouncing logic wrt the operation not being
+ // instant.
+ bool m_bt_operation = false;
+ QMutex m_bt_operation_mutex;
};
#endif // MEDIAPLAYER_H