summaryrefslogtreecommitdiffstats
path: root/app/mediaplayer.qrc
blob: d3fa02eddfaa75388d019b8a10d586fd6b90fea2 (plain)
1
2
3
4
5
<RCC>
    <qresource prefix="/">
        <file>MediaPlayer.qml</file>
    </qresource>
</RCC>
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393
/*
 * 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) :
	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_CONNECTION_TIMEOUT / 2);

	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::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 (!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_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_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();
}