1
0
mirror of https://github.com/clementine-player/Clementine synced 2025-01-31 11:35:24 +01:00

Add seek ability to Spotify tracks.

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
This commit is contained in:
Arnaud Bienner 2014-09-14 02:15:58 +02:00
parent bc1d56f935
commit 160b151652
10 changed files with 76 additions and 10 deletions

View File

@ -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);

View File

@ -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;
}
@ -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) {

View File

@ -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);

View File

@ -173,6 +173,9 @@ message SeekRequest {
optional int64 offset_bytes = 1;
}
message SeekCompleted {
}
enum Bitrate {
Bitrate96k = 1;
Bitrate160k = 2;
@ -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;
@ -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;
}

View File

@ -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),
@ -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,
@ -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(),
@ -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;
}
@ -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;
@ -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();

View File

@ -163,6 +163,7 @@ signals:
private slots:
void FaderTimelineFinished();
void SpotifySeekCompleted();
private:
static const int kGstStateTimeoutNanosecs;
@ -228,6 +229,13 @@ signals:
// 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.

View File

@ -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();
}
}

View File

@ -72,6 +72,7 @@ signals:
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);

View File

@ -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;
@ -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();

View File

@ -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;