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:
parent
bc1d56f935
commit
160b151652
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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.
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user