5#include <QRegularExpression>
6#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
7#include <QStringConverter>
10#include "libmythbase/mythconfig.h"
16#define LOC QString("HLSReader[%1]: ").arg(m_inputId)
23static QUrl
RelativeURI (
const QString& baseString,
const QString& uriString)
25 QUrl base(baseString);
26 QUrl uri(QUrl::fromEncoded(uriString.toLatin1()));
28 return base.resolved(uri);
33 LOG(VB_RECORD, LOG_INFO,
LOC +
"dtor -- start");
35 LOG(VB_RECORD, LOG_INFO,
LOC +
"dtor -- end");
40 LOG(VB_RECORD, LOG_INFO,
LOC + QString(
"Opening '%1'").arg(m3u));
46 LOG(VB_RECORD, LOG_ERR,
LOC +
"Already open");
54#ifdef HLS_USE_MYTHDOWNLOADMANAGER
55 if (!DownloadURL(m3u, &buffer))
57 LOG(VB_RECORD, LOG_ERR,
LOC +
"Open failed.");
63 if (!downloader.
DownloadURL(m3u, &buffer, 30s, 0, 0, &redir))
65 LOG(VB_GENERAL, LOG_ERR,
71 QTextStream text(&buffer);
72#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
73 text.setCodec(
"UTF-8");
75 text.setEncoding(QStringConverter::Utf8);
80 LOG(VB_RECORD, LOG_ERR,
LOC +
81 QString(
"Open '%1': not a valid playlist").arg(m3u));
98 StreamContainer::iterator Istream;
110 LOG(VB_RECORD, LOG_ERR,
LOC +
111 QString(
"Open '%1': Only %2 bitrates, %3 is not a valid index")
127 LOG(VB_RECORD, LOG_ERR,
LOC + QString(
"No stream selected"));
131 LOG(VB_RECORD, LOG_INFO,
LOC +
132 QString(
"Selected stream with %3 bitrate")
143 LOG(VB_RECORD, LOG_INFO,
LOC +
"Open -- end");
149 LOG(VB_RECORD, (
quiet ? LOG_DEBUG : LOG_INFO),
LOC +
"Close -- start");
156 StreamContainer::iterator Istream;
168 LOG(VB_RECORD, (
quiet ? LOG_DEBUG : LOG_INFO),
LOC +
"Close -- end");
173 LOG(VB_RECORD, (
quiet ? LOG_DEBUG : LOG_INFO),
LOC +
"Cancel -- start");
184 LOG(VB_RECORD, LOG_INFO,
LOC +
"Cancel");
192#ifdef HLS_USE_MYTHDOWNLOADMANAGER
193 if (!m_sements.empty())
197 LOG(VB_RECORD, (
quiet ? LOG_DEBUG : LOG_INFO),
LOC +
"Cancel -- done");
202 LOG(VB_RECORD, LOG_INFO,
LOC + QString(
"Throttle(%1)")
203 .arg(val ?
"true" :
"false"));
218 LOG(VB_RECORD, LOG_ERR,
LOC +
"Read: no stream selected");
223 LOG(VB_RECORD, LOG_DEBUG,
LOC + QString(
"Read: canceled"));
230 LOG(VB_RECORD, LOG_DEBUG,
LOC + QString(
"Reading %1 of %2 bytes")
233 memcpy(buffer,
m_buffer.constData(), len);
246#ifdef HLS_USE_MYTHDOWNLOADMANAGER
247bool HLSReader::DownloadURL(
const QString &url, QByteArray *buffer)
276 QString line = text.readLine();
277 if (!line.startsWith((
const char*)
"#EXTM3U"))
282 line = text.readLine();
285 LOG(VB_RECORD, LOG_DEBUG,
286 QString(
"IsValidPlaylist: |'%1'").arg(line));
287 if (line.startsWith(QLatin1String(
"#EXT-X-TARGETDURATION")) ||
288 line.startsWith(QLatin1String(
"#EXT-X-STREAM-INF")) ||
289 line.startsWith(QLatin1String(
"#EXT-X-MEDIA")) ||
290 line.startsWith(QLatin1String(
"#EXT-X-KEY")) ||
291 line.startsWith(QLatin1String(
"#EXT-X-ALLOW-CACHE")) ||
292 line.startsWith(QLatin1String(
"#EXT-X-ENDLIST")) ||
293 line.startsWith(QLatin1String(
"#EXT-X-DISCONTINUITY")) ||
294 line.startsWith(QLatin1String(
"#EXT-X-VERSION")))
300 LOG(VB_RECORD, LOG_ERR, QString(
"IsValidPlaylist: false"));
314 LOG(VB_RECORD, LOG_DEBUG,
LOC +
"ParseM3U8 -- begin");
316 QTextStream text(buffer);
318 QString line = text.readLine();
321 LOG(VB_RECORD, LOG_ERR,
LOC +
"ParseM3U8: empty line");
325 if (!line.startsWith(QLatin1String(
"#EXTM3U")))
327 LOG(VB_RECORD, LOG_ERR,
LOC +
328 "ParseM3U8: missing #EXTM3U tag .. aborting");
334 int p = buffer.indexOf(
"#EXT-X-VERSION:");
342 if (buffer.indexOf(
"#EXT-X-STREAM-INF") >= 0)
345 LOG(VB_RECORD, LOG_DEBUG,
LOC +
"Master Playlist");
351 line = text.readLine();
355 LOG(VB_RECORD, LOG_INFO,
LOC + QString(
"|%1").arg(line));
358 if (line.startsWith(QLatin1String(
"#EXT-X-STREAM-INF")))
360 QString uri = text.readLine();
361 if (uri.isNull() || uri.startsWith(QLatin1String(
"#")))
363 LOG(VB_RECORD, LOG_INFO,
LOC +
364 QString(
"ParseM3U8: Invalid EXT-X-STREAM-INF data '%1'")
371 StreamContainer::iterator Istream =
m_streams.find(url);
375 uint64_t bandwidth = 0;
379 id, bandwidth, audio, video))
385 LOG(VB_RECORD, LOG_INFO,
LOC +
386 QString(
"Adding stream %1")
387 .arg(hls->toString()));
393 LOG(VB_RECORD, LOG_INFO,
LOC +
394 QString(
"Already have stream '%1'").arg(url));
402 LOG(VB_RECORD, LOG_DEBUG,
LOC +
"Media Playlist");
405 if (stream ==
nullptr)
408 StreamContainer::iterator Istream =
415 LOG(VB_RECORD, LOG_INFO,
LOC +
416 QString(
"Adding new stream '%1'").arg(
m_m3u8));
423 LOG(VB_RECORD, LOG_INFO,
LOC +
424 QString(
"Updating stream '%1'").arg(hls->
toString()));
428 p = buffer.indexOf(
"#EXT-X-TARGETDURATION:");
442 LOG(VB_RECORD, LOG_INFO,
LOC +
443 QString(
"%1 Media Playlist HLS protocol version: %2")
450 std::chrono::milliseconds segment_duration = 0s;
451 int64_t first_sequence = -1;
452 int64_t sequence_num = 0;
464 line = text.readLine();
467 LOG(VB_RECORD, (
m_debug ? LOG_INFO : LOG_DEBUG),
468 LOC + QString(
"|%1").arg(line));
470 if (line.startsWith(QLatin1String(
"#EXTINF")))
472 int tmp_duration = 0;
477 segment_duration = std::chrono::milliseconds(tmp_duration);
479 else if (line.startsWith(QLatin1String(
"#EXT-X-TARGETDURATION")))
486 else if (line.startsWith(QLatin1String(
"#EXT-X-MEDIA-SEQUENCE")))
490 if (first_sequence < 0)
491 first_sequence = sequence_num;
493 else if (line.startsWith(QLatin1String(
"#EXT-X-MEDIA")))
497 else if (line.startsWith(QLatin1String(
"#EXT-X-KEY")))
509 LOG(VB_RECORD, LOG_ERR,
LOC +
"#EXT-X-KEY needs libcrypto");
513 else if (line.startsWith(QLatin1String(
"#EXT-X-MAP")))
520 else if (line.startsWith(QLatin1String(
"#EXT-X-PROGRAM-DATE-TIME")))
527 else if (line.startsWith(QLatin1String(
"#EXT-X-ALLOW-CACHE")))
529 bool do_cache =
false;
534 else if (line.startsWith(QLatin1String(
"#EXT-X-DISCONTINUITY-SEQUENCE")))
541 else if (line.startsWith(QLatin1String(
"#EXT-X-DISCONTINUITY")))
547 else if (line.startsWith(QLatin1String(
"#EXT-X-INDEPENDENT-SEGMENTS")))
553 else if (line.startsWith(QLatin1String(
"#EXT-X-VERSION")))
560 else if (line.startsWith(QLatin1String(
"#EXT-X-ENDLIST")))
567 else if (!line.startsWith(QLatin1String(
"#")) && !line.isEmpty())
569 if (m_curSeq < 0 || sequence_num >
m_curSeq)
575 if (!aes_iv.isEmpty() || !aes_keypath.isEmpty())
577 LOG(VB_RECORD, LOG_DEBUG,
LOC +
" aes_iv:" + aes_iv +
" aes_keypath:" + aes_keypath);
580 segment.SetKeyPath(aes_keypath);
581 if (!aes_iv.isEmpty() && !segment.SetAESIV(aes_iv))
583 LOG(VB_RECORD, LOG_ERR,
LOC +
"invalid AES IV:" + aes_iv);
589 new_segments.push_back(segment);
597 segment_duration = -1s;
602 LOG(VB_RECORD, LOG_DEBUG,
LOC +
603 QString(
"first_sequence:%1").arg(first_sequence) +
604 QString(
" sequence_num:%1").arg(sequence_num) +
605 QString(
" m_curSeq:%1").arg(
m_curSeq) +
606 QString(
" skipped:%1").arg(skipped));
611 LOG(VB_RECORD, LOG_WARNING,
LOC +
612 QString(
"Sequence number has been reset from %1 to %2")
613 .arg(
m_curSeq).arg(first_sequence));
623 int numseg = new_segments.size();
624 numseg = std::min(numseg, 3);
632 if (new_segments.size() > numseg)
634 int size_before = new_segments.size();
635 SegmentContainer::iterator it = new_segments.begin() + (new_segments.size() - numseg);
636 new_segments.erase(new_segments.begin(), it);
637 LOG(VB_RECORD, LOG_INFO,
LOC +
638 QString(
"Read last %1 segments instead of %2 for near-live")
639 .arg(new_segments.size()).arg(size_before));
642 first_sequence += size_before - new_segments.size();
646 SegmentContainer::iterator Inew = new_segments.begin();
647 SegmentContainer::iterator Iseg =
m_segments.end() - 1;
650 if (!
m_segments.empty() && !new_segments.empty())
652 if ((*Iseg).Sequence() >= first_sequence &&
653 (*Iseg).Sequence() < sequence_num)
657 (*Iseg).Sequence() > first_sequence &&
658 (*Iseg).Sequence() < sequence_num)
661 int64_t diff = (*Iseg).Sequence() - (*Inew).Sequence();
662 if (diff >= 0 && new_segments.size() > diff)
667 for ( ; Iseg !=
m_segments.end(); ++Iseg, ++Inew)
669 if (Inew == new_segments.end())
671 LOG(VB_RECORD, LOG_ERR,
LOC +
672 QString(
"Went off the end with %1 left")
676 if ((*Iseg).Sequence() != (*Inew).Sequence())
678 LOG(VB_RECORD, LOG_ERR,
LOC +
679 QString(
"Sequence non-sequential? %1 != %2")
680 .arg((*Iseg).Sequence())
681 .arg((*Inew).Sequence()));
685 (*Iseg).m_duration = (*Inew).Duration();
686 (*Iseg).m_title = (*Inew).Title();
687 (*Iseg).m_url = (*Inew).Url();
693 for ( ; Inew != new_segments.end(); ++Inew)
700 LOG(VB_RECORD, LOG_INFO,
LOC +
701 QString(
"new_segments.size():%1 ").arg(new_segments.size()) +
703 QString(
"behind:%1 ").arg(behind) +
704 QString(
"max_behind:%1").arg(max_behind));
706 if (behind > max_behind)
708 LOG(VB_RECORD, LOG_WARNING,
LOC +
709 QString(
"Not downloading fast enough! "
710 "%1 segments behind, skipping %2 segments. "
711 "playlist size: %3, queued: %4")
712 .arg(behind).arg(behind - max_behind)
720 Iseg =
m_segments.begin() + (behind - max_behind);
734 LOG(VB_RECORD, LOG_DEBUG,
LOC +
"ParseM3U8 -- end");
743 LOG(VB_RECORD, LOG_DEBUG,
LOC +
744 QString(
"LoadMetaPlaylists stream %1")
747 StreamContainer::iterator Istream;
757 LOG(VB_RECORD, LOG_WARNING,
LOC +
758 QString(
"Falling behind: only %1% buffered").arg(buffered));
759 LOG(VB_RECORD, LOG_DEBUG,
LOC +
760 QString(
"playlist size %1, queued %2")
766 else if (buffered > 85)
769 LOG(VB_RECORD, LOG_DEBUG,
LOC +
770 QString(
"Plenty of bandwidth, downloading %1 of %2")
773 LOG(VB_RECORD, LOG_DEBUG,
LOC +
774 QString(
"playlist size %1, queued %2")
784#ifdef HLS_USE_MYTHDOWNLOADMANAGER
791 LOG(VB_GENERAL, LOG_WARNING,
816 uint64_t candidate = 0;
820 if ((*Istream)->Id() != progid)
822 if (bitrate > (*Istream)->Bitrate() &&
823 candidate < (*Istream)->Bitrate())
825 LOG(VB_RECORD, LOG_DEBUG,
LOC +
826 QString(
"candidate stream '%1' bitrate %2 >= %3")
827 .arg(Istream.key()).arg(bitrate).arg((*Istream)->Bitrate()));
835 LOG(VB_RECORD, LOG_INFO,
LOC +
836 QString(
"Switching to a lower bitrate stream %1 -> %2")
837 .arg(bitrate).arg(candidate));
842 LOG(VB_RECORD, LOG_DEBUG,
LOC +
843 QString(
"Already at lowest bitrate %1").arg(bitrate));
851 uint64_t candidate = INT_MAX;
855 if ((*Istream)->Id() != progid)
857 if (bitrate < (*Istream)->Bitrate() &&
858 candidate > (*Istream)->Bitrate())
860 LOG(VB_RECORD, LOG_DEBUG,
LOC +
861 QString(
"candidate stream '%1' bitrate %2 >= %3")
862 .arg(Istream.key()).arg(bitrate).arg((*Istream)->Bitrate()));
870 LOG(VB_RECORD, LOG_INFO,
LOC +
871 QString(
"Switching to a higher bitrate stream %1 -> %2")
872 .arg(bitrate).arg(candidate));
877 LOG(VB_RECORD, LOG_DEBUG,
LOC +
878 QString(
"Already at highest bitrate %1").arg(bitrate));
884 LOG(VB_RECORD, LOG_DEBUG,
LOC +
"LoadSegment -- start");
888 LOG(VB_RECORD, LOG_ERR,
LOC +
"LoadSegment: current stream not set.");
907 QString(
"Downloading segment %1 (1 of %2) with %3 behind")
915 QString(
"Downloading segment %1 (%2 of %3)")
927 LOG(VB_RECORD, LOG_DEBUG,
LOC +
"LoadSegment -- no current stream");
938 SegmentContainer::iterator Iseg =
m_segments.begin() +
953 else if (throttle > 8)
957 LOG(VB_RECORD, LOG_INFO,
LOC +
958 QString(
"Throttling -- sleeping %1 secs.")
963 LOG(VB_RECORD, LOG_INFO,
LOC +
"Throttle aborted");
965 LOG(VB_RECORD, LOG_INFO,
LOC +
"Throttle done");
983 LOG(VB_RECORD, LOG_DEBUG,
LOC +
"LoadSegment -- end");
1001 LOG(VB_RECORD, LOG_DEBUG,
LOC +
1002 QString(
"Downloading seq#%1 av.bandwidth:%2 bitrate:%3")
1006 if ((bandwidth > 0) && (hls->
Bitrate() > 0) && (segment.
Duration().count() > 0))
1009 auto estimated_time = std::chrono::milliseconds(size / bandwidth);
1010 if (estimated_time > segment.
Duration())
1012 LOG(VB_RECORD, LOG_WARNING,
LOC +
1013 QString(
"downloading of %1 will take %2ms, "
1014 "which is longer than its playback (%3ms) at %4kB/s")
1016 .arg(estimated_time.count())
1018 .arg(bandwidth / 8000));
1020 LOG(VB_RECORD, LOG_DEBUG,
LOC +
1021 QString(
" sequence:%1").arg(segment.
Sequence()) +
1022 QString(
" bandwidth:%1").arg(bandwidth) +
1023 QString(
" hls->Bitrate:%1").arg(hls->
Bitrate()) +
1024 QString(
" seg.Dur.cnt:%1").arg(segment.
Duration().count()) +
1025 QString(
" est_time:%1").arg(estimated_time.count()));
1029 auto start = nowAsDuration<std::chrono::milliseconds>();
1031#ifdef HLS_USE_MYTHDOWNLOADMANAGER
1033 if (!HLSReader::DownloadURL(segment.
Url(), &buffer))
1035 LOG(VB_RECORD, LOG_ERR,
LOC +
1036 QString(
"%1 failed").arg(segment.
Sequence()));
1037 if (estimated_time * 2 < segment.
Duration())
1039 if (!HLSReader::DownloadURL(segment.
Url(), &buffer))
1041 LOG(VB_RECORD, LOG_ERR,
LOC + QString(
"%1 failed")
1052 LOG(VB_RECORD, LOG_ERR,
LOC + QString(
"%1 failed: %2")
1058 auto downloadduration = nowAsDuration<std::chrono::milliseconds>() - start;
1060 LOG(VB_RECORD, LOG_DEBUG,
LOC +
1061 QString(
"Downloaded segment %1 %2").arg(segment.
Sequence()).arg(segment.
Url().toString()));
1065 if (segment.HasKeyPath())
1067 if (!hls->DecodeData(downloader,
1068 segment.IVLoaded() ? segment.AESIV() : QByteArray(),
1074 LOG(VB_RECORD, LOG_DEBUG,
LOC +
1075 QString(
"Decoded segment sequence %1").arg(segment.
Sequence()));
1078 int64_t segment_len = buffer.size();
1081 if (
m_buffer.size() > segment_len * playlist_size)
1083 LOG(VB_RECORD, LOG_WARNING,
LOC +
1084 QString(
"streambuffer is not reading fast enough. "
1085 "buffer size %1").arg(
m_buffer.size()));
1099 if (
m_buffer.size() >= segment_len * playlist_size * 2)
1101 LOG(VB_RECORD, LOG_WARNING,
LOC +
1102 QString(
"streambuffer is not reading fast enough. "
1103 "buffer size %1. Dropping %2 bytes")
1104 .arg(
m_buffer.size()).arg(segment_len));
1114 hls->
SetBitrate((uint64_t)(((
double)segment_len * 8 * 1000) /
1115 ((
double)segment.
Duration().count())));
1118 if (downloadduration < 1ms)
1119 downloadduration = 1ms;
1122 bandwidth = 8ULL * 1000 * segment_len / downloadduration.count();
1130 QString(
"Sequence %1 took %2ms for %3 bytes, bandwidth:%4kB/s byterate:%5kB/s")
1132 .arg(downloadduration.count())
1134 .arg(bandwidth / 8000)
1166 LOG(VB_RECORD, LOG_INFO,
LOC +
"Debugging enabled");
static QUrl RelativeURI(const QString &baseString, const QString &uriString)
Handles relative URLs without breaking URI encoded parameters by avoiding storing the decoded URL in ...
void DecreaseBitrate(int progid)
static bool IsValidPlaylist(QTextStream &text)
QWaitCondition m_throttleCond
StreamContainer m_streams
void PlaylistRetrying(void)
friend class HLSStreamWorker
HLSStreamWorker * m_streamWorker
QVector< HLSRecSegment > SegmentContainer
bool IsOpen(const QString &url) const
bool ParseM3U8(const QByteArray &buffer, HLSRecStream *stream=nullptr)
HLSRecStream * m_curstream
static void CancelURL(const QString &url)
SegmentContainer m_segments
void EnableDebugging(void)
int DownloadSegmentData(MythSingleDownload &downloader, HLSRecStream *hls, HLSRecSegment &segment, int playlist_size)
QString StreamURL(void) const
uint PercentBuffered(void) const
void IncreaseBitrate(int progid)
bool LoadSegments(MythSingleDownload &downloader)
qint64 Read(uint8_t *buffer, qint64 len)
bool Open(const QString &m3u, int bitrate_index=0)
bool LoadMetaPlaylists(MythSingleDownload &downloader)
int PlaylistRetryCount(void) const
void Cancel(bool quiet=false)
friend class HLSPlaylistWorker
HLSPlaylistWorker * m_playlistWorker
void Close(bool quiet=false)
std::chrono::milliseconds Duration(void) const
int64_t Sequence(void) const
void SetDiscontinuitySequence(int s)
QString M3U8Url(void) const
void SetSegmentBaseUrl(const QString &n)
void SetCurrentByteRate(uint64_t byterate)
std::chrono::seconds TargetDuration(void) const
QString toString(void) const
void SetDateTime(QDateTime &dt)
void SetTargetDuration(std::chrono::seconds x)
QString SegmentBaseUrl(void) const
uint64_t Bitrate(void) const
int RetryCount(void) const
uint64_t CurrentByteRate(void) const
void SetMapUri(const QString &x)
void SetBitrate(uint64_t bitrate)
uint64_t AverageBandwidth(void) const
void CancelCurrentDownload(void)
void start(QThread::Priority p=QThread::InheritPriority)
Tell MThread to start running the thread in the near future.
void cancelDownload(const QString &url, bool block=true)
Cancel a queued or current download.
bool download(const QString &url, const QString &dest, bool reload=false)
Downloads a URL to a file in blocking mode.
QString ErrorString(void) const
bool DownloadURL(const QUrl &url, QByteArray *buffer, std::chrono::seconds timeout=30s, uint redirs=0, qint64 maxsize=0, QString *final_url=nullptr)
MythDownloadManager * GetMythDownloadManager(void)
Gets the pointer to the MythDownloadManager singleton.
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
bool ParseDiscontinuitySequence(const QString &line, const QString &loc, int &discontinuity_sequence)
bool ParseVersion(const QString &line, const QString &loc, int &version)
bool ParseAllowCache(const QString &line, const QString &loc, bool &do_cache)
bool ParseEndList(const QString &loc, bool &is_vod)
bool ParseMap(const QString &line, const QString &loc, QString &uri)
bool ParseTargetDuration(const QString &line, const QString &loc, int &duration)
bool ParseIndependentSegments(const QString &line, const QString &loc)
bool ParseMediaSequence(int64_t &sequence_num, const QString &line, const QString &loc)
bool ParseDiscontinuity(const QString &line, const QString &loc)
bool ParseProgramDateTime(const QString &line, const QString &loc, QDateTime &dt)
bool ParseSegmentInformation(int version, const QString &line, int &duration, QString &title, const QString &loc)
QString DecodedURI(const QString &uri)
bool ParseKey(int version, const QString &line, bool &aesmsg, const QString &loc, QString &path, QString &iv)
bool ParseStreamInformation(const QString &line, const QString &url, const QString &loc, int &id, uint64_t &bandwidth, QString &audio, QString &video)
static eu8 clamp(eu8 value, eu8 low, eu8 high)