From 1332cc7d0a618ee88b4d19813340665332d406ca Mon Sep 17 00:00:00 2001 From: Scott Murray Date: Mon, 28 Feb 2022 13:04:45 -0500 Subject: Add Bluetooth media control support Rework to expose Bluetooth AVRCP media control in the Bluetooth class, and use that support to implement a Bluetooth backend in the Mediaplayer class. This replaces the scheme that existed with the agl-service-mediaplayer binding where it abstracted away the Bluetooth support itself. However, care has been take to make sure that the exposed API to users of libqtappfw-mediaplayer has not changed. Bug-AGL: SPEC-4231 Signed-off-by: Scott Murray Change-Id: I76b4f75621ce0121364eea3259b074bf3067ee88 --- mediaplayer/MediaplayerMpdBackend.cpp | 139 ++++++++++++++++++++++++++++++---- 1 file changed, 124 insertions(+), 15 deletions(-) (limited to 'mediaplayer/MediaplayerMpdBackend.cpp') diff --git a/mediaplayer/MediaplayerMpdBackend.cpp b/mediaplayer/MediaplayerMpdBackend.cpp index c42d18a..5cc9dce 100644 --- a/mediaplayer/MediaplayerMpdBackend.cpp +++ b/mediaplayer/MediaplayerMpdBackend.cpp @@ -29,8 +29,7 @@ #define MPD_CONNECTION_TIMEOUT 60000 MediaplayerMpdBackend::MediaplayerMpdBackend(Mediaplayer *player, QQmlContext *context, QObject *parent) : - QObject(parent), - m_player(player) + MediaplayerBackend(player, parent) { struct mpd_connection *conn = mpd_connection_new(NULL, 0, MPD_CONNECTION_TIMEOUT); if (!conn) { @@ -49,36 +48,53 @@ MediaplayerMpdBackend::MediaplayerMpdBackend(Mediaplayer *player, QQmlContext *c 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::start, handler, &MpdEventHandler::handleEvents); + 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); - - // 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. + &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::updatePlaylist); + &Mediaplayer::updateLocalPlaylist); connect(handler, &MpdEventHandler::metadataUpdate, + this, + &MediaplayerMpdBackend::updateMetadata); + connect(this, + &MediaplayerMpdBackend::metadataUpdate, player, - &Mediaplayer::updateMetadata); + &Mediaplayer::updateLocalMetadata); m_handlerThread.start(); // Start event handler worker loop - emit start(); + emit startHandler(); } MediaplayerMpdBackend::~MediaplayerMpdBackend() @@ -92,6 +108,62 @@ MediaplayerMpdBackend::~MediaplayerMpdBackend() 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(); @@ -128,7 +200,8 @@ void MediaplayerMpdBackend::songPositionTimeout(void) m_song_pos_ms += 250; QVariantMap metadata; metadata["position"] = m_song_pos_ms; - m_player->updateMetadata(metadata); + m_cached_metadata["position"] = m_song_pos_ms; + emit positionMetadataUpdate(metadata); } m_state_mutex.unlock(); @@ -147,7 +220,6 @@ void MediaplayerMpdBackend::updatePlaybackState(int queue_pos, int song_pos_ms, } else { // Stop position timer m_song_pos_timer->stop(); - //m_song_pos_ms = 0; } } m_playing = state; @@ -155,6 +227,43 @@ void MediaplayerMpdBackend::updatePlaybackState(int queue_pos, int song_pos_ms, 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() -- cgit 1.2.3-korg