From 341d5d3722b8a82ff8dc46b5d7c8217dd981e6f2 Mon Sep 17 00:00:00 2001 From: Andreas Date: Tue, 17 Mar 2015 20:44:01 +0100 Subject: [PATCH] Once track can be played gaplessly. But the gst pipeline position isn't reset to 0 and this causes further problems. --- ext/clementine-spotifyblob/mediapipeline.cpp | 8 +- ext/clementine-spotifyblob/mediapipeline.h | 3 + ext/clementine-spotifyblob/spotifyclient.cpp | 105 +++++++++++++----- ext/clementine-spotifyblob/spotifyclient.h | 3 + ext/libclementine-common/core/logging.cpp | 2 +- .../spotifymessages.proto | 1 + src/engines/gstengine.cpp | 12 +- src/engines/gstenginepipeline.cpp | 21 +++- src/engines/gstenginepipeline.h | 2 + src/internet/spotify/spotifyserver.cpp | 4 +- src/internet/spotify/spotifyserver.h | 2 +- src/internet/spotify/spotifyservice.cpp | 6 +- src/internet/spotify/spotifyservice.h | 1 + src/internet/spotify/spotifysettingspage.cpp | 2 + src/internet/spotify/spotifysettingspage.ui | 24 +++- 15 files changed, 151 insertions(+), 45 deletions(-) diff --git a/ext/clementine-spotifyblob/mediapipeline.cpp b/ext/clementine-spotifyblob/mediapipeline.cpp index 5bb9ac6cb..c44172917 100644 --- a/ext/clementine-spotifyblob/mediapipeline.cpp +++ b/ext/clementine-spotifyblob/mediapipeline.cpp @@ -116,7 +116,7 @@ bool MediaPipeline::Init(int sample_rate, int channels) { // Set size byte_rate_ = quint64(sample_rate) * channels * 2; - const quint64 bytes = byte_rate_ * length_msec_ / 1000; + const quint64 bytes = -1; //byte_rate_ * length_msec_ / 1000; gst_app_src_set_size(appsrc_, bytes); // Ready to go @@ -143,6 +143,12 @@ void MediaPipeline::WriteData(const char* data, qint64 length) { gst_app_src_push_buffer(appsrc_, buffer); } +void MediaPipeline::SendEvent() { + GstTagList* list = gst_tag_list_new_empty(); + GstEvent* tag = gst_event_new_tag(list); + gst_element_send_event(pipeline_, tag); +} + void MediaPipeline::EndStream() { if (!is_initialised()) return; diff --git a/ext/clementine-spotifyblob/mediapipeline.h b/ext/clementine-spotifyblob/mediapipeline.h index 32c9b0420..dc5b5d15b 100644 --- a/ext/clementine-spotifyblob/mediapipeline.h +++ b/ext/clementine-spotifyblob/mediapipeline.h @@ -37,6 +37,9 @@ class MediaPipeline { void WriteData(const char* data, qint64 length); void EndStream(); + void SendEvent(); + + int port() const { return port_; } private: static void NeedDataCallback(GstAppSrc* src, guint length, void* data); diff --git a/ext/clementine-spotifyblob/spotifyclient.cpp b/ext/clementine-spotifyblob/spotifyclient.cpp index cf6c2a22d..151d92de7 100644 --- a/ext/clementine-spotifyblob/spotifyclient.cpp +++ b/ext/clementine-spotifyblob/spotifyclient.cpp @@ -321,11 +321,14 @@ void SpotifyClient::SetPlaybackSettings( } qLog(Debug) << "Setting playback settings: bitrate" << bitrate - << "normalisation" << req.volume_normalisation(); + << "normalisation" << req.volume_normalisation() + << "gapless" << req.gapless(); sp_session_preferred_bitrate(session_, bitrate); sp_session_preferred_offline_bitrate(session_, bitrate, false); sp_session_set_volume_normalization(session_, req.volume_normalisation()); + + gapless_playback_ = req.gapless(); } void SpotifyClient::Login(const pb::spotify::LoginRequest& req) { @@ -762,6 +765,38 @@ void SpotifyClient::MetadataUpdatedCallback(sp_session* session) { me->pending_playback_requests_) { me->TryPlaybackAgain(playback); } + if (me->gapless_playback_) { + for (const PrefetchTrackRequest req : me->prefetched_tracks_.values()) { + me->ContinueGaplessPlayback(req); + } + } + + qLog(Debug) << "MetadataUpdatedCallback"; +} + +void SpotifyClient::ContinueGaplessPlayback(const PrefetchTrackRequest& req) { + if (!gapless_playback_) + return; + + // If the track was not loaded then we have to come back later + if (!sp_track_is_loaded(req.track_)) { + qLog(Debug) << "Playback track not loaded yet, will try again later"; + return; + } + + sp_error error = sp_session_player_load(session_, req.track_); + qLog(Debug) << Q_FUNC_INFO << sp_error_message(error); + + //int port = media_pipeline_->port(); + //media_pipeline_.reset(new MediaPipeline(port, + // sp_track_duration(req.track_))); + + error = sp_session_player_play(session_, true); + qLog(Debug) << Q_FUNC_INFO << sp_error_message(error); + + sp_link_release(req.link_); + + prefetched_tracks_.clear(); } int SpotifyClient::MusicDeliveryCallback(sp_session* session, @@ -791,8 +826,23 @@ int SpotifyClient::MusicDeliveryCallback(sp_session* session, return 0; } - me->media_pipeline_->WriteData(reinterpret_cast(frames), - num_frames * format->channels * 2); + const char* buf = reinterpret_cast(frames); + bool buf_null = true; + + // Spotify sends a buffer with only null at the end of each track. To get gapless playback + // we have to check if the buffer is empty and discard this data. + if (me->gapless_playback_) { + for (int i=0;igapless_playback_ || !buf_null) { + me->media_pipeline_->WriteData(buf, num_frames * format->channels * 2); + } return num_frames; } @@ -801,7 +851,13 @@ void SpotifyClient::EndOfTrackCallback(sp_session* session) { SpotifyClient* me = reinterpret_cast(sp_session_userdata(session)); - me->media_pipeline_.reset(); + if (me->gapless_playback_ && !me->prefetched_tracks_.isEmpty()) { + for (const PrefetchTrackRequest req : me->prefetched_tracks_.values()) { + me->ContinueGaplessPlayback(req); + } + } else { + me->media_pipeline_.reset(); + } } void SpotifyClient::StreamingErrorCallback(sp_session* session, @@ -909,34 +965,20 @@ int SpotifyClient::GetDownloadProgress(sp_playlist* playlist) { void SpotifyClient::StartPlayback(const pb::spotify::PlaybackRequest& req) { QString uri = QString::fromStdString(req.track_uri()); - sp_link* link; - sp_track* track; - - if (prefetched_tracks_.contains(uri)) { - PrefetchTrackRequest prefetch_request; - prefetch_request = prefetched_tracks_.take(uri); - link = prefetch_request.link_; - track = prefetch_request.track_; - - qLog(Debug) << "Using prefetched track"; - } else { - // Get a link object from the URI - link = sp_link_create_from_string(req.track_uri().c_str()); - if (!link) { - SendPlaybackError("Invalid Spotify URI"); - return; - } - - // Get the track from the link - track = sp_link_as_track(link); - if (!track) { - SendPlaybackError("Spotify URI was not a track"); - sp_link_release(link); - return; - } + // Get a link object from the URI + sp_link* link = sp_link_create_from_string(req.track_uri().c_str()); + if (!link) { + SendPlaybackError("Invalid Spotify URI"); + return; } - prefetched_tracks_.clear(); + // Get the track from the link + sp_track* track = sp_link_as_track(link); + if (!track) { + SendPlaybackError("Spotify URI was not a track"); + sp_link_release(link); + return; + } PendingPlaybackRequest pending_playback; pending_playback.request_ = req; @@ -1019,6 +1061,9 @@ void SpotifyClient::PrefetchTrack(const pb::spotify::PrefetchRequest &req) { prefetch_request.track_ = track; prefetched_tracks_.insert(uri, prefetch_request); + + qDebug() << "Sending event"; + media_pipeline_->SendEvent(); } void SpotifyClient::SendPlaybackError(const QString& error) { diff --git a/ext/clementine-spotifyblob/spotifyclient.h b/ext/clementine-spotifyblob/spotifyclient.h index 3756e1aad..1c6cfa428 100644 --- a/ext/clementine-spotifyblob/spotifyclient.h +++ b/ext/clementine-spotifyblob/spotifyclient.h @@ -180,6 +180,8 @@ class SpotifyClient : public AbstractMessageHandler { void SendDownloadProgress(pb::spotify::PlaylistType type, int index, int download_progress); + void ContinueGaplessPlayback(const PrefetchTrackRequest& req); + QByteArray api_key_; QTcpSocket* protocol_socket_; @@ -202,6 +204,7 @@ class SpotifyClient : public AbstractMessageHandler { QMap pending_toplist_browses_; QMap prefetched_tracks_; + bool gapless_playback_; QMap> pending_search_album_browses_; QMap pending_search_album_browse_responses_; diff --git a/ext/libclementine-common/core/logging.cpp b/ext/libclementine-common/core/logging.cpp index acbc0e2f0..10894944d 100644 --- a/ext/libclementine-common/core/logging.cpp +++ b/ext/libclementine-common/core/logging.cpp @@ -39,7 +39,7 @@ static Level sDefaultLevel = Level_Debug; static QMap* sClassLevels = nullptr; static QIODevice* sNullDevice = nullptr; -const char* kDefaultLogLevels = "GstEnginePipeline:2,*:3"; +const char* kDefaultLogLevels = "GstEnginePipeline:3,*:3"; static const char* kMessageHandlerMagic = "__logging_message__"; static const int kMessageHandlerMagicLength = strlen(kMessageHandlerMagic); diff --git a/ext/libclementine-spotifyblob/spotifymessages.proto b/ext/libclementine-spotifyblob/spotifymessages.proto index 81ee035db..28e2aaa18 100644 --- a/ext/libclementine-spotifyblob/spotifymessages.proto +++ b/ext/libclementine-spotifyblob/spotifymessages.proto @@ -190,6 +190,7 @@ enum Bitrate { message PlaybackSettings { optional Bitrate bitrate = 1 [default = Bitrate320k]; optional bool volume_normalisation = 2 [default = false]; + optional bool gapless = 3 [default = false]; } message PauseRequest { diff --git a/src/engines/gstengine.cpp b/src/engines/gstengine.cpp index b9397902f..3dd6e411a 100755 --- a/src/engines/gstengine.cpp +++ b/src/engines/gstengine.cpp @@ -657,6 +657,12 @@ void GstEngine::timerEvent(QTimerEvent* e) { // only if we know the length of the current stream... if (current_length > 0) { + if (remaining < 0) { + qDebug() << "Remaning 0" << current_pipeline_->has_next_valid_url(); + EndOfStreamReached(current_pipeline_->id(), current_pipeline_->has_next_valid_url()); + //current_pipeline_->SpotifyMovedToNextTrack(); + return; + } // emit TrackAboutToEnd when we're a few seconds away from finishing if (remaining < gap + fudge) { EmitAboutToEnd(); @@ -699,10 +705,12 @@ void GstEngine::EndOfStreamReached(int pipeline_id, bool has_next_track) { if (!current_pipeline_.get() || current_pipeline_->id() != pipeline_id) return; - /*if (!has_next_track) { + if (!has_next_track) { current_pipeline_.reset(); BufferingFinished(); - }*/ + } else { + current_pipeline_->SpotifyMovedToNextTrack(); + } emit TrackEnded(); } diff --git a/src/engines/gstenginepipeline.cpp b/src/engines/gstenginepipeline.cpp index e354b743b..172714233 100644 --- a/src/engines/gstenginepipeline.cpp +++ b/src/engines/gstenginepipeline.cpp @@ -487,12 +487,12 @@ GstBusSyncReply GstEnginePipeline::BusCallbackSync(GstBus*, GstMessage* msg, gpointer self) { GstEnginePipeline* instance = reinterpret_cast(self); - //qLog(Debug) << instance->id() << "sync bus message" - // << GST_MESSAGE_TYPE_NAME(msg); + qLog(Debug) << instance->id() << "sync bus message" + << GST_MESSAGE_TYPE_NAME(msg); switch (GST_MESSAGE_TYPE(msg)) { case GST_MESSAGE_EOS: - emit instance->EndOfStreamReached(instance->id(), false); + emit instance->EndOfStreamReached(instance->id(), instance->has_next_valid_url()); break; case GST_MESSAGE_TAG: @@ -1173,3 +1173,18 @@ void GstEnginePipeline::SetNextUrl(const QUrl& url, qint64 beginning_nanosec, Q_ARG(QUrl, url)); } } + +void GstEnginePipeline::SpotifyMovedToNextTrack() { + url_ = next_url_; + end_offset_nanosec_ = next_end_offset_nanosec_; + next_url_ = QUrl(); + next_beginning_offset_nanosec_ = 0; + next_end_offset_nanosec_ = 0; + + // This function gets called when the source has been drained, even if the + // song hasn't finished playing yet. We'll get a new stream when it really + // does finish, so emit TrackEnded then. + emit_track_ended_on_stream_start_ = true; + + gst_element_seek_simple(pipeline_, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH, 0); +} diff --git a/src/engines/gstenginepipeline.h b/src/engines/gstenginepipeline.h index f1f6c173f..53ab67b4a 100644 --- a/src/engines/gstenginepipeline.h +++ b/src/engines/gstenginepipeline.h @@ -82,6 +82,8 @@ class GstEnginePipeline : public QObject { void SetNextUrl(const QUrl& url, qint64 beginning_nanosec, qint64 end_nanosec); bool has_next_valid_url() const { return next_url_.isValid(); } + QUrl next_url() const { return next_url_; } + void SpotifyMovedToNextTrack(); // Get information about the music playback QUrl url() const { return url_; } diff --git a/src/internet/spotify/spotifyserver.cpp b/src/internet/spotify/spotifyserver.cpp index a45eba1e5..f39bbe455 100644 --- a/src/internet/spotify/spotifyserver.cpp +++ b/src/internet/spotify/spotifyserver.cpp @@ -77,7 +77,8 @@ void SpotifyServer::SendOrQueueMessage(const pb::spotify::Message& message) { void SpotifyServer::Login(const QString& username, const QString& password, pb::spotify::Bitrate bitrate, - bool volume_normalisation) { + bool volume_normalisation, + bool gapless) { pb::spotify::Message message; pb::spotify::LoginRequest* request = message.mutable_login_request(); @@ -88,6 +89,7 @@ void SpotifyServer::Login(const QString& username, const QString& password, request->mutable_playback_settings()->set_bitrate(bitrate); request->mutable_playback_settings()->set_volume_normalisation( volume_normalisation); + request->mutable_playback_settings()->set_gapless(gapless); SendOrQueueMessage(message); } diff --git a/src/internet/spotify/spotifyserver.h b/src/internet/spotify/spotifyserver.h index 50d4695cc..d0bad67bf 100644 --- a/src/internet/spotify/spotifyserver.h +++ b/src/internet/spotify/spotifyserver.h @@ -39,7 +39,7 @@ class SpotifyServer : public AbstractMessageHandler { void Init(); void Login(const QString& username, const QString& password, - pb::spotify::Bitrate bitrate, bool volume_normalisation); + pb::spotify::Bitrate bitrate, bool volume_normalisation, bool gapless); void LoadStarred(); void SyncStarred(); diff --git a/src/internet/spotify/spotifyservice.cpp b/src/internet/spotify/spotifyservice.cpp index 8ec4d6da7..1fe77614d 100644 --- a/src/internet/spotify/spotifyservice.cpp +++ b/src/internet/spotify/spotifyservice.cpp @@ -86,7 +86,8 @@ SpotifyService::SpotifyService(Application* app, InternetModel* parent) search_delay_(new QTimer(this)), login_state_(LoginState_OtherError), bitrate_(pb::spotify::Bitrate320k), - volume_normalisation_(false) { + volume_normalisation_(false), + gapless_(false) { // Build the search path for the binary blob. // Look for one distributed alongside clementine first, then check in the // user's home directory for any that have been downloaded. @@ -249,6 +250,7 @@ void SpotifyService::ReloadSettings() { bitrate_ = static_cast( s.value("bitrate", pb::spotify::Bitrate320k).toInt()); volume_normalisation_ = s.value("volume_normalisation", false).toBool(); + gapless_ = s.value("gapless", false).toBool(); if (server_ && blob_process_) { server_->SetPlaybackSettings(bitrate_, volume_normalisation_); @@ -306,7 +308,7 @@ void SpotifyService::EnsureServerCreated(const QString& username, } server_->Login(login_username, login_password, bitrate_, - volume_normalisation_); + volume_normalisation_, gapless_); StartBlobProcess(); } diff --git a/src/internet/spotify/spotifyservice.h b/src/internet/spotify/spotifyservice.h index 3bcc4ddfd..10a53f81d 100644 --- a/src/internet/spotify/spotifyservice.h +++ b/src/internet/spotify/spotifyservice.h @@ -191,6 +191,7 @@ signals: LoginState login_state_; pb::spotify::Bitrate bitrate_; bool volume_normalisation_; + bool gapless_; }; #endif // INTERNET_SPOTIFY_SPOTIFYSERVICE_H_ diff --git a/src/internet/spotify/spotifysettingspage.cpp b/src/internet/spotify/spotifysettingspage.cpp index 4a35de177..64d83d13b 100644 --- a/src/internet/spotify/spotifysettingspage.cpp +++ b/src/internet/spotify/spotifysettingspage.cpp @@ -111,6 +111,7 @@ void SpotifySettingsPage::Load() { s.value("bitrate", pb::spotify::Bitrate320k).toInt())); ui_->volume_normalisation->setChecked( s.value("volume_normalisation", false).toBool()); + ui_->gapless->setChecked(s.value("gapless", false).toBool()); UpdateLoginState(); } @@ -125,6 +126,7 @@ void SpotifySettingsPage::Save() { s.setValue("bitrate", ui_->bitrate->itemData(ui_->bitrate->currentIndex()).toInt()); s.setValue("volume_normalisation", ui_->volume_normalisation->isChecked()); + s.setValue("gapless", ui_->gapless->isChecked()); } void SpotifySettingsPage::LoginFinished(bool success) { diff --git a/src/internet/spotify/spotifysettingspage.ui b/src/internet/spotify/spotifysettingspage.ui index cc7d9632d..ddd6d42e5 100644 --- a/src/internet/spotify/spotifysettingspage.ui +++ b/src/internet/spotify/spotifysettingspage.ui @@ -29,7 +29,16 @@ true - + + 0 + + + 0 + + + 0 + + 0 @@ -135,7 +144,14 @@ - + + + + Gapless playback + + + + Use volume normalisation @@ -188,7 +204,7 @@ - :/spotify-attribution.png + :/spotify-attribution.png @@ -205,7 +221,7 @@ - +