Add MediaPlaybackRequest class.

Add a class to wrap the URL in the playback engines. In the future, this will
contain authentication information for the specified URL. It can also include
the start and end time as well as other data that is currently specified along
with the URL.
This commit is contained in:
Jim Broadus 2020-02-17 22:24:07 -08:00 committed by John Maguire
parent 2c1ae986c5
commit 9ed5503ee3
8 changed files with 105 additions and 65 deletions

View File

@ -146,9 +146,10 @@ void Player::HandleLoadResult(const UrlHandler::LoadResult& result) {
item->SetTemporaryMetadata(song);
app_->playlist_manager()->active()->InformOfCurrentSongChange();
}
engine_->Play(
result.media_url_, stream_change_type_, item->Metadata().has_cue(),
item->Metadata().beginning_nanosec(), item->Metadata().end_nanosec());
MediaPlaybackRequest req(result.media_url_);
engine_->Play(req, stream_change_type_, item->Metadata().has_cue(),
item->Metadata().beginning_nanosec(),
item->Metadata().end_nanosec());
current_item_ = item;
loading_async_ = QUrl();
@ -420,8 +421,8 @@ void Player::PlayAt(int index, Engine::TrackChangeFlags change,
HandleLoadResult(url_handlers_[url.scheme()]->StartLoading(url));
} else {
loading_async_ = QUrl();
engine_->Play(current_item_->Url(), change,
current_item_->Metadata().has_cue(),
MediaPlaybackRequest req(current_item_->Url());
engine_->Play(req, change, current_item_->Metadata().has_cue(),
current_item_->Metadata().beginning_nanosec(),
current_item_->Metadata().end_nanosec());
@ -604,7 +605,8 @@ void Player::TrackAboutToEnd() {
break;
}
}
engine_->StartPreloading(url, next_item->Metadata().has_cue(),
MediaPlaybackRequest req(url);
engine_->StartPreloading(req, next_item->Metadata().has_cue(),
next_item->Metadata().beginning_nanosec(),
next_item->Metadata().end_nanosec());
}

View File

@ -43,12 +43,12 @@ Engine::Base::Base()
Engine::Base::~Base() {}
bool Engine::Base::Load(const QUrl& url, TrackChangeFlags,
bool Engine::Base::Load(const MediaPlaybackRequest& req, TrackChangeFlags,
bool force_stop_at_end, quint64 beginning_nanosec,
qint64 end_nanosec) {
Q_UNUSED(force_stop_at_end);
url_ = url;
playback_req_ = req;
beginning_nanosec_ = beginning_nanosec;
end_nanosec_ = end_nanosec;
@ -92,10 +92,10 @@ void Engine::Base::EmitAboutToEnd() {
int Engine::Base::AddBackgroundStream(const QUrl& url) { return -1; }
bool Engine::Base::Play(const QUrl& u, TrackChangeFlags c,
bool Engine::Base::Play(const MediaPlaybackRequest& req, TrackChangeFlags c,
bool force_stop_at_end, quint64 beginning_nanosec,
qint64 end_nanosec) {
if (!Load(u, c, force_stop_at_end, beginning_nanosec, end_nanosec))
if (!Load(req, c, force_stop_at_end, beginning_nanosec, end_nanosec))
return false;
return Play(0);

View File

@ -32,6 +32,7 @@
#include <QUrl>
#include "engine_fwd.h"
#include "playbackrequest.h"
namespace Engine {
@ -45,7 +46,8 @@ class Base : public QObject {
virtual bool Init() = 0;
virtual void StartPreloading(const QUrl&, bool, qint64, qint64) {}
virtual void StartPreloading(const MediaPlaybackRequest&, bool, qint64,
qint64) {}
virtual bool Play(quint64 offset_nanosec) = 0;
virtual void Stop(bool stop_after = false) = 0;
virtual void Pause() = 0;
@ -62,7 +64,7 @@ class Base : public QObject {
// Subclasses should respect given markers (beginning and end) which are
// in milliseconds.
virtual bool Load(const QUrl& url, TrackChangeFlags change,
virtual bool Load(const MediaPlaybackRequest& req, TrackChangeFlags change,
bool force_stop_at_end, quint64 beginning_nanosec,
qint64 end_nanosec);
// Sets new values for the beginning and end markers of the currently playing
@ -78,8 +80,9 @@ class Base : public QObject {
// to the given 'end' (usually from 0 to a song's length). Both markers
// should be passed in nanoseconds. 'end' can be negative, indicating that the
// real length of 'u' stream is unknown.
bool Play(const QUrl& u, TrackChangeFlags c, bool force_stop_at_end,
quint64 beginning_nanosec, qint64 end_nanosec);
bool Play(const MediaPlaybackRequest& req, TrackChangeFlags c,
bool force_stop_at_end, quint64 beginning_nanosec,
qint64 end_nanosec);
void SetVolume(uint value);
@ -139,7 +142,7 @@ signals:
uint volume_;
quint64 beginning_nanosec_;
qint64 end_nanosec_;
QUrl url_;
MediaPlaybackRequest playback_req_;
Scope scope_;
bool fadeout_enabled_;

View File

@ -251,7 +251,8 @@ qint64 GstEngine::length_nanosec() const {
}
Engine::State GstEngine::state() const {
if (!current_pipeline_) return url_.isEmpty() ? Engine::Empty : Engine::Idle;
if (!current_pipeline_)
return playback_req_.url_.isEmpty() ? Engine::Empty : Engine::Idle;
switch (current_pipeline_->state()) {
case GST_STATE_NULL:
@ -368,23 +369,24 @@ void GstEngine::UpdateScope(int chunk_length) {
}
}
void GstEngine::StartPreloading(const QUrl& url, bool force_stop_at_end,
void GstEngine::StartPreloading(const MediaPlaybackRequest& req,
bool force_stop_at_end,
qint64 beginning_nanosec, qint64 end_nanosec) {
EnsureInitialised();
// No crossfading, so we can just queue the new URL in the existing
// pipeline and get gapless playback (hopefully)
if (current_pipeline_)
current_pipeline_->SetNextUrl(url, beginning_nanosec,
current_pipeline_->SetNextReq(req, beginning_nanosec,
force_stop_at_end ? end_nanosec : 0);
}
bool GstEngine::Load(const QUrl& url, Engine::TrackChangeFlags change,
bool force_stop_at_end, quint64 beginning_nanosec,
qint64 end_nanosec) {
bool GstEngine::Load(const MediaPlaybackRequest& req,
Engine::TrackChangeFlags change, bool force_stop_at_end,
quint64 beginning_nanosec, qint64 end_nanosec) {
EnsureInitialised();
Engine::Base::Load(url, change, force_stop_at_end, beginning_nanosec,
Engine::Base::Load(req, change, force_stop_at_end, beginning_nanosec,
end_nanosec);
bool crossfade =
@ -397,7 +399,7 @@ bool GstEngine::Load(const QUrl& url, Engine::TrackChangeFlags change,
!crossfade_same_album_)
crossfade = false;
if (!crossfade && current_pipeline_ && current_pipeline_->url() == url &&
if (!crossfade && current_pipeline_ && current_pipeline_->url() == req.url_ &&
change & Engine::Auto) {
// We're not crossfading, and the pipeline is already playing the URI we
// want, so just do nothing.
@ -405,7 +407,7 @@ bool GstEngine::Load(const QUrl& url, Engine::TrackChangeFlags change,
}
shared_ptr<GstEnginePipeline> pipeline =
CreatePipeline(url, force_stop_at_end ? end_nanosec : 0);
CreatePipeline(req, force_stop_at_end ? end_nanosec : 0);
if (!pipeline) return false;
if (crossfade) StartFadeout();
@ -505,13 +507,14 @@ void GstEngine::PlayDone(QFuture<GstStateChangeReturn> future,
emit StateChanged(Engine::Playing);
// we've successfully started playing a media stream with this url
emit ValidSongRequested(url_);
emit ValidSongRequested(playback_req_.url_);
}
void GstEngine::Stop(bool stop_after) {
StopTimers();
url_ = QUrl(); // To ensure we return Empty from state()
playback_req_ =
MediaPlaybackRequest(); // To ensure we return Empty from state()
beginning_nanosec_ = end_nanosec_ = 0;
// Check if we started a fade out. If it isn't finished yet and the user
@ -695,14 +698,13 @@ void GstEngine::HandlePipelineError(int pipeline_id, const QString& message,
// try to reload the URL in case of a drop of the connection
if (domain == GST_RESOURCE_ERROR && error_code == GST_RESOURCE_ERROR_SEEK) {
if (Load(url_, 0, false, 0, 0)) {
if (Load(playback_req_, 0, false, 0, 0)) {
current_pipeline_->SetState(GST_STATE_PLAYING);
return;
}
qLog(Warning) << "Attempt to reload " << url_ << " failed";
qLog(Warning) << "Attempt to reload " << playback_req_.url_ << " failed";
}
current_pipeline_.reset();
@ -710,7 +712,7 @@ void GstEngine::HandlePipelineError(int pipeline_id, const QString& message,
BufferingFinished();
emit StateChanged(Engine::Error);
// unable to play media stream with this url
emit InvalidSongRequested(url_);
emit InvalidSongRequested(playback_req_.url_);
// TODO: the types of errors listed below won't be shown to user - they will
// get logged and the current song will be skipped; instead of maintaining
@ -822,21 +824,21 @@ shared_ptr<GstEnginePipeline> GstEngine::CreatePipeline() {
return ret;
}
shared_ptr<GstEnginePipeline> GstEngine::CreatePipeline(const QUrl& url,
qint64 end_nanosec) {
shared_ptr<GstEnginePipeline> GstEngine::CreatePipeline(
const MediaPlaybackRequest& req, qint64 end_nanosec) {
shared_ptr<GstEnginePipeline> ret = CreatePipeline();
if (url.scheme() == "hypnotoad") {
if (req.url_.scheme() == "hypnotoad") {
ret->InitFromString(kHypnotoadPipeline);
return ret;
}
if (url.scheme() == "enterprise") {
if (req.url_.scheme() == "enterprise") {
ret->InitFromString(kEnterprisePipeline);
return ret;
}
if (!ret->InitFromUrl(url, end_nanosec)) ret.reset();
if (!ret->InitFromReq(req, end_nanosec)) ret.reset();
return ret;
}
@ -885,7 +887,7 @@ int GstEngine::AddBackgroundStream(const QUrl& url) {
return -1;
}
pipeline->SetVolume(30);
pipeline->SetNextUrl(url, 0, 0);
pipeline->SetNextReq(url, 0, 0);
return AddBackgroundStream(pipeline);
}
@ -895,7 +897,7 @@ void GstEngine::StopBackgroundStream(int id) {
void GstEngine::BackgroundStreamFinished() {
GstEnginePipeline* pipeline = qobject_cast<GstEnginePipeline*>(sender());
pipeline->SetNextUrl(pipeline->url(), 0, 0);
pipeline->SetNextReq(pipeline->url(), 0, 0);
}
void GstEngine::SetBackgroundStreamVolume(int id, int volume) {

View File

@ -95,9 +95,9 @@ class GstEngine : public Engine::Base, public BufferConsumer {
void ConsumeBuffer(GstBuffer* buffer, int pipeline_id);
public slots:
void StartPreloading(const QUrl& url, bool force_stop_at_end,
void StartPreloading(const MediaPlaybackRequest& req, bool force_stop_at_end,
qint64 beginning_nanosec, qint64 end_nanosec);
bool Load(const QUrl&, Engine::TrackChangeFlags change,
bool Load(const MediaPlaybackRequest&, Engine::TrackChangeFlags change,
bool force_stop_at_end, quint64 beginning_nanosec,
qint64 end_nanosec);
bool Play(quint64 offset_nanosec);
@ -162,8 +162,8 @@ class GstEngine : public Engine::Base, public BufferConsumer {
void StopTimers();
std::shared_ptr<GstEnginePipeline> CreatePipeline();
std::shared_ptr<GstEnginePipeline> CreatePipeline(const QUrl& url,
qint64 end_nanosec);
std::shared_ptr<GstEnginePipeline> CreatePipeline(
const MediaPlaybackRequest& req, qint64 end_nanosec);
void UpdateScope(int chunk_length);

View File

@ -510,25 +510,25 @@ bool GstEnginePipeline::InitFromString(const QString& pipeline) {
return gst_element_link(new_bin, audiobin_);
}
bool GstEnginePipeline::InitFromUrl(const QUrl& url, qint64 end_nanosec) {
bool GstEnginePipeline::InitFromReq(const MediaPlaybackRequest& req,
qint64 end_nanosec) {
pipeline_ = gst_pipeline_new("pipeline");
if (url.scheme() == "cdda" && !url.path().isEmpty()) {
current_ = req;
if (current_.url_.scheme() == "cdda" && !current_.url_.path().isEmpty()) {
// Currently, Gstreamer can't handle input CD devices inside cdda URL. So
// we handle them ourself: we extract the track number and re-create an
// URL with only cdda:// + the track number (which can be handled by
// Gstreamer). We keep the device in mind, and we will set it later using
// SourceSetupCallback
QStringList path = url.path().split('/');
url_ = QUrl(QString("cdda://%1").arg(path.takeLast()));
QStringList path = current_.url_.path().split('/');
current_.url_ = QUrl(QString("cdda://%1").arg(path.takeLast()));
source_device_ = path.join("/");
} else {
url_ = url;
}
end_offset_nanosec_ = end_nanosec;
// Decode bin
if (!ReplaceDecodeBin(url_)) return false;
if (!ReplaceDecodeBin(current_.url_)) return false;
return Init();
}
@ -917,13 +917,13 @@ GstPadProbeReturn GstEnginePipeline::HandoffCallback(GstPad*,
if (end_time > instance->end_offset_nanosec_) {
if (instance->has_next_valid_url()) {
if (instance->next_url_ == instance->url_ &&
if (instance->next_.url_ == instance->current_.url_ &&
instance->next_beginning_offset_nanosec_ ==
instance->end_offset_nanosec_) {
// The "next" song is actually the next segment of this file - so
// cheat and keep on playing, but just tell the Engine we've moved on.
instance->end_offset_nanosec_ = instance->next_end_offset_nanosec_;
instance->next_url_ = QUrl();
instance->next_ = MediaPlaybackRequest();
instance->next_beginning_offset_nanosec_ = 0;
instance->next_end_offset_nanosec_ = 0;
@ -995,8 +995,8 @@ void GstEnginePipeline::SourceDrainedCallback(GstURIDecodeBin* bin,
// not start or with some offset. So just do nothing here: when the song
// finished, EndOfStreamReached/TrackEnded will be emitted anyway so
// NextItem will be called.
!(instance->url_.scheme() != "spotify" &&
instance->next_url_.scheme() == "spotify")) {
!(instance->current_.url_.scheme() != "spotify" &&
instance->next_.url_.scheme() == "spotify")) {
instance->TransitionToNext();
}
}
@ -1042,16 +1042,16 @@ void GstEnginePipeline::TransitionToNext() {
ignore_tags_ = true;
if (!ReplaceDecodeBin(next_url_)) {
qLog(Error) << "ReplaceDecodeBin failed with " << next_url_;
if (!ReplaceDecodeBin(next_.url_)) {
qLog(Error) << "ReplaceDecodeBin failed with " << next_.url_;
return;
}
gst_element_set_state(uridecodebin_, GST_STATE_PLAYING);
MaybeLinkDecodeToAudio();
url_ = next_url_;
current_ = next_;
end_offset_nanosec_ = next_end_offset_nanosec_;
next_url_ = QUrl();
next_ = MediaPlaybackRequest();
next_beginning_offset_nanosec_ = 0;
next_end_offset_nanosec_ = 0;
@ -1093,7 +1093,7 @@ GstState GstEnginePipeline::state() const {
QFuture<GstStateChangeReturn> GstEnginePipeline::SetState(GstState state) {
#ifdef HAVE_SPOTIFY
if (url_.scheme() == "spotify" && !buffering_) {
if (current_.url_.scheme() == "spotify" && !buffering_) {
const GstState current_state = this->state();
if (state == GST_STATE_PAUSED && current_state == GST_STATE_PLAYING) {
@ -1275,9 +1275,10 @@ void GstEnginePipeline::RemoveAllBufferConsumers() {
buffer_consumers_.clear();
}
void GstEnginePipeline::SetNextUrl(const QUrl& url, qint64 beginning_nanosec,
void GstEnginePipeline::SetNextReq(const MediaPlaybackRequest& req,
qint64 beginning_nanosec,
qint64 end_nanosec) {
next_url_ = url;
next_ = req;
next_beginning_offset_nanosec_ = beginning_nanosec;
next_end_offset_nanosec_ = end_nanosec;
}

View File

@ -31,6 +31,7 @@
#include <gst/gst.h>
#include "engine_fwd.h"
#include "playbackrequest.h"
class GstElementDeleter;
class GstEngine;
@ -58,7 +59,7 @@ class GstEnginePipeline : public QObject {
void set_sample_rate(int rate);
// Creates the pipeline, returns false on error
bool InitFromUrl(const QUrl& url, qint64 end_nanosec);
bool InitFromReq(const MediaPlaybackRequest& req, qint64 end_nanosec);
bool InitFromString(const QString& pipeline);
// BufferConsumers get fed audio data. Thread-safe.
@ -80,12 +81,12 @@ class GstEnginePipeline : public QObject {
// If this is set then it will be loaded automatically when playback finishes
// for gapless playback
void SetNextUrl(const QUrl& url, qint64 beginning_nanosec,
void SetNextReq(const MediaPlaybackRequest& req, qint64 beginning_nanosec,
qint64 end_nanosec);
bool has_next_valid_url() const { return next_url_.isValid(); }
bool has_next_valid_url() const { return next_.url_.isValid(); }
// Get information about the music playback
QUrl url() const { return url_; }
QUrl url() const { return current_.url_; }
bool is_valid() const { return valid_; }
// Please note that this method (unlike GstEngine's.position()) is
// multiple-section media unaware.
@ -226,8 +227,8 @@ signals:
// The URL that is currently playing, and the URL that is to be preloaded
// when the current track is close to finishing.
QUrl url_;
QUrl next_url_;
MediaPlaybackRequest current_;
MediaPlaybackRequest next_;
// If this is > 0 then the pipeline will be forced to stop when playback goes
// past this position.

View File

@ -0,0 +1,31 @@
/* This file is part of Clementine.
Copyright 2020, Jim Broadus <jbroadus@gmail.com>
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Clementine is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef ENGINES_PLAYBACKREQUEST_H_
#define ENGINES_PLAYBACKREQUEST_H_
#include <QUrl>
class MediaPlaybackRequest {
public:
MediaPlaybackRequest(const QUrl& url) : url_(url) {}
MediaPlaybackRequest() {}
QUrl url_;
};
#endif // ENGINES_PLAYBACKREQUEST_H_