diff options
-rw-r--r-- | CMakeLists.txt | 3 | ||||
-rw-r--r-- | mediaplayer/CMakeLists.txt | 7 | ||||
-rw-r--r-- | mediaplayer/MediaplayerMpdBackend.cpp | 284 | ||||
-rw-r--r-- | mediaplayer/MediaplayerMpdBackend.h | 72 | ||||
-rw-r--r-- | mediaplayer/MpdEventHandler.cpp | 309 | ||||
-rw-r--r-- | mediaplayer/MpdEventHandler.h | 55 | ||||
-rw-r--r-- | mediaplayer/mediaplayer.cpp | 243 | ||||
-rw-r--r-- | mediaplayer/mediaplayer.h | 62 |
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 |