diff options
Diffstat (limited to 'mediaplayer/MediaplayerMpdBackend.cpp')
-rw-r--r-- | mediaplayer/MediaplayerMpdBackend.cpp | 284 |
1 files changed, 284 insertions, 0 deletions
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(); +} |