summaryrefslogtreecommitdiffstats
path: root/examples/multimedia/player/qplaylistfileparser.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'examples/multimedia/player/qplaylistfileparser.cpp')
-rw-r--r--examples/multimedia/player/qplaylistfileparser.cpp621
1 files changed, 621 insertions, 0 deletions
diff --git a/examples/multimedia/player/qplaylistfileparser.cpp b/examples/multimedia/player/qplaylistfileparser.cpp
new file mode 100644
index 000000000..3f5104761
--- /dev/null
+++ b/examples/multimedia/player/qplaylistfileparser.cpp
@@ -0,0 +1,621 @@
+// Copyright (C) 2016 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#include "qmediametadata.h"
+#include "qmediaplayer.h"
+#include "qplaylistfileparser.h"
+
+#include <QDebug>
+#include <QFileInfo>
+#include <QIODevice>
+#include <QNetworkReply>
+#include <QNetworkRequest>
+#include <QPointer>
+
+QT_BEGIN_NAMESPACE
+
+namespace {
+
+class ParserBase
+{
+public:
+ explicit ParserBase(QPlaylistFileParser *parent) : m_parent(parent), m_aborted(false)
+ {
+ Q_ASSERT(m_parent);
+ }
+
+ bool parseLine(int lineIndex, const QString &line, const QUrl &root)
+ {
+ if (m_aborted)
+ return false;
+
+ const bool ok = parseLineImpl(lineIndex, line, root);
+ return ok && !m_aborted;
+ }
+
+ virtual void abort() { m_aborted = true; }
+ virtual ~ParserBase() = default;
+
+protected:
+ virtual bool parseLineImpl(int lineIndex, const QString &line, const QUrl &root) = 0;
+
+ static QUrl expandToFullPath(const QUrl &root, const QString &line)
+ {
+ // On Linux, backslashes are not converted to forward slashes :/
+ if (line.startsWith(QLatin1String("//")) || line.startsWith(QLatin1String("\\\\"))) {
+ // Network share paths are not resolved
+ return QUrl::fromLocalFile(line);
+ }
+
+ QUrl url(line);
+ if (url.scheme().isEmpty()) {
+ // Resolve it relative to root
+ if (root.isLocalFile())
+ return QUrl::fromUserInput(line, root.adjusted(QUrl::RemoveFilename).toLocalFile(),
+ QUrl::AssumeLocalFile);
+ return root.resolved(url);
+ }
+ if (url.scheme().length() == 1)
+ // Assume it's a drive letter for a Windows path
+ url = QUrl::fromLocalFile(line);
+
+ return url;
+ }
+
+ void newItemFound(const QVariant &content) { Q_EMIT m_parent->newItem(content); }
+
+ QPlaylistFileParser *m_parent;
+ bool m_aborted;
+};
+
+class M3UParser : public ParserBase
+{
+public:
+ explicit M3UParser(QPlaylistFileParser *q) : ParserBase(q), m_extendedFormat(false) { }
+
+ /*
+ *
+ Extended M3U directives
+
+ #EXTM3U - header - must be first line of file
+ #EXTINF - extra info - length (seconds), title
+ #EXTINF - extra info - length (seconds), artist '-' title
+
+ Example
+
+ #EXTM3U
+ #EXTINF:123, Sample artist - Sample title
+ C:\Documents and Settings\I\My Music\Sample.mp3
+ #EXTINF:321,Example Artist - Example title
+ C:\Documents and Settings\I\My Music\Greatest Hits\Example.ogg
+
+ */
+ bool parseLineImpl(int lineIndex, const QString &line, const QUrl &root) override
+ {
+ if (line[0] == u'#') {
+ if (m_extendedFormat) {
+ if (line.startsWith(QLatin1String("#EXTINF:"))) {
+ m_extraInfo.clear();
+ int artistStart = line.indexOf(QLatin1String(","), 8);
+ bool ok = false;
+ QStringView lineView{ line };
+ int length = lineView.mid(8, artistStart < 8 ? -1 : artistStart - 8)
+ .trimmed()
+ .toInt(&ok);
+ if (ok && length > 0) {
+ // convert from second to milisecond
+ m_extraInfo[QMediaMetaData::Duration] = QVariant(length * 1000);
+ }
+ if (artistStart > 0) {
+ int titleStart = getSplitIndex(line, artistStart);
+ if (titleStart > artistStart) {
+ m_extraInfo[QMediaMetaData::Author] =
+ lineView.mid(artistStart + 1, titleStart - artistStart - 1)
+ .trimmed()
+ .toString()
+ .replace(QLatin1String("--"), QLatin1String("-"));
+ m_extraInfo[QMediaMetaData::Title] =
+ lineView.mid(titleStart + 1)
+ .trimmed()
+ .toString()
+ .replace(QLatin1String("--"), QLatin1String("-"));
+ } else {
+ m_extraInfo[QMediaMetaData::Title] =
+ lineView.mid(artistStart + 1)
+ .trimmed()
+ .toString()
+ .replace(QLatin1String("--"), QLatin1String("-"));
+ }
+ }
+ }
+ } else if (lineIndex == 0 && line.startsWith(QLatin1String("#EXTM3U"))) {
+ m_extendedFormat = true;
+ }
+ } else {
+ QUrl url = expandToFullPath(root, line);
+ m_extraInfo[QMediaMetaData::Url] = url;
+ m_parent->playlist.append(url);
+ newItemFound(QVariant::fromValue(m_extraInfo));
+ m_extraInfo.clear();
+ }
+
+ return true;
+ }
+
+ int getSplitIndex(const QString &line, int startPos)
+ {
+ if (startPos < 0)
+ startPos = 0;
+ const QChar *buf = line.data();
+ for (int i = startPos; i < line.length(); ++i) {
+ if (buf[i] == u'-') {
+ if (i == line.length() - 1)
+ return i;
+ ++i;
+ if (buf[i] != u'-')
+ return i - 1;
+ }
+ }
+ return -1;
+ }
+
+private:
+ QMediaMetaData m_extraInfo;
+ bool m_extendedFormat;
+};
+
+class PLSParser : public ParserBase
+{
+public:
+ explicit PLSParser(QPlaylistFileParser *q) : ParserBase(q) { }
+
+ /*
+ *
+ The format is essentially that of an INI file structured as follows:
+
+ Header
+
+ * [playlist] : This tag indicates that it is a Playlist File
+
+ Track Entry
+ Assuming track entry #X
+
+ * FileX : Variable defining location of stream.
+ * TitleX : Defines track title.
+ * LengthX : Length in seconds of track. Value of -1 indicates indefinite.
+
+ Footer
+
+ * NumberOfEntries : This variable indicates the number of tracks.
+ * Version : Playlist version. Currently only a value of 2 is valid.
+
+ [playlist]
+
+ File1=Alternative\everclear - SMFTA.mp3
+
+ Title1=Everclear - So Much For The Afterglow
+
+ Length1=233
+
+ File2=http://www.site.com:8000/listen.pls
+
+ Title2=My Cool Stream
+
+ Length5=-1
+
+ NumberOfEntries=2
+
+ Version=2
+ */
+ bool parseLineImpl(int, const QString &line, const QUrl &root) override
+ {
+ // We ignore everything but 'File' entries, since that's the only thing we care about.
+ if (!line.startsWith(QLatin1String("File")))
+ return true;
+
+ QString value = getValue(line);
+ if (value.isEmpty())
+ return true;
+
+ QUrl path = expandToFullPath(root, value);
+ m_parent->playlist.append(path);
+ newItemFound(path);
+
+ return true;
+ }
+
+ QString getValue(QStringView line)
+ {
+ int start = line.indexOf(u'=');
+ if (start < 0)
+ return QString();
+ return line.mid(start + 1).trimmed().toString();
+ }
+};
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////////////
+
+class QPlaylistFileParserPrivate
+{
+ Q_DECLARE_PUBLIC(QPlaylistFileParser)
+public:
+ QPlaylistFileParserPrivate(QPlaylistFileParser *q)
+ : q_ptr(q),
+ m_stream(nullptr),
+ m_type(QPlaylistFileParser::UNKNOWN),
+ m_scanIndex(0),
+ m_lineIndex(-1),
+ m_utf8(false),
+ m_aborted(false)
+ {
+ }
+
+ void handleData();
+ void handleParserFinished();
+ void abort();
+ void reset();
+
+ QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> m_source;
+ QScopedPointer<ParserBase> m_currentParser;
+ QByteArray m_buffer;
+ QUrl m_root;
+ QNetworkAccessManager m_mgr;
+ QString m_mimeType;
+ QPlaylistFileParser *q_ptr;
+ QPointer<QIODevice> m_stream;
+ QPlaylistFileParser::FileType m_type;
+ struct ParserJob
+ {
+ QIODevice *m_stream;
+ QUrl m_media;
+ QString m_mimeType;
+ [[nodiscard]] bool isValid() const { return m_stream || !m_media.isEmpty(); }
+ void reset()
+ {
+ m_stream = nullptr;
+ m_media = QUrl();
+ m_mimeType = QString();
+ }
+ } m_pendingJob;
+ int m_scanIndex;
+ int m_lineIndex;
+ bool m_utf8;
+ bool m_aborted;
+
+private:
+ bool processLine(int startIndex, int length);
+};
+
+#define LINE_LIMIT 4096
+#define READ_LIMIT 64
+
+bool QPlaylistFileParserPrivate::processLine(int startIndex, int length)
+{
+ Q_Q(QPlaylistFileParser);
+ m_lineIndex++;
+
+ if (!m_currentParser) {
+ const QString urlString = m_root.toString();
+ const QString &suffix = !urlString.isEmpty() ? QFileInfo(urlString).suffix() : urlString;
+ QString mimeType;
+ if (m_source)
+ mimeType = m_source->header(QNetworkRequest::ContentTypeHeader).toString();
+ m_type = QPlaylistFileParser::findPlaylistType(
+ suffix, !mimeType.isEmpty() ? mimeType : m_mimeType, m_buffer.constData(),
+ quint32(m_buffer.size()));
+
+ switch (m_type) {
+ case QPlaylistFileParser::UNKNOWN:
+ emit q->error(QMediaPlaylist::FormatError,
+ QMediaPlaylist::tr("%1 playlist type is unknown").arg(m_root.toString()));
+ q->abort();
+ return false;
+ case QPlaylistFileParser::M3U:
+ m_currentParser.reset(new M3UParser(q));
+ break;
+ case QPlaylistFileParser::M3U8:
+ m_currentParser.reset(new M3UParser(q));
+ m_utf8 = true;
+ break;
+ case QPlaylistFileParser::PLS:
+ m_currentParser.reset(new PLSParser(q));
+ break;
+ }
+
+ Q_ASSERT(!m_currentParser.isNull());
+ }
+
+ QString line;
+
+ if (m_utf8) {
+ line = QString::fromUtf8(m_buffer.constData() + startIndex, length).trimmed();
+ } else {
+ line = QString::fromLatin1(m_buffer.constData() + startIndex, length).trimmed();
+ }
+ if (line.isEmpty())
+ return true;
+
+ Q_ASSERT(m_currentParser);
+ return m_currentParser->parseLine(m_lineIndex, line, m_root);
+}
+
+void QPlaylistFileParserPrivate::handleData()
+{
+ Q_Q(QPlaylistFileParser);
+ while (m_stream->bytesAvailable() && !m_aborted) {
+ int expectedBytes =
+ qMin(READ_LIMIT,
+ int(qMin(m_stream->bytesAvailable(), qint64(LINE_LIMIT - m_buffer.size()))));
+ m_buffer.push_back(m_stream->read(expectedBytes));
+ int processedBytes = 0;
+ while (m_scanIndex < m_buffer.length() && !m_aborted) {
+ char s = m_buffer[m_scanIndex];
+ if (s == '\r' || s == '\n') {
+ int l = m_scanIndex - processedBytes;
+ if (l > 0) {
+ if (!processLine(processedBytes, l))
+ break;
+ }
+ processedBytes = m_scanIndex + 1;
+ if (!m_stream) {
+ // some error happened, so exit parsing
+ return;
+ }
+ }
+ m_scanIndex++;
+ }
+
+ if (m_aborted)
+ break;
+
+ if (m_buffer.length() - processedBytes >= LINE_LIMIT) {
+ emit q->error(QMediaPlaylist::FormatError,
+ QMediaPlaylist::tr("invalid line in playlist file"));
+ q->abort();
+ break;
+ }
+
+ if (!m_stream->bytesAvailable() && (!m_source || !m_source->isFinished())) {
+ // last line
+ processLine(processedBytes, -1);
+ break;
+ }
+
+ Q_ASSERT(m_buffer.length() == m_scanIndex);
+ if (processedBytes == 0)
+ continue;
+
+ int copyLength = m_buffer.length() - processedBytes;
+ if (copyLength > 0) {
+ Q_ASSERT(copyLength <= READ_LIMIT);
+ m_buffer = m_buffer.right(copyLength);
+ } else {
+ m_buffer.clear();
+ }
+ m_scanIndex = 0;
+ }
+
+ handleParserFinished();
+}
+
+QPlaylistFileParser::QPlaylistFileParser(QObject *parent)
+ : QObject(parent), d_ptr(new QPlaylistFileParserPrivate(this))
+{
+}
+
+QPlaylistFileParser::~QPlaylistFileParser() = default;
+
+QPlaylistFileParser::FileType QPlaylistFileParser::findByMimeType(const QString &mime)
+{
+ if (mime == QLatin1String("text/uri-list") || mime == QLatin1String("audio/x-mpegurl")
+ || mime == QLatin1String("audio/mpegurl"))
+ return QPlaylistFileParser::M3U;
+
+ if (mime == QLatin1String("application/x-mpegURL")
+ || mime == QLatin1String("application/vnd.apple.mpegurl"))
+ return QPlaylistFileParser::M3U8;
+
+ if (mime == QLatin1String("audio/x-scpls"))
+ return QPlaylistFileParser::PLS;
+
+ return QPlaylistFileParser::UNKNOWN;
+}
+
+QPlaylistFileParser::FileType QPlaylistFileParser::findBySuffixType(const QString &suffix)
+{
+ const QString &s = suffix.toLower();
+
+ if (s == QLatin1String("m3u"))
+ return QPlaylistFileParser::M3U;
+
+ if (s == QLatin1String("m3u8"))
+ return QPlaylistFileParser::M3U8;
+
+ if (s == QLatin1String("pls"))
+ return QPlaylistFileParser::PLS;
+
+ return QPlaylistFileParser::UNKNOWN;
+}
+
+QPlaylistFileParser::FileType QPlaylistFileParser::findByDataHeader(const char *data, quint32 size)
+{
+ if (!data || size == 0)
+ return QPlaylistFileParser::UNKNOWN;
+
+ if (size >= 7 && strncmp(data, "#EXTM3U", 7) == 0)
+ return QPlaylistFileParser::M3U;
+
+ if (size >= 10 && strncmp(data, "[playlist]", 10) == 0)
+ return QPlaylistFileParser::PLS;
+
+ return QPlaylistFileParser::UNKNOWN;
+}
+
+QPlaylistFileParser::FileType QPlaylistFileParser::findPlaylistType(const QString &suffix,
+ const QString &mime,
+ const char *data, quint32 size)
+{
+
+ FileType dataHeaderType = findByDataHeader(data, size);
+ if (dataHeaderType != UNKNOWN)
+ return dataHeaderType;
+
+ FileType mimeType = findByMimeType(mime);
+ if (mimeType != UNKNOWN)
+ return mimeType;
+
+ mimeType = findBySuffixType(mime);
+ if (mimeType != UNKNOWN)
+ return mimeType;
+
+ FileType suffixType = findBySuffixType(suffix);
+ if (suffixType != UNKNOWN)
+ return suffixType;
+
+ return UNKNOWN;
+}
+
+/*
+ * Delegating
+ */
+void QPlaylistFileParser::start(const QUrl &media, QIODevice *stream, const QString &mimeType)
+{
+ if (stream)
+ start(stream, mimeType);
+ else
+ start(media, mimeType);
+}
+
+void QPlaylistFileParser::start(QIODevice *stream, const QString &mimeType)
+{
+ Q_D(QPlaylistFileParser);
+ const bool validStream = stream ? (stream->isOpen() && stream->isReadable()) : false;
+
+ if (!validStream) {
+ Q_EMIT error(QMediaPlaylist::AccessDeniedError, QMediaPlaylist::tr("Invalid stream"));
+ return;
+ }
+
+ if (!d->m_currentParser.isNull()) {
+ abort();
+ d->m_pendingJob = { stream, QUrl(), mimeType };
+ return;
+ }
+
+ playlist.clear();
+ d->reset();
+ d->m_mimeType = mimeType;
+ d->m_stream = stream;
+ connect(d->m_stream, SIGNAL(readyRead()), this, SLOT(handleData()));
+ d->handleData();
+}
+
+void QPlaylistFileParser::start(const QUrl &request, const QString &mimeType)
+{
+ Q_D(QPlaylistFileParser);
+ const QUrl &url = request.url();
+
+ if (url.isLocalFile() && !QFile::exists(url.toLocalFile())) {
+ emit error(QMediaPlaylist::AccessDeniedError,
+ QString(QMediaPlaylist::tr("%1 does not exist")).arg(url.toString()));
+ return;
+ }
+
+ if (!d->m_currentParser.isNull()) {
+ abort();
+ d->m_pendingJob = { nullptr, request, mimeType };
+ return;
+ }
+
+ d->reset();
+ d->m_root = url;
+ d->m_mimeType = mimeType;
+ d->m_source.reset(d->m_mgr.get(QNetworkRequest(request)));
+ d->m_stream = d->m_source.get();
+ connect(d->m_source.data(), SIGNAL(readyRead()), this, SLOT(handleData()));
+ connect(d->m_source.data(), SIGNAL(finished()), this, SLOT(handleData()));
+ connect(d->m_source.data(), SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this,
+ SLOT(handleError()));
+
+ if (url.isLocalFile())
+ d->handleData();
+}
+
+void QPlaylistFileParser::abort()
+{
+ Q_D(QPlaylistFileParser);
+ d->abort();
+
+ if (d->m_source)
+ d->m_source->disconnect();
+
+ if (d->m_stream)
+ disconnect(d->m_stream, SIGNAL(readyRead()), this, SLOT(handleData()));
+
+ playlist.clear();
+}
+
+void QPlaylistFileParser::handleData()
+{
+ Q_D(QPlaylistFileParser);
+ d->handleData();
+}
+
+void QPlaylistFileParserPrivate::handleParserFinished()
+{
+ Q_Q(QPlaylistFileParser);
+ const bool isParserValid = !m_currentParser.isNull();
+ if (!isParserValid && !m_aborted)
+ emit q->error(QMediaPlaylist::FormatNotSupportedError,
+ QMediaPlaylist::tr("Empty file provided"));
+
+ if (isParserValid && !m_aborted) {
+ m_currentParser.reset();
+ emit q->finished();
+ }
+
+ if (!m_aborted)
+ q->abort();
+
+ if (!m_source.isNull())
+ m_source.reset();
+
+ if (m_pendingJob.isValid())
+ q->start(m_pendingJob.m_media, m_pendingJob.m_stream, m_pendingJob.m_mimeType);
+}
+
+void QPlaylistFileParserPrivate::abort()
+{
+ m_aborted = true;
+ if (!m_currentParser.isNull())
+ m_currentParser->abort();
+}
+
+void QPlaylistFileParserPrivate::reset()
+{
+ Q_ASSERT(m_currentParser.isNull());
+ Q_ASSERT(m_source.isNull());
+ m_buffer.clear();
+ m_root.clear();
+ m_mimeType.clear();
+ m_stream = nullptr;
+ m_type = QPlaylistFileParser::UNKNOWN;
+ m_scanIndex = 0;
+ m_lineIndex = -1;
+ m_utf8 = false;
+ m_aborted = false;
+ m_pendingJob.reset();
+}
+
+void QPlaylistFileParser::handleError()
+{
+ Q_D(QPlaylistFileParser);
+ const QString &errorString = d->m_source->errorString();
+ Q_EMIT error(QMediaPlaylist::NetworkError, errorString);
+ abort();
+}
+
+QT_END_NAMESPACE
+
+#include "moc_qplaylistfileparser.cpp"