summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt3
-rw-r--r--mediaplayer/CMakeLists.txt7
-rw-r--r--mediaplayer/MediaplayerMpdBackend.cpp284
-rw-r--r--mediaplayer/MediaplayerMpdBackend.h72
-rw-r--r--mediaplayer/MpdEventHandler.cpp309
-rw-r--r--mediaplayer/MpdEventHandler.h55
-rw-r--r--mediaplayer/mediaplayer.cpp243
-rw-r--r--mediaplayer/mediaplayer.h62
8 files changed, 835 insertions, 200 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index f69e364..678ccaf 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -12,12 +12,14 @@ find_package(PkgConfig REQUIRED)
pkg_check_modules(glib REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(bluez_glib REQUIRED IMPORTED_TARGET bluez-glib)
pkg_check_modules(connman_glib REQUIRED IMPORTED_TARGET connman-glib)
+pkg_check_modules(libmpdclient REQUIRED IMPORTED_TARGET libmpdclient)
include(GNUInstallDirs)
set(DEST_DIR "${CMAKE_INSTALL_PREFIX}")
set(PRIVATE_LIBS "${PRIVATE_LIBS} -lqtappfw-bt
-lqtappfw-hvac
+ -lqtappfw-mediaplayer
-lqtappfw-navigation
-lqtappfw-network
-lqtappfw-weather")
@@ -26,6 +28,7 @@ set (SUBDIRS
docs
bluetooth
hvac
+ mediaplayer
navigation
network
weather)
diff --git a/mediaplayer/CMakeLists.txt b/mediaplayer/CMakeLists.txt
index e21b590..3f77b8d 100644
--- a/mediaplayer/CMakeLists.txt
+++ b/mediaplayer/CMakeLists.txt
@@ -3,12 +3,15 @@ CONFIGURE_FILE("qtappfw-mediaplayer.pc.in" "qtappfw-mediaplayer.pc" @ONLY)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/qtappfw-mediaplayer.pc
DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/pkgconfig)
-add_library(qtappfw-mediaplayer SHARED mediaplayer.cpp)
+add_library(qtappfw-mediaplayer SHARED
+ mediaplayer.cpp
+ MediaplayerMpdBackend.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 qtappfw-core)
+target_link_libraries(qtappfw-mediaplayer Qt5::Qml PkgConfig::libmpdclient)
set_target_properties(qtappfw-mediaplayer PROPERTIES
VERSION ${PROJECT_VERSION}
SOVERSION 1
diff --git a/mediaplayer/MediaplayerMpdBackend.cpp b/mediaplayer/MediaplayerMpdBackend.cpp
new file mode 100644
index 0000000..c42d18a
--- /dev/null
+++ b/mediaplayer/MediaplayerMpdBackend.cpp
@@ -0,0 +1,284 @@
+/*
+ * 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 "MediaplayerMpdBackend.h"
+#include "MpdEventHandler.h"
+#include "mediaplayer.h"
+
+// Use a 60s timeout on our MPD connection
+// NOTE: The connection is actively poked at a higher frequency than
+// this to ensure we don't hit the timeout. The alternatives
+// are to either open and close a connection for every command,
+// or try to keep the connection in idle mode when not using it.
+// The latter is deemed too complicated for our purposes for now,
+// due to it likely requiring another thread.
+#define MPD_CONNECTION_TIMEOUT 60000
+
+MediaplayerMpdBackend::MediaplayerMpdBackend(Mediaplayer *player, QQmlContext *context, QObject *parent) :
+ QObject(parent),
+ m_player(player)
+{
+ struct mpd_connection *conn = mpd_connection_new(NULL, 0, MPD_CONNECTION_TIMEOUT);
+ if (!conn) {
+ qFatal("Could not create MPD connection");
+ }
+ if (mpd_connection_get_error(conn) != MPD_ERROR_SUCCESS) {
+ qFatal("%s", mpd_connection_get_error_message(conn));
+ }
+ m_mpd_conn = conn;
+
+ // Set up connection keepalive timer
+ m_mpd_conn_timer = new QTimer(this);
+ connect(m_mpd_conn_timer, &QTimer::timeout, this, &MediaplayerMpdBackend::connectionKeepaliveTimeout);
+ m_mpd_conn_timer->start(MPD_CONNECTION_TIMEOUT / 2);
+
+ m_song_pos_timer = new QTimer(this);
+ connect(m_song_pos_timer, &QTimer::timeout, this, &MediaplayerMpdBackend::songPositionTimeout);
+
+ MpdEventHandler *handler = new MpdEventHandler();
+ handler->moveToThread(&m_handlerThread);
+ connect(&m_handlerThread, &QThread::finished, handler, &QObject::deleteLater);
+ connect(this, &MediaplayerMpdBackend::start, 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.
+ connect(handler,
+ &MpdEventHandler::playlistUpdate,
+ player,
+ &Mediaplayer::updatePlaylist);
+ connect(handler,
+ &MpdEventHandler::metadataUpdate,
+ player,
+ &Mediaplayer::updateMetadata);
+
+ m_handlerThread.start();
+
+ // Start event handler worker loop
+ emit start();
+}
+
+MediaplayerMpdBackend::~MediaplayerMpdBackend()
+{
+ m_handlerThread.quit();
+ m_handlerThread.wait();
+
+ m_mpd_conn_timer->stop();
+ delete m_mpd_conn_timer;
+
+ mpd_connection_free(m_mpd_conn);
+}
+
+void MediaplayerMpdBackend::connectionKeepaliveTimeout(void)
+{
+ m_mpd_conn_mutex.lock();
+
+ // Clear any lingering non-fatal errors
+ if (!mpd_connection_clear_error(m_mpd_conn)) {
+ // NOTE: There should likely be an attempt to reconnect here,
+ // but it definitely would complicate things for all the
+ // other users.
+ qWarning() << "MPD connection in error state!";
+ m_mpd_conn_mutex.unlock();
+ return;
+ }
+
+ struct mpd_status *status = mpd_run_status(m_mpd_conn);
+ if (!status) {
+ qWarning() << "MPD connection status check failed";
+ } else {
+ mpd_status_free(status);
+ }
+
+ m_mpd_conn_mutex.unlock();
+}
+
+void MediaplayerMpdBackend::songPositionTimeout(void)
+{
+ m_state_mutex.lock();
+
+ if (m_playing) {
+ // Instead of the expense of repeatedly calling mpd_run_status,
+ // provide our own elapsed time. In practice this seems
+ // sufficient for reasonable behavior in the application UI, and
+ // it is what seems recommended for MPD client implementations.
+ m_song_pos_ms += 250;
+ QVariantMap metadata;
+ metadata["position"] = m_song_pos_ms;
+ m_player->updateMetadata(metadata);
+ }
+
+ m_state_mutex.unlock();
+}
+
+void MediaplayerMpdBackend::updatePlaybackState(int queue_pos, int song_pos_ms, bool state)
+{
+ m_state_mutex.lock();
+
+ m_queue_pos = queue_pos;
+ m_song_pos_ms = song_pos_ms;
+ if (m_playing != state) {
+ if (state) {
+ // Start position timer
+ m_song_pos_timer->start(250);
+ } else {
+ // Stop position timer
+ m_song_pos_timer->stop();
+ //m_song_pos_ms = 0;
+ }
+ }
+ m_playing = state;
+
+ m_state_mutex.unlock();
+}
+
+// Control methods
+
+void MediaplayerMpdBackend::play()
+{
+ m_mpd_conn_mutex.lock();
+
+ m_state_mutex.lock();
+ if (!m_playing) {
+ mpd_run_play(m_mpd_conn);
+ }
+ m_state_mutex.unlock();
+
+ m_mpd_conn_mutex.unlock();
+}
+
+void MediaplayerMpdBackend::pause()
+{
+ m_mpd_conn_mutex.lock();
+
+ m_state_mutex.lock();
+ if (m_playing) {
+ mpd_run_pause(m_mpd_conn, true);
+ }
+ m_state_mutex.unlock();
+
+ m_mpd_conn_mutex.unlock();
+}
+
+void MediaplayerMpdBackend::previous()
+{
+ m_mpd_conn_mutex.lock();
+
+ // MPD only allows next/previous if playing
+ m_state_mutex.lock();
+ if (m_playing) {
+ mpd_run_previous(m_mpd_conn);
+ }
+ m_state_mutex.unlock();
+
+ m_mpd_conn_mutex.unlock();
+}
+
+void MediaplayerMpdBackend::next()
+{
+ m_mpd_conn_mutex.lock();
+
+ // MPD only allows next/previous if playing
+ m_state_mutex.lock();
+ if (m_playing) {
+ mpd_run_next(m_mpd_conn);
+ }
+ m_state_mutex.unlock();
+
+ m_mpd_conn_mutex.unlock();
+}
+
+void MediaplayerMpdBackend::seek(int milliseconds)
+{
+ m_mpd_conn_mutex.lock();
+
+ float t = milliseconds;
+ t /= 1000.0;
+ mpd_run_seek_current(m_mpd_conn, t, false);
+
+ m_mpd_conn_mutex.unlock();
+}
+
+// Relative to current position
+void MediaplayerMpdBackend::fastforward(int milliseconds)
+{
+ m_mpd_conn_mutex.lock();
+
+ float t = milliseconds;
+ t /= 1000.0;
+ mpd_run_seek_current(m_mpd_conn, t, true);
+
+ m_mpd_conn_mutex.unlock();
+}
+
+// Relative to current position
+void MediaplayerMpdBackend::rewind(int milliseconds)
+{
+ m_mpd_conn_mutex.lock();
+
+ float t = -milliseconds;
+ t /= 1000.0;
+ mpd_run_seek_current(m_mpd_conn, t, true);
+
+ m_mpd_conn_mutex.unlock();
+}
+
+void MediaplayerMpdBackend::picktrack(int track)
+{
+ m_mpd_conn_mutex.lock();
+
+ if (track >= 0) {
+ mpd_run_play_pos(m_mpd_conn, track);
+ }
+
+ m_mpd_conn_mutex.unlock();
+}
+
+void MediaplayerMpdBackend::volume(int volume)
+{
+ // Not implemented
+}
+
+void MediaplayerMpdBackend::loop(QString state)
+{
+ m_mpd_conn_mutex.lock();
+
+ // Song:
+ // mpd_run_single_state(m_mpd_conn, MPD_SINGLE_ON)
+ // mpd_run_repeat(m_mpd_conn, true) to loop
+ //
+ // Playlist:
+ // mpd_run_single_state(m_mpd_conn, MPD_SINGLE_OFF) (default)
+ // mpd_run_repeat(m_mpd_conn, true) to loop
+
+ if (state == "playlist") {
+ mpd_run_repeat(m_mpd_conn, true);
+ } else {
+ mpd_run_repeat(m_mpd_conn, false);
+ }
+
+ m_mpd_conn_mutex.unlock();
+}
diff --git a/mediaplayer/MediaplayerMpdBackend.h b/mediaplayer/MediaplayerMpdBackend.h
new file mode 100644
index 0000000..b181fda
--- /dev/null
+++ b/mediaplayer/MediaplayerMpdBackend.h
@@ -0,0 +1,72 @@
+/*
+ * 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_MPD_BACKEND_H
+#define MEDIAPLAYER_MPD_BACKEND_H
+
+#include <QObject>
+#include <QtQml/QQmlContext>
+#include <QThread>
+#include <QTimer>
+#include <QMutex>
+#include <mpd/client.h>
+#include "mediaplayer.h"
+
+class MediaplayerMpdBackend : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit MediaplayerMpdBackend(Mediaplayer *player, QQmlContext *context, QObject * parent = Q_NULLPTR);
+ virtual ~MediaplayerMpdBackend();
+
+ 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);
+
+signals:
+ void start(void);
+
+private:
+ Mediaplayer *m_player;
+
+ // MPD connection for sending commands
+ struct mpd_connection *m_mpd_conn;
+ QTimer *m_mpd_conn_timer;
+ QMutex m_mpd_conn_mutex;
+
+ int m_queue_pos = -1;
+ int m_song_pos_ms = 0;
+ bool m_playing = false;
+ QTimer *m_song_pos_timer;
+ QMutex m_state_mutex;
+
+ QThread m_handlerThread;
+
+private slots:
+ void connectionKeepaliveTimeout(void);
+ void songPositionTimeout(void);
+ void updatePlaybackState(int queue_pos, int song_pos_ms, bool state);
+};
+
+#endif // MEDIAPLAYER_MPD_BACKEND_H
diff --git a/mediaplayer/MpdEventHandler.cpp b/mediaplayer/MpdEventHandler.cpp
new file mode 100644
index 0000000..40d2999
--- /dev/null
+++ b/mediaplayer/MpdEventHandler.cpp
@@ -0,0 +1,309 @@
+/*
+ * 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 "MpdEventHandler.h"
+
+MpdEventHandler::MpdEventHandler(QObject *parent) :
+ QObject(parent)
+{
+ // NOTE: Not specifying a timeout here as it is assumed to be long
+ // enough that we won't timeout in the brief intervals when
+ // switching in and out of idle mode.
+ struct mpd_connection *conn = mpd_connection_new(NULL, 0, 0);
+ if (!conn) {
+ qFatal("Could not create MPD connection");
+ }
+ if (mpd_connection_get_error(conn) != MPD_ERROR_SUCCESS) {
+ qFatal("%s", mpd_connection_get_error_message(conn));
+ }
+ m_mpd_conn = conn;
+}
+
+MpdEventHandler::~MpdEventHandler()
+{
+ mpd_connection_free(m_mpd_conn);
+}
+
+void MpdEventHandler::handleEvents(void)
+{
+ bool done = false;
+ enum mpd_idle mask = (enum mpd_idle)(MPD_IDLE_DATABASE|MPD_IDLE_QUEUE|MPD_IDLE_PLAYER);
+ enum mpd_idle events;
+ while (!done) {
+ // Wait for an event
+ events = mpd_run_idle_mask(m_mpd_conn, mask);
+
+ // NOTE: Realistically should be checking the result of
+ // mpd_connection_get_error here, but handling it will
+ // complicate things significantly.
+
+ // handle the events
+ if (events & MPD_IDLE_DATABASE) {
+ handleDatabaseEvent();
+ }
+ else if (events & MPD_IDLE_QUEUE) {
+ handleQueueEvent();
+ }
+ else if (events & MPD_IDLE_PLAYER) {
+ handlePlayerEvent();
+ }
+ }
+
+ qDebug() << "MpdEventHandler::handleEvents: exit";
+}
+
+void MpdEventHandler::handleDatabaseEvent(void)
+{
+ // Maybe clear queue here?
+ //mpd_run_delete_range(m_mpd_con, 0, UINT_MAX);
+
+ if(!mpd_run_add(m_mpd_conn, "/")) {
+ qWarning() << "mpd_run_add failed";
+ }
+}
+
+void MpdEventHandler::handleQueueEvent(void)
+{
+ QVariantList playlist;
+ bool done = false;
+ struct mpd_entity *entity;
+ mpd_send_list_queue_meta(m_mpd_conn);
+ do {
+ entity = mpd_recv_entity(m_mpd_conn);
+ if (!entity) {
+ enum mpd_error error = mpd_connection_get_error(m_mpd_conn);
+ if (error == MPD_ERROR_SUCCESS)
+ done = true;
+ break;
+ }
+ if (mpd_entity_get_type(entity) != MPD_ENTITY_TYPE_SONG) {
+ mpd_entity_free(entity);
+ continue;
+ }
+ const struct mpd_song *song = mpd_entity_get_song(entity);
+ QString title(mpd_song_get_tag(song, MPD_TAG_TITLE, 0));
+ QString artist(mpd_song_get_tag(song, MPD_TAG_ARTIST, 0));
+ QString album(mpd_song_get_tag(song, MPD_TAG_ALBUM, 0));
+ 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);
+ //qDebug() << "Queue[" << pos << "]: " << artist << " - " << title << " / " << album << ", genre " << genre;
+
+ QVariantMap track;
+ track["title"] = title;
+ track["artist"] = artist;
+ track["album"] = album;
+ track["genre"] = genre;
+ track["index"] = pos;
+ track["duration"] = mpd_song_get_duration_ms(song);
+ track["path"] = uri;
+ playlist.append(track);
+
+ mpd_entity_free(entity);
+ } while(!done);
+
+ if (done) {
+ QVariantMap metadata;
+ metadata["list"] = playlist;
+
+ emit playlistUpdate(metadata);
+ }
+}
+
+void MpdEventHandler::handlePlayerEvent(void)
+{
+ int pos = -1;
+ QString uri;
+ QVariantMap track;
+ QVariantMap metadata;
+ struct mpd_song* song = mpd_run_current_song(m_mpd_conn);
+ if (song) {
+ QString title(mpd_song_get_tag(song, MPD_TAG_TITLE, 0));
+ QString artist(mpd_song_get_tag(song, MPD_TAG_ARTIST, 0));
+ QString album(mpd_song_get_tag(song, MPD_TAG_ALBUM, 0));
+ QString genre(mpd_song_get_tag(song, MPD_TAG_GENRE, 0));
+ pos = mpd_song_get_pos(song);
+ uri = mpd_song_get_uri(song);
+ //qDebug() << "Current song[" << pos << "]: " << artist << " - " << title << " / " << album << ", genre " << genre;
+
+ track["title"] = title;
+ track["artist"] = artist;
+ track["album"] = album;
+ track["genre"] = genre;
+ track["index"] = pos;
+ track["duration"] = mpd_song_get_duration_ms(song);
+ track["path"] = uri;
+ mpd_song_free(song);
+
+ metadata["track"] = track;
+ }
+ // NOTE:
+ // It may make sense to check status first, and then try to handle the
+ // no song + stopped case at the end of the queue to trigger a move to
+ // the top of the playlist in the UI if that is deemed desirable (the
+ // old binding does not seem to). However, this may prove a bit
+ // complicated if triggering stop instead of pause is done to trigger
+ // corking in WirePlumber...
+
+ struct mpd_status *status = mpd_run_status(m_mpd_conn);
+
+ int elapsed_ms = mpd_status_get_elapsed_ms(status);
+ metadata["position"] = elapsed_ms;
+
+ int volume = mpd_status_get_volume(status);
+ metadata["volume"] = volume == -1 ? 0 : volume;
+
+ // NOTE: current UI client user does not care about paused vs stopped,
+ // and the old binding did not differentiate in its responses,
+ // so do not do so either for now.
+ enum mpd_state state = mpd_status_get_state(status);
+ QString status_name("stopped");
+ if (state == MPD_STATE_PLAY)
+ status_name = "playing";
+ metadata["status"] = status_name;
+
+ mpd_status_free(status);
+
+ // For UI
+ emit metadataUpdate(metadata);
+
+ // For backend state tracking
+ emit playbackStateUpdate(pos, elapsed_ms, (state == MPD_STATE_PLAY));
+
+ if (uri.size()) {
+ // Send album art to UI as a separate update.
+ // This avoids things being out of sync than delaying while
+ // the art is is read.
+ // NOTE: Some form of caching might be desirable here.
+ QByteArray buffer;
+ QString mime_type;
+ if (getSongArt(uri, buffer, mime_type) && mime_type.size()) {
+ QString image_base64(buffer.toBase64());
+ QString mime_type_header("data:");
+ mime_type_header += mime_type;
+ mime_type_header += ";base64,";
+ image_base64.prepend(mime_type_header);
+
+ // Re-use metadata map...
+ track["image"] = image_base64;
+ metadata["track"] = track;
+
+ // ...but clear out the ephemeral metadata
+ metadata.remove("position");
+ metadata.remove("status");
+ metadata.remove("volume");
+
+ // Update UI
+ emit metadataUpdate(metadata);
+ }
+ }
+}
+
+bool MpdEventHandler::getSongArt(const QString &path, QByteArray &buffer, QString &type)
+{
+ bool rc = false;
+ unsigned pic_offset = 0;
+ unsigned pic_size = 0;
+
+ QByteArray path_ba = path.toLocal8Bit();
+ const char *path_cstr = path_ba.data();
+
+ bool first = true;
+ do {
+ QString pic_offset_str = QString::number(pic_offset);
+ QByteArray pic_offset_ba = pic_offset_str.toLocal8Bit();
+ const char *pic_offset_cstr = pic_offset_ba.data();
+
+ if (!mpd_send_command(m_mpd_conn, "readpicture", path_cstr, pic_offset_cstr, NULL)) {
+ if (mpd_connection_get_error(m_mpd_conn) != MPD_ERROR_SUCCESS)
+ goto conn_error;
+ }
+ struct mpd_pair *pair = mpd_recv_pair_named(m_mpd_conn, "size");
+ if (!pair) {
+ if (first) {
+ // No art, exit
+ break;
+ }
+
+ if (mpd_connection_get_error(m_mpd_conn) != MPD_ERROR_SUCCESS)
+ goto conn_error;
+ }
+ pic_size = QString(pair->value).toInt();
+ mpd_return_pair(m_mpd_conn, pair);
+
+ pair = mpd_recv_pair_named(m_mpd_conn, "type");
+ if (!pair) {
+ // check for error
+ }
+ QString mime_type(pair->value);
+ mpd_return_pair(m_mpd_conn, pair);
+ if (first) {
+ if (mime_type.size()) {
+ type = mime_type;
+ } else {
+ break;
+ }
+ first = false;
+ }
+
+ pair = mpd_recv_pair_named(m_mpd_conn, "binary");
+ if (!pair) {
+ if (mpd_connection_get_error(m_mpd_conn) != MPD_ERROR_SUCCESS)
+ goto conn_error;
+ }
+
+ unsigned chunk_size = QString(pair->value).toInt();
+ mpd_return_pair(m_mpd_conn, pair);
+
+ if (!chunk_size)
+ break;
+
+ char *buf = new (std::nothrow) char[chunk_size];
+ if (!buf)
+ goto conn_error;
+
+ if (!mpd_recv_binary(m_mpd_conn, buf, chunk_size)) {
+ if (mpd_connection_get_error(m_mpd_conn) != MPD_ERROR_SUCCESS) {
+ delete[] buf;
+ goto conn_error;
+ }
+ }
+
+ if (!mpd_response_finish(m_mpd_conn)) {
+ if (mpd_connection_get_error(m_mpd_conn) != MPD_ERROR_SUCCESS) {
+ delete[] buf;
+ goto conn_error;
+ }
+ }
+
+ buffer.append(buf, chunk_size);
+ delete[] buf;
+
+ pic_offset += chunk_size;
+
+ } while (pic_offset < pic_size);
+
+conn_error:
+ if (pic_offset == pic_size) {
+ rc = true;
+ } else {
+ // Don't pass garbage to caller
+ buffer.clear();
+ }
+
+ return rc;
+}
diff --git a/mediaplayer/MpdEventHandler.h b/mediaplayer/MpdEventHandler.h
new file mode 100644
index 0000000..fcd6155
--- /dev/null
+++ b/mediaplayer/MpdEventHandler.h
@@ -0,0 +1,55 @@
+/*
+ * 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 MPD_EVENT_HANDLER_H
+#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
+#define MPD_CONNECTION_TIMEOUT 60000
+
+class MpdEventHandler : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit MpdEventHandler(QObject *parent = nullptr);
+ virtual ~MpdEventHandler();
+
+public slots:
+ void handleEvents(void);
+
+signals:
+ void playbackStateUpdate(int queue_pos, int song_pos_ms, bool state);
+ void playlistUpdate(QVariantMap playlist);
+ void metadataUpdate(QVariantMap metadata);
+
+private:
+ void handleDatabaseEvent(void);
+ void handleQueueEvent(void);
+ void handlePlayerEvent(void);
+
+ bool getSongArt(const QString &path, QByteArray &buffer, QString &type);
+
+ struct mpd_connection *m_mpd_conn;
+};
+
+#endif // MPD_EVENT_HANDLER_H
diff --git a/mediaplayer/mediaplayer.cpp b/mediaplayer/mediaplayer.cpp
index c578145..f6a9cbd 100644
--- a/mediaplayer/mediaplayer.cpp
+++ b/mediaplayer/mediaplayer.cpp
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018-2020 Konsulko Group
+ * Copyright (C) 2018-2020,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.
@@ -15,37 +15,34 @@
*/
#include <QDebug>
+#include <QJsonObject>
-#include "callmessage.h"
-#include "eventmessage.h"
-#include "messageengine.h"
-#include "messagefactory.h"
-#include "messageenginefactory.h"
#include "mediaplayer.h"
+#include "MediaplayerMpdBackend.h"
Playlist::Playlist(QVariantMap &item)
{
- m_index = item["index"].toInt();
- m_duration = item["duration"].toInt();
- m_genre = item["genre"].toString();
- m_path = item["path"].toString();
- m_title = item["title"].toString();
- m_album = item["album"].toString();
- m_artist = item["artist"].toString();
+ m_index = item["index"].toInt();
+ m_duration = item["duration"].toInt();
+ m_genre = item["genre"].toString();
+ m_path = item["path"].toString();
+ m_title = item["title"].toString();
+ m_album = item["album"].toString();
+ m_artist = item["artist"].toString();
}
Playlist::~Playlist() {}
-Mediaplayer::Mediaplayer (QUrl &url, QQmlContext *context, QObject * parent) :
- QObject(parent)
+Mediaplayer::Mediaplayer (QQmlContext *context, QObject * parent) :
+ QObject(parent)
{
- m_mloop = MessageEngineFactory::getInstance().getMessageEngine(url);
- m_context = context;
- m_context->setContextProperty("MediaplayerModel", QVariant::fromValue(m_playlist));
- QObject::connect(m_mloop.get(), &MessageEngine::connected, this, &Mediaplayer::onConnected);
- QObject::connect(m_mloop.get(), &MessageEngine::disconnected, this, &Mediaplayer::onDisconnected);
- QObject::connect(m_mloop.get(), &MessageEngine::messageReceived, this, &Mediaplayer::onMessageReceived);
+ 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");
}
Mediaplayer::~Mediaplayer()
@@ -56,207 +53,129 @@ Mediaplayer::~Mediaplayer()
void Mediaplayer::updatePlaylist(QVariantMap playlist)
{
- QVariantList list = playlist["list"].toList();
+ QVariantList list = playlist["list"].toList();
- m_playlist.clear();
+ m_playlist.clear();
- for (auto i : list) {
- QVariantMap item = qvariant_cast<QVariantMap>(i);
- m_playlist.append(new Playlist(item));
- }
+ for (auto i : list) {
+ QVariantMap item = qvariant_cast<QVariantMap>(i);
+ m_playlist.append(new Playlist(item));
+ }
- if (m_playlist.count() == 0) {
- QVariantMap tmp, track;
+ if (m_playlist.count() == 0) {
+ QVariantMap tmp, track;
- track.insert("title", "");
- track.insert("artist", "");
- track.insert("album", "");
- track.insert("duration", 0);
+ track.insert("title", "");
+ track.insert("artist", "");
+ track.insert("album", "");
+ track.insert("duration", 0);
- tmp.insert("position", 0);
- tmp.insert("track", track);
+ tmp.insert("position", 0);
+ tmp.insert("track", track);
- // clear metadata in UI
- m_context->setContextProperty("AlbumArt", "");
- emit metadataChanged(tmp);
- }
+ // clear metadata in UI
+ m_context->setContextProperty("AlbumArt", "");
+ emit metadataChanged(tmp);
+ }
- // Refresh model
- m_context->setContextProperty("MediaplayerModel", QVariant::fromValue(m_playlist));
+ // Refresh model
+ m_context->setContextProperty("MediaplayerModel", QVariant::fromValue(m_playlist));
}
-// Control Verb methods
-
-void Mediaplayer::control(QString control, QJsonObject parameter)
+void Mediaplayer::updateMetadata(QVariantMap metadata)
{
- std::unique_ptr<Message> msg = MessageFactory::getInstance().createOutboundMessage(MessageId::Call);
- if (!msg)
- return;
+ if (metadata.contains("track")) {
+ QVariantMap track = metadata.value("track").toMap();
- CallMessage* mpmsg = static_cast<CallMessage*>(msg.get());
- parameter.insert("value", control);
+ 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;
+ }
+ }
- mpmsg->createRequest("mediaplayer", "controls", parameter);
- m_mloop->sendMessage(std::move(msg));
+ emit metadataChanged(metadata);
}
+// Control methods
-void Mediaplayer::control(QString control)
+// For backwards compatibility
+void Mediaplayer::disconnect()
{
- QJsonObject parameter;
+ disconnectBluetooth();
+}
- Mediaplayer::control(control, parameter);
+// For backwards compatibility
+void Mediaplayer::connect()
+{
+ connectBluetooth();
}
-void Mediaplayer::disconnect()
+void Mediaplayer::disconnectBluetooth()
{
- control("disconnect");
+ // Disconnect from Bluetooth media
}
-void Mediaplayer::connect()
+void Mediaplayer::connectBluetooth()
{
- control("connect");
+ // Connect to Bluetooth media
}
+
void Mediaplayer::play()
{
- control("play");
+ m_backend->play();
}
void Mediaplayer::pause()
{
- control("pause");
+ m_backend->pause();
}
void Mediaplayer::previous()
{
- control("previous");
+ m_backend->previous();
}
void Mediaplayer::next()
{
- control("next");
+ m_backend->next();
}
void Mediaplayer::seek(int milliseconds)
{
- QJsonObject parameter;
- parameter.insert("position", QString::number(milliseconds));
-
- control("seek", parameter);
+ m_backend->seek(milliseconds);
}
void Mediaplayer::fastforward(int milliseconds)
{
- QJsonObject parameter;
- parameter.insert("position", QString::number(milliseconds));
-
- control("fast-forward", parameter);
+ m_backend->fastforward(milliseconds);
}
void Mediaplayer::rewind(int milliseconds)
{
- QJsonObject parameter;
- parameter.insert("position", QString::number(milliseconds));
-
- control("rewind", parameter);
+ m_backend->rewind(milliseconds);
}
void Mediaplayer::picktrack(int track)
{
- QJsonObject parameter;
- parameter.insert("index", QString::number(track));
-
- control("pick-track", parameter);
+ m_backend->picktrack(track);
}
void Mediaplayer::volume(int volume)
{
- QJsonObject parameter;
- parameter.insert("volume", QString(volume));
-
- control("volume", parameter);
+ m_backend->volume(volume);
}
void Mediaplayer::loop(QString state)
{
- QJsonObject parameter;
- parameter.insert("state", state);
-
- control("loop", parameter);
-}
-
-void Mediaplayer::onConnected()
-{
- QStringListIterator eventIterator(events);
-
- while (eventIterator.hasNext()) {
- std::unique_ptr<Message> msg = MessageFactory::getInstance().createOutboundMessage(MessageId::Call);
- if (!msg)
- return;
-
- CallMessage* mpmsg = static_cast<CallMessage*>(msg.get());
- QJsonObject parameter;
- parameter.insert("value", eventIterator.next());
- mpmsg->createRequest("mediaplayer", "subscribe", parameter);
- m_mloop->sendMessage(std::move(msg));
- }
-}
-
-void Mediaplayer::onDisconnected()
-{
- QStringListIterator eventIterator(events);
-
- while (eventIterator.hasNext()) {
- std::unique_ptr<Message> msg = MessageFactory::getInstance().createOutboundMessage(MessageId::Call);
- if (!msg)
- return;
-
- CallMessage* mpmsg = static_cast<CallMessage*>(msg.get());
- QJsonObject parameter;
- parameter.insert("value", eventIterator.next());
- mpmsg->createRequest("mediaplayer", "unsubscribe", parameter);
- m_mloop->sendMessage(std::move(msg));
- }
-}
-
-void Mediaplayer::onMessageReceived(std::shared_ptr<Message> msg)
-{
- if (!msg)
- return;
-
- if (msg->isEvent()){
- std::shared_ptr<EventMessage> emsg = std::static_pointer_cast<EventMessage>(msg);
- QString ename = emsg->eventName();
- QString eapi = emsg->eventApi();
- QJsonObject data = emsg->eventData();
- if (eapi != "mediaplayer")
- return;
-
- if (ename == "playlist") {
- updatePlaylist(data.toVariantMap());
- } else if (ename == "metadata") {
- QVariantMap map = data.toVariantMap();
-
- if (map.contains("track")) {
- QVariantMap track = map.value("track").toMap();
-
- if (track.contains("image")) {
- m_context->setContextProperty("AlbumArt",
- QVariant::fromValue(track.value("image")));
- }
-
- if (!track.contains("artist")) {
- track.insert("artist", "");
- map["track"] = track;
- }
-
- if (!track.contains("album")) {
- track.insert("album", "");
- map["track"] = track;
- }
- }
-
- emit metadataChanged(map);
- }
- }
+ m_backend->loop(state);
}
diff --git a/mediaplayer/mediaplayer.h b/mediaplayer/mediaplayer.h
index 3d53a24..9afe6c6 100644
--- a/mediaplayer/mediaplayer.h
+++ b/mediaplayer/mediaplayer.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018-2020 Konsulko Group
+ * Copyright (C) 2018-2020,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.
@@ -17,29 +17,25 @@
#ifndef MEDIAPLAYER_H
#define MEDIAPLAYER_H
-#include <memory>
#include <QObject>
#include <QtQml/QQmlContext>
#include <QtQml/QQmlListProperty>
-class MessageEngine;
-class Message;
-
class Playlist : public QObject
{
- Q_OBJECT
+ Q_OBJECT
- Q_PROPERTY(int index READ index NOTIFY indexChanged)
- Q_PROPERTY(int duration READ duration NOTIFY durationChanged)
- Q_PROPERTY(QString path READ path NOTIFY pathChanged)
+ Q_PROPERTY(int index READ index NOTIFY indexChanged)
+ Q_PROPERTY(int duration READ duration NOTIFY durationChanged)
+ Q_PROPERTY(QString path READ path NOTIFY pathChanged)
- // METADATA FIELDS
- Q_PROPERTY(QString title READ title NOTIFY titleChanged)
- Q_PROPERTY(QString album READ album NOTIFY albumChanged)
- Q_PROPERTY(QString artist READ artist NOTIFY artistChanged)
- Q_PROPERTY(QString genre READ genre NOTIFY genreChanged)
+ // METADATA FIELDS
+ Q_PROPERTY(QString title READ title NOTIFY titleChanged)
+ Q_PROPERTY(QString album READ album NOTIFY albumChanged)
+ Q_PROPERTY(QString artist READ artist NOTIFY artistChanged)
+ Q_PROPERTY(QString genre READ genre NOTIFY genreChanged)
- public:
+public:
explicit Playlist(QVariantMap& item);
virtual ~Playlist();
@@ -54,7 +50,7 @@ class Playlist : public QObject
QString artist() { return m_artist; };
QString genre() { return m_genre; };
- signals:
+signals:
void indexChanged();
void durationChanged();
void pathChanged();
@@ -63,22 +59,26 @@ class Playlist : public QObject
void artistChanged();
void genreChanged();
- private:
+private:
int m_index, m_duration;
QString m_path, m_title, m_album, m_artist, m_genre;
};
+class MediaplayerMpdBackend;
+
class Mediaplayer : public QObject
{
- Q_OBJECT
+ Q_OBJECT
- public:
- explicit Mediaplayer(QUrl &url, QQmlContext *context, QObject * parent = Q_NULLPTR);
+public:
+ explicit Mediaplayer(QQmlContext *context, QObject * parent = Q_NULLPTR);
virtual ~Mediaplayer();
// controls
- Q_INVOKABLE void disconnect();
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();
@@ -90,27 +90,17 @@ class Mediaplayer : public QObject
Q_INVOKABLE void volume(int);
Q_INVOKABLE void loop(QString);
+public slots:
void updatePlaylist(QVariantMap playlist);
+ void updateMetadata(QVariantMap metadata);
- signals:
+signals:
void metadataChanged(QVariantMap metadata);
- private:
- std::shared_ptr<MessageEngine> m_mloop;
+private:
QQmlContext *m_context;
QList<QObject *> m_playlist;
-
- void control(QString, QJsonObject);
- void control(QString);
-
- void onConnected();
- void onDisconnected();
- void onMessageReceived(std::shared_ptr<Message>);
-
- const QStringList events {
- "playlist",
- "metadata",
- };
+ MediaplayerMpdBackend *m_backend;
};
#endif // MEDIAPLAYER_H