diff options
Diffstat (limited to 'mediaplayer')
-rw-r--r-- | mediaplayer/CMakeLists.txt | 4 | ||||
-rw-r--r-- | mediaplayer/MediaplayerBackend.cpp | 25 | ||||
-rw-r--r-- | mediaplayer/MediaplayerBackend.h | 50 | ||||
-rw-r--r-- | mediaplayer/MediaplayerBluezBackend.cpp | 149 | ||||
-rw-r--r-- | mediaplayer/MediaplayerBluezBackend.h | 70 | ||||
-rw-r--r-- | mediaplayer/MediaplayerMpdBackend.cpp | 139 | ||||
-rw-r--r-- | mediaplayer/MediaplayerMpdBackend.h | 22 | ||||
-rw-r--r-- | mediaplayer/MpdEventHandler.cpp | 16 | ||||
-rw-r--r-- | mediaplayer/MpdEventHandler.h | 3 | ||||
-rw-r--r-- | mediaplayer/mediaplayer.cpp | 138 | ||||
-rw-r--r-- | mediaplayer/mediaplayer.h | 111 |
11 files changed, 635 insertions, 92 deletions
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 |