/* * Copyright (C) 2022 Konsulko Group * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include "MediaplayerMpdBackend.h" #include "MpdEventHandler.h" #include "mediaplayer.h" // Use a 30s 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 30000 #define MPD_KEEPALIVE_TIMEOUT 5000 MediaplayerMpdBackend::MediaplayerMpdBackend(Mediaplayer *player, QQmlContext *context, QObject *parent) : MediaplayerBackend(player, parent) { 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_KEEPALIVE_TIMEOUT); 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, this, &MediaplayerMpdBackend::handleHandlerFinish); connect(&m_handlerThread, &QThread::finished, handler, &QObject::deleteLater); 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, 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::updateLocalPlaylist); connect(handler, &MpdEventHandler::metadataUpdate, this, &MediaplayerMpdBackend::updateMetadata); connect(this, &MediaplayerMpdBackend::metadataUpdate, player, &Mediaplayer::updateLocalMetadata); m_handlerThread.start(); // Start event handler worker loop emit startHandler(); } 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::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(); // Clear any lingering non-fatal errors if (m_mpd_conn && !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!"; mpd_connection_free(m_mpd_conn); m_mpd_conn = NULL; m_mpd_conn_timer->stop(); } m_mpd_conn_mutex.unlock(); } void MediaplayerMpdBackend::handleHandlerFinish(void) { m_mpd_conn_mutex.lock(); // If the event thread finished, our connection is likely dead. // Given the behavior seen when mpd is hung is most client API // calls hang, trying to determine mpd liveliness here seems // problematic. Attempting to reconnect would probably be the // only robust solution. mpd_connection_free(m_mpd_conn); m_mpd_conn = NULL; m_mpd_conn_mutex.unlock(); m_mpd_conn_timer->stop(); delete m_mpd_conn_timer; } 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_cached_metadata["position"] = m_song_pos_ms; emit positionMetadataUpdate(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_playing = state; 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() { m_mpd_conn_mutex.lock(); m_state_mutex.lock(); if (m_mpd_conn && !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_mpd_conn && 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_mpd_conn && 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_mpd_conn && 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(); if (m_mpd_conn) { 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(); if (m_mpd_conn) { 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(); if (m_mpd_conn) { 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 (m_mpd_conn && 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 (m_mpd_conn) { if (state == "playlist") { mpd_run_repeat(m_mpd_conn, true); } else { mpd_run_repeat(m_mpd_conn, false); } } m_mpd_conn_mutex.unlock(); }