From 1332cc7d0a618ee88b4d19813340665332d406ca Mon Sep 17 00:00:00 2001 From: Scott Murray Date: Mon, 28 Feb 2022 13:04:45 -0500 Subject: Add Bluetooth media control support 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 Change-Id: I76b4f75621ce0121364eea3259b074bf3067ee88 --- bluetooth/bluetooth.cpp | 195 +++++++++++++++++++++++++++++--- bluetooth/bluetooth.h | 106 ++++++++++------- bluetooth/bluetootheventhandler.cpp | 142 ++++++++++++++++++++++- bluetooth/bluetootheventhandler.h | 43 ++++++- mediaplayer/CMakeLists.txt | 4 +- mediaplayer/MediaplayerBackend.cpp | 25 ++++ mediaplayer/MediaplayerBackend.h | 50 ++++++++ mediaplayer/MediaplayerBluezBackend.cpp | 149 ++++++++++++++++++++++++ mediaplayer/MediaplayerBluezBackend.h | 70 ++++++++++++ mediaplayer/MediaplayerMpdBackend.cpp | 139 ++++++++++++++++++++--- mediaplayer/MediaplayerMpdBackend.h | 22 +++- mediaplayer/MpdEventHandler.cpp | 16 +++ mediaplayer/MpdEventHandler.h | 3 - mediaplayer/mediaplayer.cpp | 138 ++++++++++++++++++---- mediaplayer/mediaplayer.h | 111 +++++++++++------- 15 files changed, 1062 insertions(+), 151 deletions(-) create mode 100644 mediaplayer/MediaplayerBackend.cpp create mode 100644 mediaplayer/MediaplayerBackend.h create mode 100644 mediaplayer/MediaplayerBluezBackend.cpp create mode 100644 mediaplayer/MediaplayerBluezBackend.h 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 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 uuids; + QMap 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 #include - #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 +#include +#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 +#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 +#include +#include +#include +#include + +#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 #include #include +#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 +#include #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 -//#include -//#include -//#include #include // 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 -#include +#include #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 +#include #include #include @@ -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 m_playlist; - MediaplayerMpdBackend *m_backend; + void updateMetadata(QVariantMap &metadata); + + QQmlContext *m_context; + QList 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 -- cgit 1.2.3-korg