Skip to content

Commit

Permalink
Add seek ability to Spotify tracks.
Browse files Browse the repository at this point in the history
This is functional but pretty hacky.
And, as noted in the comments, there is a small delay (depends, but usually several seconds) to have the seek taken into account. But IMHO it's better than nothing.
Fixes #2503
  • Loading branch information
ArnaudBienner committed Sep 14, 2014
1 parent bc1d56f commit 160b151
Show file tree
Hide file tree
Showing 10 changed files with 76 additions and 10 deletions.
5 changes: 3 additions & 2 deletions ext/clementine-spotifyblob/mediapipeline.cpp
Expand Up @@ -81,8 +81,9 @@ bool MediaPipeline::Init(int sample_rate, int channels) {

// Try to send 5 seconds of audio in advance to initially fill Clementine's
// buffer.
g_object_set(G_OBJECT(tcpsink_), "ts-offset", qint64(-5 * kNsecPerSec),
nullptr);
// Commented for now as otherwise the seek will take too long.
//g_object_set(G_OBJECT(tcpsink_), "ts-offset", qint64(-5 * kNsecPerSec),
// nullptr);

// We know the time of each buffer
g_object_set(G_OBJECT(appsrc_), "format", GST_FORMAT_TIME, nullptr);
Expand Down
15 changes: 13 additions & 2 deletions ext/clementine-spotifyblob/spotifyclient.cpp
Expand Up @@ -684,6 +684,9 @@ int SpotifyClient::MusicDeliveryCallback(sp_session* session,
}

if (num_frames == 0) {
// According to libspotify documentation, this occurs when a discontinuity
// has occurred (such as after a seek). Maybe should clear buffers here as
// well? (in addition of clearing buffers in gstenginepipeline.cpp)
return 0;
}

Expand Down Expand Up @@ -842,8 +845,16 @@ void SpotifyClient::StartPlayback(const pb::spotify::PlaybackRequest& req) {
}

void SpotifyClient::Seek(qint64 offset_bytes) {
// TODO
qLog(Error) << "TODO seeking";
if (sp_session_player_seek(session_, offset_bytes) != SP_ERROR_OK) {
qLog(Error) << "Seek error";
return;
}

pb::spotify::Message message;

pb::spotify::SeekCompleted* response = message.mutable_seek_completed();
Q_UNUSED(response);
SendMessage(message);
}

void SpotifyClient::TryPlaybackAgain(const PendingPlaybackRequest& req) {
Expand Down
1 change: 1 addition & 0 deletions ext/clementine-spotifyblob/spotifyclient.h
Expand Up @@ -59,6 +59,7 @@ class SpotifyClient : public AbstractMessageHandler<pb::spotify::Message> {
pb::spotify::LoginResponse_Error error_code);
void SendPlaybackError(const QString& error);
void SendSearchResponse(sp_search* result);
void SendSeekCompleted();

// Spotify session callbacks.
static void SP_CALLCONV LoggedInCallback(sp_session* session, sp_error error);
Expand Down
6 changes: 5 additions & 1 deletion ext/libclementine-spotifyblob/spotifymessages.proto
Expand Up @@ -173,6 +173,9 @@ message SeekRequest {
optional int64 offset_bytes = 1;
}

message SeekCompleted {
}

enum Bitrate {
Bitrate96k = 1;
Bitrate160k = 2;
Expand All @@ -188,7 +191,7 @@ message PauseRequest {
optional bool paused = 1 [default = false];
}

// NEXT_ID: 21
// NEXT_ID: 23
message Message {
// Not currently used
optional int32 id = 18;
Expand All @@ -213,4 +216,5 @@ message Message {
optional BrowseToplistRequest browse_toplist_request = 19;
optional BrowseToplistResponse browse_toplist_response = 20;
optional PauseRequest pause_request = 21;
optional SeekCompleted seek_completed = 22;
}
36 changes: 36 additions & 0 deletions src/engines/gstenginepipeline.cpp
Expand Up @@ -66,6 +66,7 @@ GstEnginePipeline::GstEnginePipeline(GstEngine* engine)
buffering_(false),
mono_playback_(false),
end_offset_nanosec_(-1),
spotify_offset_(0),
next_beginning_offset_nanosec_(-1),
next_end_offset_nanosec_(-1),
ignore_next_seek_(false),
Expand Down Expand Up @@ -93,6 +94,10 @@ GstEnginePipeline::GstEnginePipeline(GstEngine* engine)
}

for (int i = 0; i < kEqBandCount; ++i) eq_band_gains_ << 0;

// Spotify hack
connect(InternetModel::Service<SpotifyService>()->server(), SIGNAL(SeekCompleted()),
SLOT(SpotifySeekCompleted()));
}

void GstEnginePipeline::set_output_device(const QString& sink,
Expand Down Expand Up @@ -165,9 +170,15 @@ bool GstEnginePipeline::ReplaceDecodeBin(const QUrl& url) {
gst_element_add_pad(GST_ELEMENT(new_bin), gst_ghost_pad_new("src", pad));
gst_object_unref(GST_OBJECT(pad));

// g_object_set(G_OBJECT(new_bin), "max-size-time", 100,
// nullptr);
// g_object_set(G_OBJECT(new_bin), "use-buffering", true, nullptr);


// Tell spotify to start sending data to us.
InternetModel::Service<SpotifyService>()->server()->StartPlaybackLater(
url.toString(), port);
spotify_offset_ = 0;
} else {
new_bin = engine_->CreateElement("uridecodebin");
g_object_set(G_OBJECT(new_bin), "uri", url.toEncoded().constData(),
Expand Down Expand Up @@ -896,6 +907,10 @@ qint64 GstEnginePipeline::position() const {
gint64 value = 0;
gst_element_query_position(pipeline_, &fmt, &value);

if (url_.scheme() == "spotify") {
value += spotify_offset_;
}

return value;
}

Expand Down Expand Up @@ -944,6 +959,18 @@ bool GstEnginePipeline::Seek(qint64 nanosec) {
return true;
}

if (url_.scheme() == "spotify" && !buffering_) {
SpotifyService* spotify = InternetModel::Service<SpotifyService>();
// Need to schedule this in the spotify service's thread
QMetaObject::invokeMethod(spotify, "Seek", Qt::QueuedConnection,
Q_ARG(int, nanosec / kNsecPerMsec));
// Need to reset spotify_offset_ to get the real pipeline position, as it is
// used in position()
spotify_offset_ = 0;
spotify_offset_ = nanosec - position() ;
return true;
}

if (!pipeline_is_connected_ || !pipeline_is_initialised_) {
pending_seek_nanosec_ = nanosec;
return true;
Expand All @@ -954,6 +981,15 @@ bool GstEnginePipeline::Seek(qint64 nanosec) {
GST_SEEK_FLAG_FLUSH, nanosec);
}

void GstEnginePipeline::SpotifySeekCompleted() {
qLog(Debug) << "Spotify Seek completed";
// FIXME: we should clear buffers to start playing data from seek point right
// now (currently there is small delay) but I didn't managed to tell gstreamer
// to do this without breaking the streaming completely...
// Funny thing to notice: for me the delay varies when changing buffer size,
// but a larger buffer doesn't necessary increase the delay.
}

void GstEnginePipeline::SetEqualizerEnabled(bool enabled) {
eq_enabled_ = enabled;
UpdateEqualizer();
Expand Down
8 changes: 8 additions & 0 deletions src/engines/gstenginepipeline.h
Expand Up @@ -163,6 +163,7 @@ class GstEnginePipeline : public QObject {

private slots:
void FaderTimelineFinished();
void SpotifySeekCompleted();

private:
static const int kGstStateTimeoutNanosecs;
Expand Down Expand Up @@ -228,6 +229,13 @@ class GstEnginePipeline : public QObject {
// past this position.
qint64 end_offset_nanosec_;

// Another Spotify hack...
// Used in position(). We need this because when seeking Spotify tracks, we
// don't actually seek the pipeline, but ask libspotify to send us data with
// a seek offset instead. So querying the pipeline to get track's position
// wouldn't make sense.
qint64 spotify_offset_;

// We store the beginning and end for the preloading song too, so we can just
// carry on without reloading the file if the sections carry on from each
// other.
Expand Down
2 changes: 2 additions & 0 deletions src/internet/spotifyserver.cpp
Expand Up @@ -154,6 +154,8 @@ void SpotifyServer::MessageArrived(const pb::spotify::Message& message) {
emit AlbumBrowseResults(message.browse_album_response());
} else if (message.has_browse_toplist_response()) {
emit ToplistBrowseResults(message.browse_toplist_response());
} else if (message.has_seek_completed()) {
emit SeekCompleted();
}
}

Expand Down
1 change: 1 addition & 0 deletions src/internet/spotifyserver.h
Expand Up @@ -72,6 +72,7 @@ class SpotifyServer : public AbstractMessageHandler<pb::spotify::Message> {
void SyncPlaylistProgress(const pb::spotify::SyncPlaylistProgress& progress);
void AlbumBrowseResults(const pb::spotify::BrowseAlbumResponse& response);
void ToplistBrowseResults(const pb::spotify::BrowseToplistResponse& response);
void SeekCompleted();

protected:
void MessageArrived(const pb::spotify::Message& message);
Expand Down
10 changes: 6 additions & 4 deletions src/internet/spotifyservice.cpp
Expand Up @@ -528,10 +528,6 @@ void SpotifyService::SongFromProtobuf(const pb::spotify::Track& track,
song->set_filesize(0);
}

PlaylistItem::Options SpotifyService::playlistitem_options() const {
return PlaylistItem::SeekDisabled;
}

QWidget* SpotifyService::HeaderWidget() const {
if (IsLoggedIn()) return search_box_;
return nullptr;
Expand Down Expand Up @@ -702,6 +698,12 @@ void SpotifyService::SetPaused(const bool paused) {
server_->SetPaused(paused);
}

void SpotifyService::Seek(const int offset /* in msec */) {
EnsureServerCreated();
server_->Seek(offset);
}


void SpotifyService::SyncPlaylistProgress(
const pb::spotify::SyncPlaylistProgress& progress) {
qLog(Debug) << "Sync progress:" << progress.sync_progress();
Expand Down
2 changes: 1 addition & 1 deletion src/internet/spotifyservice.h
Expand Up @@ -53,13 +53,13 @@ class SpotifyService : public InternetService {
void ShowContextMenu(const QPoint& global_pos);
void ItemDoubleClicked(QStandardItem* item);
void DropMimeData(const QMimeData* data, const QModelIndex& index);
PlaylistItem::Options playlistitem_options() const;
QWidget* HeaderWidget() const;

void Logout();
void Login(const QString& username, const QString& password);
Q_INVOKABLE void LoadImage(const QString& id);
Q_INVOKABLE void SetPaused(const bool paused);
Q_INVOKABLE void Seek(const int offset /* in msec */);

SpotifyServer* server() const;

Expand Down

0 comments on commit 160b151

Please sign in to comment.