strawberry-audio-player-win.../src/core/songloader.cpp

794 lines
22 KiB
C++
Raw Normal View History

2018-02-27 18:06:05 +01:00
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
2021-03-20 21:14:47 +01:00
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
2018-02-27 18:06:05 +01:00
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
2018-08-09 18:10:03 +02:00
*
2018-02-27 18:06:05 +01:00
*/
#include "config.h"
2018-10-19 20:18:46 +02:00
#include <algorithm>
2018-02-27 18:06:05 +01:00
#ifdef HAVE_GSTREAMER
# include <gst/gst.h>
2018-02-27 18:06:05 +01:00
#endif
#include <QObject>
#include <QIODevice>
2018-02-27 18:06:05 +01:00
#include <QBuffer>
#include <QByteArray>
#include <QDir>
2018-02-27 18:06:05 +01:00
#include <QDirIterator>
#include <QFile>
2018-02-27 18:06:05 +01:00
#include <QFileInfo>
#include <QSet>
2018-02-27 18:06:05 +01:00
#include <QTimer>
#include <QString>
2018-02-27 18:06:05 +01:00
#include <QUrl>
#include <QEventLoop>
2018-02-27 18:06:05 +01:00
#include "core/logging.h"
#include "shared_ptr.h"
#include "signalchecker.h"
#include "player.h"
#include "song.h"
#include "songloader.h"
#include "tagreaderclient.h"
#include "database.h"
2022-05-13 18:15:04 +02:00
#include "sqlrow.h"
2018-08-09 18:10:03 +02:00
#include "engine/enginebase.h"
2018-02-27 18:06:05 +01:00
#include "collection/collectionbackend.h"
#include "collection/collectionquery.h"
2018-02-27 18:06:05 +01:00
#include "playlistparsers/cueparser.h"
#include "playlistparsers/parserbase.h"
#include "playlistparsers/playlistparser.h"
#if defined(HAVE_AUDIOCD) && defined(HAVE_GSTREAMER)
# include "device/cddasongloader.h"
2018-02-27 18:06:05 +01:00
#endif
QSet<QString> SongLoader::sRawUriSchemes;
const int SongLoader::kDefaultTimeout = 5000;
SongLoader::SongLoader(SharedPtr<CollectionBackendInterface> collection_backend, const SharedPtr<Player> player, QObject *parent)
2021-07-11 07:40:57 +02:00
: QObject(parent),
2023-03-19 05:08:30 +01:00
player_(player),
collection_backend_(collection_backend),
2018-02-27 18:06:05 +01:00
timeout_timer_(new QTimer(this)),
playlist_parser_(new PlaylistParser(collection_backend, this)),
cue_parser_(new CueParser(collection_backend, this)),
2018-02-27 18:06:05 +01:00
parser_(nullptr),
2023-03-19 05:08:30 +01:00
state_(State::WaitingForType),
timeout_(kDefaultTimeout),
#ifdef HAVE_GSTREAMER
fakesink_(nullptr),
buffer_probe_cb_id_(0),
#endif
success_(false) {
2018-08-09 18:10:03 +02:00
2018-02-27 18:06:05 +01:00
if (sRawUriSchemes.isEmpty()) {
2024-04-09 23:20:26 +02:00
sRawUriSchemes << QStringLiteral("udp")
<< QStringLiteral("mms")
<< QStringLiteral("mmsh")
<< QStringLiteral("mmst")
<< QStringLiteral("mmsu")
<< QStringLiteral("rtsp")
<< QStringLiteral("rtspu")
<< QStringLiteral("rtspt")
<< QStringLiteral("rtsph");
2018-02-27 18:06:05 +01:00
}
timeout_timer_->setSingleShot(true);
2021-01-26 16:48:04 +01:00
QObject::connect(timeout_timer_, &QTimer::timeout, this, &SongLoader::Timeout);
2018-10-02 00:38:52 +02:00
2018-02-27 18:06:05 +01:00
}
SongLoader::~SongLoader() {
2018-08-09 18:10:03 +02:00
2018-02-27 18:06:05 +01:00
#ifdef HAVE_GSTREAMER
2023-03-19 05:08:30 +01:00
CleanupPipeline();
2018-02-27 18:06:05 +01:00
#endif
}
SongLoader::Result SongLoader::Load(const QUrl &url) {
2023-02-18 14:09:27 +01:00
if (url.isEmpty()) return Result::Error;
2018-02-27 18:06:05 +01:00
url_ = url;
2019-07-09 21:43:56 +02:00
if (url_.isLocalFile()) {
2018-02-27 18:06:05 +01:00
return LoadLocal(url_.toLocalFile());
}
if (sRawUriSchemes.contains(url_.scheme()) || player_->HandlerForUrl(url)) {
// The URI scheme indicates that it can't possibly be a playlist,
// or we have a custom handler for the URL, so add it as a raw stream.
AddAsRawStream();
2023-02-18 14:09:27 +01:00
return Result::Success;
2018-02-27 18:06:05 +01:00
}
2023-04-22 19:13:42 +02:00
if (player_->engine()->type() == EngineBase::Type::GStreamer) {
2018-02-27 18:06:05 +01:00
#ifdef HAVE_GSTREAMER
2018-08-09 18:10:03 +02:00
preload_func_ = std::bind(&SongLoader::LoadRemote, this);
2023-02-18 14:09:27 +01:00
return Result::BlockingLoadRequired;
2018-08-09 18:10:03 +02:00
#else
2019-04-20 15:28:16 +02:00
errors_ << tr("You need GStreamer for this URL.");
2023-03-19 05:08:30 +01:00
return Result::Error;
2018-02-27 18:06:05 +01:00
#endif
2018-08-09 18:10:03 +02:00
}
2019-04-20 15:28:16 +02:00
else {
errors_ << tr("You need GStreamer for this URL.");
2023-02-18 14:09:27 +01:00
return Result::Error;
2019-04-20 15:28:16 +02:00
}
2018-02-27 18:06:05 +01:00
2023-02-18 14:09:27 +01:00
return Result::Success;
2018-03-10 13:02:56 +01:00
2018-02-27 18:06:05 +01:00
}
2019-04-20 15:28:16 +02:00
SongLoader::Result SongLoader::LoadFilenamesBlocking() {
2018-08-09 18:10:03 +02:00
2018-02-27 18:06:05 +01:00
if (preload_func_) {
2019-04-20 15:28:16 +02:00
return preload_func_();
}
else {
errors_ << tr("Preload function was not set for blocking operation.");
2023-02-18 14:09:27 +01:00
return Result::Error;
2018-02-27 18:06:05 +01:00
}
2018-08-09 18:10:03 +02:00
2018-02-27 18:06:05 +01:00
}
SongLoader::Result SongLoader::LoadLocalPartial(const QString &filename) {
2018-08-09 18:10:03 +02:00
2018-02-27 18:06:05 +01:00
qLog(Debug) << "Fast Loading local file" << filename;
QFileInfo fileinfo(filename);
if (!fileinfo.exists()) {
errors_ << tr("File %1 does not exist.").arg(filename);
2023-02-18 14:09:27 +01:00
return Result::Error;
}
2018-03-10 13:02:56 +01:00
// First check to see if it's a directory - if so we can load all the songs inside right away.
if (fileinfo.isDir()) {
2018-02-27 18:06:05 +01:00
LoadLocalDirectory(filename);
2023-02-18 14:09:27 +01:00
return Result::Success;
2018-02-27 18:06:05 +01:00
}
// Assume it's just a normal file
if (TagReaderClient::Instance()->IsMediaFileBlocking(filename) || Song::kAcceptedExtensions.contains(fileinfo.suffix(), Qt::CaseInsensitive)) {
2023-02-18 14:09:27 +01:00
Song song(Song::Source::LocalFile);
song.InitFromFilePartial(filename, fileinfo);
if (song.is_valid()) {
songs_ << song;
2023-02-18 14:09:27 +01:00
return Result::Success;
}
2019-04-20 15:28:16 +02:00
}
2018-08-09 18:10:03 +02:00
errors_ << QObject::tr("File %1 is not recognized as a valid audio file.").arg(filename);
2023-02-18 14:09:27 +01:00
return Result::Error;
2018-02-27 18:06:05 +01:00
}
SongLoader::Result SongLoader::LoadAudioCD() {
#if defined(HAVE_AUDIOCD) && defined(HAVE_GSTREAMER)
2023-04-22 19:13:42 +02:00
if (player_->engine()->type() == EngineBase::Type::GStreamer) {
CddaSongLoader *cdda_song_loader = new CddaSongLoader(QUrl(), this);
2021-01-26 16:48:04 +01:00
QObject::connect(cdda_song_loader, &CddaSongLoader::SongsDurationLoaded, this, &SongLoader::AudioCDTracksLoadFinishedSlot);
QObject::connect(cdda_song_loader, &CddaSongLoader::SongsMetadataLoaded, this, &SongLoader::AudioCDTracksTagsLoaded);
2019-04-20 15:28:16 +02:00
cdda_song_loader->LoadSongs();
2023-02-18 14:09:27 +01:00
return Result::Success;
2019-04-20 15:28:16 +02:00
}
else {
#endif
errors_ << tr("CD playback is only available with the GStreamer engine.");
2023-02-18 14:09:27 +01:00
return Result::Error;
2019-04-20 15:28:16 +02:00
#if defined(HAVE_AUDIOCD) && defined(HAVE_GSTREAMER)
}
2018-02-27 18:06:05 +01:00
#endif
}
#if defined(HAVE_AUDIOCD) && defined(HAVE_GSTREAMER)
void SongLoader::AudioCDTracksLoadFinishedSlot(const SongList &songs, const QString &error) {
2018-02-27 18:06:05 +01:00
songs_ = songs;
errors_ << error;
emit AudioCDTracksLoadFinished();
2018-02-27 18:06:05 +01:00
}
void SongLoader::AudioCDTracksTagsLoaded(const SongList &songs) {
2018-02-27 18:06:05 +01:00
CddaSongLoader *cdda_song_loader = qobject_cast<CddaSongLoader*>(sender());
cdda_song_loader->deleteLater();
songs_ = songs;
emit LoadAudioCDFinished(true);
2018-02-27 18:06:05 +01:00
}
#endif
SongLoader::Result SongLoader::LoadLocal(const QString &filename) {
qLog(Debug) << "Loading local file" << filename;
// Search in the database.
QUrl url = QUrl::fromLocalFile(filename);
QMutexLocker l(collection_backend_->db()->Mutex());
QSqlDatabase db(collection_backend_->db()->Connect());
CollectionQuery query(db, collection_backend_->songs_table(), collection_backend_->fts_table());
query.SetColumnSpec(QStringLiteral("%songs_table.ROWID, ") + Song::kColumnSpec);
2024-04-09 23:20:26 +02:00
query.AddWhere(QStringLiteral("url"), url.toEncoded());
2018-02-27 18:06:05 +01:00
if (query.Exec() && query.Next()) {
// We may have many results when the file has many sections
2018-02-27 18:06:05 +01:00
do {
2023-02-18 14:09:27 +01:00
Song song(Song::Source::Collection);
2018-02-27 18:06:05 +01:00
song.InitFromQuery(query, true);
if (song.is_valid()) {
songs_ << song;
}
2021-07-11 09:49:38 +02:00
} while (query.Next());
2018-02-27 18:06:05 +01:00
2023-02-18 14:09:27 +01:00
return Result::Success;
2018-02-27 18:06:05 +01:00
}
// It's not in the database, load it asynchronously.
preload_func_ = std::bind(&SongLoader::LoadLocalAsync, this, filename);
2023-02-18 14:09:27 +01:00
return Result::BlockingLoadRequired;
2018-08-09 18:10:03 +02:00
2018-02-27 18:06:05 +01:00
}
2019-04-20 15:28:16 +02:00
SongLoader::Result SongLoader::LoadLocalAsync(const QString &filename) {
2018-02-27 18:06:05 +01:00
QFileInfo fileinfo(filename);
if (!fileinfo.exists()) {
errors_ << tr("File %1 does not exist.").arg(filename);
2023-02-18 14:09:27 +01:00
return Result::Error;
}
2018-03-10 13:02:56 +01:00
// First check to see if it's a directory - if so we will load all the songs inside right away.
if (fileinfo.isDir()) {
2018-02-27 18:06:05 +01:00
LoadLocalDirectory(filename);
2023-02-18 14:09:27 +01:00
return Result::Success;
2018-02-27 18:06:05 +01:00
}
2018-03-10 13:02:56 +01:00
// It's a local file, so check if it looks like a playlist. Read the first few bytes.
2018-02-27 18:06:05 +01:00
QFile file(filename);
2019-04-20 15:28:16 +02:00
if (!file.open(QIODevice::ReadOnly)) {
2021-08-25 02:57:57 +02:00
errors_ << tr("Could not open file %1 for reading: %2").arg(filename, file.errorString());
2023-02-18 14:09:27 +01:00
return Result::Error;
2019-04-20 15:28:16 +02:00
}
2018-02-27 18:06:05 +01:00
QByteArray data(file.read(PlaylistParser::kMagicSize));
2021-07-11 00:45:30 +02:00
file.close();
2018-02-27 18:06:05 +01:00
ParserBase *parser = playlist_parser_->ParserForMagic(data);
if (!parser) {
// Check the file extension as well, maybe the magic failed, or it was a basic M3U file which is just a plain list of filenames.
2023-02-18 14:09:27 +01:00
parser = playlist_parser_->ParserForExtension(PlaylistParser::Type::Load, fileinfo.suffix().toLower());
2018-02-27 18:06:05 +01:00
}
2021-07-11 09:49:38 +02:00
if (parser) { // It's a playlist!
2018-02-27 18:06:05 +01:00
qLog(Debug) << "Parsing using" << parser->name();
LoadPlaylist(parser, filename);
2023-02-18 14:09:27 +01:00
return Result::Success;
2018-02-27 18:06:05 +01:00
}
// Check if it's a CUE file
2021-11-27 20:28:00 +01:00
QString matching_cue = CueParser::FindCueFilename(filename);
2018-02-27 18:06:05 +01:00
if (QFile::exists(matching_cue)) {
// It's a CUE - create virtual tracks
2018-02-27 18:06:05 +01:00
QFile cue(matching_cue);
if (cue.open(QIODevice::ReadOnly)) {
const SongList songs = cue_parser_->Load(&cue, matching_cue, QDir(filename.section(QLatin1Char('/'), 0, -2)));
2021-07-11 00:45:30 +02:00
cue.close();
for (const Song &song : songs) {
if (song.is_valid()) songs_ << song;
}
2023-02-18 14:09:27 +01:00
return Result::Success;
2018-02-27 18:06:05 +01:00
}
else {
2021-08-25 02:57:57 +02:00
errors_ << tr("Could not open CUE file %1 for reading: %2").arg(matching_cue, cue.errorString());
2023-02-18 14:09:27 +01:00
return Result::Error;
}
2018-02-27 18:06:05 +01:00
}
// Assume it's just a normal file
if (TagReaderClient::Instance()->IsMediaFileBlocking(filename) || Song::kAcceptedExtensions.contains(fileinfo.suffix(), Qt::CaseInsensitive)) {
2023-02-18 14:09:27 +01:00
Song song(Song::Source::LocalFile);
song.InitFromFilePartial(filename, fileinfo);
if (song.is_valid()) {
songs_ << song;
2023-02-18 14:09:27 +01:00
return Result::Success;
}
2019-04-20 15:28:16 +02:00
}
2018-08-09 18:10:03 +02:00
errors_ << QObject::tr("File %1 is not recognized as a valid audio file.").arg(filename);
2023-02-18 14:09:27 +01:00
return Result::Error;
2018-02-27 18:06:05 +01:00
}
void SongLoader::LoadMetadataBlocking() {
2021-08-19 18:57:06 +02:00
2018-02-27 18:06:05 +01:00
for (int i = 0; i < songs_.size(); i++) {
EffectiveSongLoad(&songs_[i]);
}
2021-08-19 18:57:06 +02:00
2018-02-27 18:06:05 +01:00
}
void SongLoader::EffectiveSongLoad(Song *song) {
if (!song || !song->url().isLocalFile()) return;
2018-02-27 18:06:05 +01:00
2023-02-18 14:09:27 +01:00
if (song->init_from_file() && song->filetype() != Song::FileType::Unknown) {
2018-02-27 18:06:05 +01:00
// Maybe we loaded the metadata already, for example from a cuesheet.
return;
}
// First, try to get the song from the collection
Song collection_song = collection_backend_->GetSongByUrl(song->url());
2018-02-27 18:06:05 +01:00
if (collection_song.is_valid()) {
*song = collection_song;
2018-08-09 18:10:03 +02:00
}
else {
2021-08-19 18:57:06 +02:00
// It's a normal media file
2018-02-27 18:06:05 +01:00
QString filename = song->url().toLocalFile();
TagReaderClient::Instance()->ReadFileBlocking(filename, song);
}
2018-03-10 13:02:56 +01:00
2018-02-27 18:06:05 +01:00
}
void SongLoader::LoadPlaylist(ParserBase *parser, const QString &filename) {
2018-02-27 18:06:05 +01:00
QFile file(filename);
if (file.open(QIODevice::ReadOnly)) {
songs_ = parser->Load(&file, filename, QFileInfo(filename).path());
file.close();
}
else {
2021-08-25 02:57:57 +02:00
errors_ << tr("Could not open playlist file %1 for reading: %2").arg(filename, file.errorString());
}
2018-02-27 18:06:05 +01:00
}
static bool CompareSongs(const Song &left, const Song &right) {
// Order by artist, album, disc, track
if (left.artist() < right.artist()) return true;
if (left.artist() > right.artist()) return false;
if (left.album() < right.album()) return true;
if (left.album() > right.album()) return false;
if (left.disc() < right.disc()) return true;
if (left.disc() > right.disc()) return false;
if (left.track() < right.track()) return true;
if (left.track() > right.track()) return false;
return left.url() < right.url();
2018-03-10 13:02:56 +01:00
2018-02-27 18:06:05 +01:00
}
void SongLoader::LoadLocalDirectory(const QString &filename) {
QDirIterator it(filename, QDir::Files | QDir::NoDotAndDotDot | QDir::Readable, QDirIterator::Subdirectories);
while (it.hasNext()) {
LoadLocalPartial(it.next());
}
2018-10-19 20:18:46 +02:00
std::stable_sort(songs_.begin(), songs_.end(), CompareSongs);
2018-02-27 18:06:05 +01:00
// Load the first song:
// all songs will be loaded async, but we want the first one in our list to be fully loaded,
// so if the user has the "Start playing when adding to playlist" preference behaviour set,
// it can enjoy the first song being played (seek it, have moodbar, etc.)
2018-02-27 18:06:05 +01:00
if (!songs_.isEmpty()) EffectiveSongLoad(&(*songs_.begin()));
2018-08-09 18:10:03 +02:00
}
2018-02-27 18:06:05 +01:00
2018-08-09 18:10:03 +02:00
void SongLoader::AddAsRawStream() {
Song song(Song::SourceFromURL(url_));
2018-08-09 18:10:03 +02:00
song.set_valid(true);
2023-02-18 14:09:27 +01:00
song.set_filetype(Song::FileType::Stream);
2018-08-09 18:10:03 +02:00
song.set_url(url_);
song.set_title(url_.toString());
songs_ << song;
2018-03-10 13:02:56 +01:00
}
2018-02-27 18:06:05 +01:00
void SongLoader::Timeout() {
2023-03-19 05:08:30 +01:00
2023-02-18 14:09:27 +01:00
state_ = State::Finished;
2018-02-27 18:06:05 +01:00
success_ = false;
StopTypefind();
2023-03-19 05:08:30 +01:00
2018-02-27 18:06:05 +01:00
}
void SongLoader::StopTypefind() {
#ifdef HAVE_GSTREAMER
// Destroy the pipeline
if (pipeline_) {
gst_element_set_state(&*pipeline_, GST_STATE_NULL);
2023-03-19 05:08:30 +01:00
CleanupPipeline();
2018-02-27 18:06:05 +01:00
}
#endif
timeout_timer_->stop();
if (success_ && parser_) {
qLog(Debug) << "Parsing" << url_ << "with" << parser_->name();
// Parse the playlist
QBuffer buf(&buffer_);
if (buf.open(QIODevice::ReadOnly)) {
songs_ = parser_->Load(&buf);
buf.close();
}
2018-02-27 18:06:05 +01:00
}
else if (success_) {
2018-08-09 18:10:03 +02:00
qLog(Debug) << "Loading" << url_ << "as raw stream";
2018-02-27 18:06:05 +01:00
// It wasn't a playlist - just put the URL in as a stream
2018-08-09 18:10:03 +02:00
AddAsRawStream();
2018-02-27 18:06:05 +01:00
}
emit LoadRemoteFinished();
2018-03-10 13:02:56 +01:00
2018-02-27 18:06:05 +01:00
}
#ifdef HAVE_GSTREAMER
2019-04-20 15:28:16 +02:00
SongLoader::Result SongLoader::LoadRemote() {
2018-02-27 18:06:05 +01:00
qLog(Debug) << "Loading remote file" << url_;
// It's not a local file so we have to fetch it to see what it is.
// We use gstreamer to do this since it handles funky URLs for us (http://, ssh://, etc) and also has typefinder plugins.
// First we wait for typefinder to tell us what it is. If it's not text/plain or text/uri-list assume it's a song and return success.
// Otherwise wait to get 512 bytes of data and do magic on it - if the magic fails then we don't know what it is so return failure.
// If the magic succeeds then we know for sure it's a playlist - so read the rest of the file, parse the playlist and return success.
2018-02-27 18:06:05 +01:00
ScheduleTimeoutAsync();
2018-02-27 18:06:05 +01:00
// Create the pipeline - it gets unreffed if it goes out of scope
SharedPtr<GstElement> pipeline(gst_pipeline_new(nullptr), std::bind(&gst_object_unref, std::placeholders::_1));
2018-02-27 18:06:05 +01:00
// Create the source element automatically based on the URL
GstElement *source = gst_element_make_from_uri(GST_URI_SRC, url_.toEncoded().constData(), nullptr, nullptr);
if (!source) {
2021-07-11 05:17:45 +02:00
errors_ << tr("Couldn't create GStreamer source element for %1").arg(url_.toString());
2023-02-18 14:09:27 +01:00
return Result::Error;
2018-02-27 18:06:05 +01:00
}
gst_bin_add(GST_BIN(&*pipeline), source);
2023-03-19 05:08:30 +01:00
2020-11-03 18:48:35 +01:00
g_object_set(source, "ssl-strict", FALSE, nullptr);
2018-02-27 18:06:05 +01:00
// Create the other elements and link them up
GstElement *typefind = gst_element_factory_make("typefind", nullptr);
2023-03-19 05:08:30 +01:00
if (!typefind) {
errors_ << tr("Couldn't create GStreamer typefind element for %1").arg(url_.toString());
return Result::Error;
}
gst_bin_add(GST_BIN(&*pipeline), typefind);
2023-03-19 05:08:30 +01:00
fakesink_ = gst_element_factory_make("fakesink", nullptr);
if (!fakesink_) {
errors_ << tr("Couldn't create GStreamer fakesink element for %1").arg(url_.toString());
return Result::Error;
}
gst_bin_add(GST_BIN(&*pipeline), fakesink_);
2018-02-27 18:06:05 +01:00
2023-03-19 05:08:30 +01:00
if (!gst_element_link_many(source, typefind, fakesink_, nullptr)) {
errors_ << tr("Couldn't link GStreamer source, typefind and fakesink elements for %1").arg(url_.toString());
return Result::Error;
}
2018-02-27 18:06:05 +01:00
// Connect callbacks
CHECKED_GCONNECT(typefind, "have-type", &TypeFound, this);
GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(&*pipeline));
2023-03-19 05:08:30 +01:00
if (bus) {
gst_bus_set_sync_handler(bus, BusCallbackSync, this, nullptr);
gst_bus_add_watch(bus, BusWatchCallback, this);
gst_object_unref(bus);
}
2018-02-27 18:06:05 +01:00
// Add a probe to the sink so we can capture the data if it's a playlist
2023-03-19 05:08:30 +01:00
GstPad *pad = gst_element_get_static_pad(fakesink_, "sink");
if (pad) {
buffer_probe_cb_id_ = gst_pad_add_probe(pad, GST_PAD_PROBE_TYPE_BUFFER, &DataReady, this, nullptr);
gst_object_unref(pad);
}
2018-02-27 18:06:05 +01:00
QEventLoop loop;
2021-01-26 16:48:04 +01:00
loop.connect(this, &SongLoader::LoadRemoteFinished, &loop, &QEventLoop::quit);
2018-02-27 18:06:05 +01:00
// Start "playing"
pipeline_ = pipeline;
gst_element_set_state(&*pipeline, GST_STATE_PLAYING);
2018-02-27 18:06:05 +01:00
// Wait until loading is finished
loop.exec();
2019-04-20 15:28:16 +02:00
2023-02-18 14:09:27 +01:00
return Result::Success;
2019-04-20 15:28:16 +02:00
2018-02-27 18:06:05 +01:00
}
#endif
#ifdef HAVE_GSTREAMER
2021-06-13 20:55:37 +02:00
void SongLoader::TypeFound(GstElement*, uint, GstCaps *caps, void *self) {
2018-08-09 18:10:03 +02:00
2018-02-27 18:06:05 +01:00
SongLoader *instance = static_cast<SongLoader*>(self);
2023-02-18 14:09:27 +01:00
if (instance->state_ != State::WaitingForType) return;
2018-02-27 18:06:05 +01:00
// Check the mimetype
instance->mime_type_ = QString::fromUtf8(gst_structure_get_name(gst_caps_get_structure(caps, 0)));
2018-02-27 18:06:05 +01:00
qLog(Debug) << "Mime type is" << instance->mime_type_;
if (instance->mime_type_ == QStringLiteral("text/plain") || instance->mime_type_ == QStringLiteral("text/uri-list")) {
2018-02-27 18:06:05 +01:00
// Yeah it might be a playlist, let's get some data and have a better look
2023-02-18 14:09:27 +01:00
instance->state_ = State::WaitingForMagic;
2018-02-27 18:06:05 +01:00
return;
}
// Nope, not a playlist - we're done
instance->StopTypefindAsync(true);
}
#endif
#ifdef HAVE_GSTREAMER
GstPadProbeReturn SongLoader::DataReady(GstPad*, GstPadProbeInfo *info, gpointer self) {
SongLoader *instance = reinterpret_cast<SongLoader*>(self);
2023-02-18 14:09:27 +01:00
if (instance->state_ == State::Finished) {
2018-02-27 18:06:05 +01:00
return GST_PAD_PROBE_OK;
2021-08-23 21:21:08 +02:00
}
2018-02-27 18:06:05 +01:00
GstBuffer *buffer = gst_pad_probe_info_get_buffer(info);
GstMapInfo map;
gst_buffer_map(buffer, &map, GST_MAP_READ);
// Append the data to the buffer
2021-10-30 02:21:29 +02:00
instance->buffer_.append(reinterpret_cast<const char*>(map.data), static_cast<qint64>(map.size));
2018-02-27 18:06:05 +01:00
qLog(Debug) << "Received total" << instance->buffer_.size() << "bytes";
gst_buffer_unmap(buffer, &map);
2023-02-18 14:09:27 +01:00
if (instance->state_ == State::WaitingForMagic && (instance->buffer_.size() >= PlaylistParser::kMagicSize || !instance->IsPipelinePlaying())) {
2018-02-27 18:06:05 +01:00
// Got enough that we can test the magic
instance->MagicReady();
}
return GST_PAD_PROBE_OK;
}
#endif
#ifdef HAVE_GSTREAMER
2023-03-19 05:08:30 +01:00
gboolean SongLoader::BusWatchCallback(GstBus*, GstMessage *msg, gpointer self) {
2018-09-08 12:38:02 +02:00
2018-02-27 18:06:05 +01:00
SongLoader *instance = reinterpret_cast<SongLoader*>(self);
switch (GST_MESSAGE_TYPE(msg)) {
case GST_MESSAGE_ERROR:
instance->ErrorMessageReceived(msg);
break;
default:
break;
}
return TRUE;
}
#endif
#ifdef HAVE_GSTREAMER
2021-06-13 20:55:37 +02:00
GstBusSyncReply SongLoader::BusCallbackSync(GstBus*, GstMessage *msg, gpointer self) {
2018-02-27 18:06:05 +01:00
SongLoader *instance = reinterpret_cast<SongLoader*>(self);
switch (GST_MESSAGE_TYPE(msg)) {
case GST_MESSAGE_EOS:
instance->EndOfStreamReached();
break;
case GST_MESSAGE_ERROR:
instance->ErrorMessageReceived(msg);
break;
default:
break;
}
return GST_BUS_PASS;
}
#endif
#ifdef HAVE_GSTREAMER
void SongLoader::ErrorMessageReceived(GstMessage *msg) {
2018-10-02 00:38:52 +02:00
2023-02-18 14:09:27 +01:00
if (state_ == State::Finished) return;
2018-02-27 18:06:05 +01:00
2021-03-26 21:30:13 +01:00
GError *error = nullptr;
gchar *debugs = nullptr;
2018-02-27 18:06:05 +01:00
gst_message_parse_error(msg, &error, &debugs);
2021-08-24 17:52:08 +02:00
qLog(Error) << error->message;
qLog(Error) << debugs;
2018-02-27 18:06:05 +01:00
QString message_str = QString::fromUtf8(error->message);
2018-02-27 18:06:05 +01:00
g_error_free(error);
2022-02-06 18:46:59 +01:00
g_free(debugs);
2018-02-27 18:06:05 +01:00
if (state_ == State::WaitingForType && message_str == QString::fromUtf8(gst_error_get_message(GST_STREAM_ERROR, GST_STREAM_ERROR_TYPE_NOT_FOUND))) {
2018-03-10 13:02:56 +01:00
// Don't give up - assume it's a playlist and see if one of our parsers can read it.
2023-02-18 14:09:27 +01:00
state_ = State::WaitingForMagic;
2018-02-27 18:06:05 +01:00
return;
}
StopTypefindAsync(false);
}
#endif
#ifdef HAVE_GSTREAMER
void SongLoader::EndOfStreamReached() {
2023-02-18 14:09:27 +01:00
qLog(Debug) << Q_FUNC_INFO << static_cast<int>(state_);
2023-03-19 05:08:30 +01:00
2018-02-27 18:06:05 +01:00
switch (state_) {
2023-02-18 14:09:27 +01:00
case State::Finished:
2018-02-27 18:06:05 +01:00
break;
2023-02-18 14:09:27 +01:00
case State::WaitingForMagic:
2018-02-27 18:06:05 +01:00
// Do the magic on the data we have already
MagicReady();
2023-02-18 14:09:27 +01:00
if (state_ == State::Finished) break;
2018-02-27 18:06:05 +01:00
// It looks like a playlist, so parse it
2022-07-26 20:37:06 +02:00
[[fallthrough]];
2023-02-18 14:09:27 +01:00
case State::WaitingForData:
2018-02-27 18:06:05 +01:00
// It's a playlist and we've got all the data - finish and parse it
StopTypefindAsync(true);
break;
2023-02-18 14:09:27 +01:00
case State::WaitingForType:
2018-02-27 18:06:05 +01:00
StopTypefindAsync(false);
break;
}
}
#endif
#ifdef HAVE_GSTREAMER
void SongLoader::MagicReady() {
qLog(Debug) << Q_FUNC_INFO;
2023-03-19 05:08:30 +01:00
2018-02-27 18:06:05 +01:00
parser_ = playlist_parser_->ParserForMagic(buffer_, mime_type_);
if (!parser_) {
2018-03-10 13:02:56 +01:00
qLog(Warning) << url_.toString() << "is text, but not a recognised playlist";
// It doesn't look like a playlist, so just finish
StopTypefindAsync(false);
return;
}
2018-02-27 18:06:05 +01:00
// We'll get more data and parse the whole thing in EndOfStreamReached
2018-03-10 13:02:56 +01:00
qLog(Debug) << "Magic says" << parser_->name();
2023-03-19 05:08:30 +01:00
if (parser_->name() == QStringLiteral("ASX/INI") && url_.scheme() == QStringLiteral("http")) {
2018-03-10 13:02:56 +01:00
// This is actually a weird MS-WMSP stream. Changing the protocol to MMS from HTTP makes it playable.
parser_ = nullptr;
2024-04-09 23:20:26 +02:00
url_.setScheme(QStringLiteral("mms"));
2018-03-10 13:02:56 +01:00
StopTypefindAsync(true);
}
2018-02-27 18:06:05 +01:00
2023-02-18 14:09:27 +01:00
state_ = State::WaitingForData;
2018-02-27 18:06:05 +01:00
if (!IsPipelinePlaying()) {
EndOfStreamReached();
}
}
#endif
#ifdef HAVE_GSTREAMER
bool SongLoader::IsPipelinePlaying() {
GstState state = GST_STATE_NULL;
GstState pending_state = GST_STATE_NULL;
GstStateChangeReturn ret = gst_element_get_state(&*pipeline_, &state, &pending_state, GST_SECOND);
2018-02-27 18:06:05 +01:00
if (ret == GST_STATE_CHANGE_ASYNC && pending_state == GST_STATE_PLAYING) {
// We're still on the way to playing
return true;
}
return state == GST_STATE_PLAYING;
}
#endif
#ifdef HAVE_GSTREAMER
2023-03-19 05:08:30 +01:00
void SongLoader::StopTypefindAsync(const bool success) {
2018-02-27 18:06:05 +01:00
2023-02-18 14:09:27 +01:00
state_ = State::Finished;
2018-02-27 18:06:05 +01:00
success_ = success;
QMetaObject::invokeMethod(this, &SongLoader::StopTypefind, Qt::QueuedConnection);
2018-02-27 18:06:05 +01:00
}
#endif
void SongLoader::ScheduleTimeoutAsync() {
if (QThread::currentThread() == thread()) {
ScheduleTimeout();
}
else {
QMetaObject::invokeMethod(this, &SongLoader::ScheduleTimeout, Qt::QueuedConnection);
}
}
void SongLoader::ScheduleTimeout() {
timeout_timer_->start(timeout_);
}
2023-03-19 05:08:30 +01:00
#ifdef HAVE_GSTREAMER
void SongLoader::CleanupPipeline() {
if (pipeline_) {
gst_element_set_state(&*pipeline_, GST_STATE_NULL);
2023-03-19 05:08:30 +01:00
if (fakesink_ && buffer_probe_cb_id_ != 0) {
GstPad *pad = gst_element_get_static_pad(fakesink_, "src");
if (pad) {
gst_pad_remove_probe(pad, buffer_probe_cb_id_);
gst_object_unref(pad);
}
}
{
GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(&*pipeline_));
2023-03-19 05:08:30 +01:00
if (bus) {
gst_bus_remove_watch(bus);
gst_bus_set_sync_handler(bus, nullptr, nullptr, nullptr);
gst_object_unref(bus);
}
}
pipeline_.reset();
}
state_ = State::Finished;
}
#endif