8#include <QRegularExpression>
9#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
10#include <QStringConverter>
14#include "libmythbase/mythconfig.h"
20#define LOC QString("HLSReader[%1]: ").arg(m_inputId)
27static QUrl
RelativeURI (
const QString& baseString,
const QString& uriString)
29 QUrl base(baseString);
30 QUrl uri(QUrl::fromEncoded(uriString.toLatin1()));
32 return base.resolved(uri);
37 LOG(VB_RECORD, LOG_INFO,
LOC +
"dtor -- start");
39 LOG(VB_RECORD, LOG_INFO,
LOC +
"dtor -- end");
44 LOG(VB_RECORD, LOG_INFO,
LOC + QString(
"Opening '%1'").arg(m3u));
50 LOG(VB_RECORD, LOG_ERR,
LOC +
"Already open");
58#ifdef HLS_USE_MYTHDOWNLOADMANAGER
59 if (!DownloadURL(m3u, &buffer))
61 LOG(VB_RECORD, LOG_ERR,
LOC +
"Open failed.");
67 if (!downloader.
DownloadURL(m3u, &buffer, 30s, 0, 0, &redir))
69 LOG(VB_GENERAL, LOG_ERR,
75 QTextStream text(&buffer);
76#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
77 text.setCodec(
"UTF-8");
79 text.setEncoding(QStringConverter::Utf8);
84 LOG(VB_RECORD, LOG_ERR,
LOC +
85 QString(
"Open '%1': not a valid playlist").arg(m3u));
102 StreamContainer::iterator Istream;
114 LOG(VB_RECORD, LOG_ERR,
LOC +
115 QString(
"Open '%1': Only %2 bitrates, %3 is not a valid index")
131 LOG(VB_RECORD, LOG_ERR,
LOC + QString(
"No stream selected"));
135 LOG(VB_RECORD, LOG_INFO,
LOC +
136 QString(
"Selected stream with %3 bitrate")
147 LOG(VB_RECORD, LOG_INFO,
LOC +
"Open -- end");
153 LOG(VB_RECORD, (quiet ? LOG_DEBUG : LOG_INFO),
LOC +
"Close -- start");
160 StreamContainer::iterator Istream;
172 LOG(VB_RECORD, (quiet ? LOG_DEBUG : LOG_INFO),
LOC +
"Close -- end");
177 LOG(VB_RECORD, (quiet ? LOG_DEBUG : LOG_INFO),
LOC +
"Cancel -- start");
188 LOG(VB_RECORD, LOG_INFO,
LOC +
"Cancel");
196#ifdef HLS_USE_MYTHDOWNLOADMANAGER
197 if (!m_sements.empty())
201 LOG(VB_RECORD, (quiet ? LOG_DEBUG : LOG_INFO),
LOC +
"Cancel -- done");
206 LOG(VB_RECORD, LOG_INFO,
LOC + QString(
"Throttle(%1)")
207 .arg(val ?
"true" :
"false"));
222 LOG(VB_RECORD, LOG_ERR,
LOC +
"Read: no stream selected");
227 LOG(VB_RECORD, LOG_DEBUG,
LOC + QString(
"Read: canceled"));
234 LOG(VB_RECORD, LOG_DEBUG,
LOC + QString(
"Reading %1 of %2 bytes")
237 memcpy(buffer,
m_buffer.constData(), len);
250#ifdef HLS_USE_MYTHDOWNLOADMANAGER
251bool HLSReader::DownloadURL(
const QString &url, QByteArray *buffer)
280 QString line = text.readLine();
281 if (!line.startsWith((
const char*)
"#EXTM3U"))
286 line = text.readLine();
289 LOG(VB_RECORD, LOG_DEBUG,
290 QString(
"IsValidPlaylist: |'%1'").arg(line));
291 if (line.startsWith(QLatin1String(
"#EXT-X-TARGETDURATION")) ||
292 line.startsWith(QLatin1String(
"#EXT-X-STREAM-INF")) ||
293 line.startsWith(QLatin1String(
"#EXT-X-MEDIA")) ||
294 line.startsWith(QLatin1String(
"#EXT-X-KEY")) ||
295 line.startsWith(QLatin1String(
"#EXT-X-ALLOW-CACHE")) ||
296 line.startsWith(QLatin1String(
"#EXT-X-ENDLIST")) ||
297 line.startsWith(QLatin1String(
"#EXT-X-DISCONTINUITY")) ||
298 line.startsWith(QLatin1String(
"#EXT-X-VERSION")))
304 LOG(VB_RECORD, LOG_ERR, QString(
"IsValidPlaylist: false"));
318 LOG(VB_RECORD, LOG_DEBUG,
LOC +
"ParseM3U8 -- begin");
320 QTextStream text(buffer);
322 QString line = text.readLine();
325 LOG(VB_RECORD, LOG_ERR,
LOC +
"ParseM3U8: empty line");
329 if (!line.startsWith(QLatin1String(
"#EXTM3U")))
331 LOG(VB_RECORD, LOG_ERR,
LOC +
332 "ParseM3U8: missing #EXTM3U tag .. aborting");
338 int p = buffer.indexOf(
"#EXT-X-VERSION:");
346 if (buffer.indexOf(
"#EXT-X-STREAM-INF") >= 0)
349 LOG(VB_RECORD, LOG_DEBUG,
LOC +
"Master Playlist");
355 line = text.readLine();
359 LOG(VB_RECORD, LOG_INFO,
LOC + QString(
"|%1").arg(line));
362 if (line.startsWith(QLatin1String(
"#EXT-X-STREAM-INF")))
364 QString uri = text.readLine();
365 if (uri.isNull() || uri.startsWith(QLatin1String(
"#")))
367 LOG(VB_RECORD, LOG_INFO,
LOC +
368 QString(
"ParseM3U8: Invalid EXT-X-STREAM-INF data '%1'")
375 StreamContainer::iterator Istream =
m_streams.find(url);
379 uint64_t bandwidth = 0;
383 id, bandwidth, audio, video))
389 LOG(VB_RECORD, LOG_INFO,
LOC +
390 QString(
"Adding stream %1")
391 .arg(hls->toString()));
397 LOG(VB_RECORD, LOG_INFO,
LOC +
398 QString(
"Already have stream '%1'").arg(url));
406 LOG(VB_RECORD, LOG_DEBUG,
LOC +
"Media Playlist");
409 if (stream ==
nullptr)
412 StreamContainer::iterator Istream =
419 LOG(VB_RECORD, LOG_INFO,
LOC +
420 QString(
"Adding new stream '%1'").arg(
m_m3u8));
427 LOG(VB_RECORD, LOG_INFO,
LOC +
428 QString(
"Updating stream '%1'").arg(hls->
toString()));
432 p = buffer.indexOf(
"#EXT-X-TARGETDURATION:");
446 LOG(VB_RECORD, LOG_INFO,
LOC +
447 QString(
"%1 Media Playlist HLS protocol version: %2")
454 std::chrono::milliseconds segment_duration = 0s;
455 int64_t first_sequence = -1;
456 int64_t sequence_num = 0;
468 line = text.readLine();
471 LOG(VB_RECORD, (
m_debug ? LOG_INFO : LOG_DEBUG),
472 LOC + QString(
"|%1").arg(line));
474 if (line.startsWith(QLatin1String(
"#EXTINF")))
476 int tmp_duration = 0;
481 segment_duration = std::chrono::milliseconds(tmp_duration);
483 else if (line.startsWith(QLatin1String(
"#EXT-X-TARGETDURATION")))
490 else if (line.startsWith(QLatin1String(
"#EXT-X-MEDIA-SEQUENCE")))
494 if (first_sequence < 0)
495 first_sequence = sequence_num;
497 else if (line.startsWith(QLatin1String(
"#EXT-X-MEDIA")))
501 else if (line.startsWith(QLatin1String(
"#EXT-X-KEY")))
513 LOG(VB_RECORD, LOG_ERR,
LOC +
"#EXT-X-KEY needs libcrypto");
517 else if (line.startsWith(QLatin1String(
"#EXT-X-MAP")))
524 else if (line.startsWith(QLatin1String(
"#EXT-X-PROGRAM-DATE-TIME")))
531 else if (line.startsWith(QLatin1String(
"#EXT-X-ALLOW-CACHE")))
533 bool do_cache =
false;
538 else if (line.startsWith(QLatin1String(
"#EXT-X-DISCONTINUITY-SEQUENCE")))
545 else if (line.startsWith(QLatin1String(
"#EXT-X-DISCONTINUITY")))
551 else if (line.startsWith(QLatin1String(
"#EXT-X-INDEPENDENT-SEGMENTS")))
557 else if (line.startsWith(QLatin1String(
"#EXT-X-VERSION")))
564 else if (line.startsWith(QLatin1String(
"#EXT-X-ENDLIST")))
571 else if (!line.startsWith(QLatin1String(
"#")) && !line.isEmpty())
573 if (m_curSeq < 0 || sequence_num >
m_curSeq)
579 if (!aes_iv.isEmpty() || !aes_keypath.isEmpty())
581 LOG(VB_RECORD, LOG_DEBUG,
LOC +
" aes_iv:" + aes_iv +
" aes_keypath:" + aes_keypath);
584 segment.SetKeyPath(aes_keypath);
585 if (!aes_iv.isEmpty() && !segment.SetAESIV(aes_iv))
587 LOG(VB_RECORD, LOG_ERR,
LOC +
"invalid AES IV:" + aes_iv);
593 new_segments.push_back(segment);
601 segment_duration = -1s;
606 LOG(VB_RECORD, LOG_DEBUG,
LOC +
607 QString(
"first_sequence:%1").arg(first_sequence) +
608 QString(
" sequence_num:%1").arg(sequence_num) +
609 QString(
" m_curSeq:%1").arg(
m_curSeq) +
610 QString(
" skipped:%1").arg(skipped));
615 LOG(VB_RECORD, LOG_WARNING,
LOC +
616 QString(
"Sequence number has been reset from %1 to %2")
617 .arg(
m_curSeq).arg(first_sequence));
627 int numseg = new_segments.size();
628 numseg = std::min(numseg, 3);
636 if (new_segments.size() > numseg)
638 int size_before = new_segments.size();
639 SegmentContainer::iterator it = new_segments.begin() + (new_segments.size() - numseg);
640 new_segments.erase(new_segments.begin(), it);
641 LOG(VB_RECORD, LOG_INFO,
LOC +
642 QString(
"Read last %1 segments instead of %2 for near-live")
643 .arg(new_segments.size()).arg(size_before));
646 first_sequence += size_before - new_segments.size();
650 SegmentContainer::iterator Inew = new_segments.begin();
651 SegmentContainer::iterator Iseg =
m_segments.end() - 1;
654 if (!
m_segments.empty() && !new_segments.empty())
656 if ((*Iseg).Sequence() >= first_sequence &&
657 (*Iseg).Sequence() < sequence_num)
661 (*Iseg).Sequence() > first_sequence &&
662 (*Iseg).Sequence() < sequence_num)
665 int64_t diff = (*Iseg).Sequence() - (*Inew).Sequence();
666 if (diff >= 0 && new_segments.size() > diff)
671 for ( ; Iseg !=
m_segments.end(); ++Iseg, ++Inew)
673 if (Inew == new_segments.end())
675 LOG(VB_RECORD, LOG_ERR,
LOC +
676 QString(
"Went off the end with %1 left")
680 if ((*Iseg).Sequence() != (*Inew).Sequence())
682 LOG(VB_RECORD, LOG_ERR,
LOC +
683 QString(
"Sequence non-sequential? %1 != %2")
684 .arg((*Iseg).Sequence())
685 .arg((*Inew).Sequence()));
689 (*Iseg).m_duration = (*Inew).Duration();
690 (*Iseg).m_title = (*Inew).Title();
691 (*Iseg).m_url = (*Inew).Url();
697 for ( ; Inew != new_segments.end(); ++Inew)
704 LOG(VB_RECORD, LOG_INFO,
LOC +
705 QString(
"new_segments.size():%1 ").arg(new_segments.size()) +
707 QString(
"behind:%1 ").arg(behind) +
708 QString(
"max_behind:%1").arg(max_behind));
710 if (behind > max_behind)
712 LOG(VB_RECORD, LOG_WARNING,
LOC +
713 QString(
"Not downloading fast enough! "
714 "%1 segments behind, skipping %2 segments. "
715 "playlist size: %3, queued: %4")
716 .arg(behind).arg(behind - max_behind)
724 Iseg =
m_segments.begin() + (behind - max_behind);
738 LOG(VB_RECORD, LOG_DEBUG,
LOC +
"ParseM3U8 -- end");
747 LOG(VB_RECORD, LOG_DEBUG,
LOC +
748 QString(
"LoadMetaPlaylists stream %1")
751 StreamContainer::iterator Istream;
761 LOG(VB_RECORD, LOG_WARNING,
LOC +
762 QString(
"Falling behind: only %1% buffered").arg(buffered));
763 LOG(VB_RECORD, LOG_DEBUG,
LOC +
764 QString(
"playlist size %1, queued %2")
770 else if (buffered > 85)
773 LOG(VB_RECORD, LOG_DEBUG,
LOC +
774 QString(
"Plenty of bandwidth, downloading %1 of %2")
777 LOG(VB_RECORD, LOG_DEBUG,
LOC +
778 QString(
"playlist size %1, queued %2")
788#ifdef HLS_USE_MYTHDOWNLOADMANAGER
795 LOG(VB_GENERAL, LOG_WARNING,
820 uint64_t candidate = 0;
824 if ((*Istream)->Id() != progid)
826 if (bitrate > (*Istream)->Bitrate() &&
827 candidate < (*Istream)->Bitrate())
829 LOG(VB_RECORD, LOG_DEBUG,
LOC +
830 QString(
"candidate stream '%1' bitrate %2 >= %3")
831 .arg(Istream.key()).arg(bitrate).arg((*Istream)->Bitrate()));
839 LOG(VB_RECORD, LOG_INFO,
LOC +
840 QString(
"Switching to a lower bitrate stream %1 -> %2")
841 .arg(bitrate).arg(candidate));
846 LOG(VB_RECORD, LOG_DEBUG,
LOC +
847 QString(
"Already at lowest bitrate %1").arg(bitrate));
855 uint64_t candidate = INT_MAX;
859 if ((*Istream)->Id() != progid)
861 if (bitrate < (*Istream)->Bitrate() &&
862 candidate > (*Istream)->Bitrate())
864 LOG(VB_RECORD, LOG_DEBUG,
LOC +
865 QString(
"candidate stream '%1' bitrate %2 >= %3")
866 .arg(Istream.key()).arg(bitrate).arg((*Istream)->Bitrate()));
874 LOG(VB_RECORD, LOG_INFO,
LOC +
875 QString(
"Switching to a higher bitrate stream %1 -> %2")
876 .arg(bitrate).arg(candidate));
881 LOG(VB_RECORD, LOG_DEBUG,
LOC +
882 QString(
"Already at highest bitrate %1").arg(bitrate));
888 LOG(VB_RECORD, LOG_DEBUG,
LOC +
"LoadSegment -- start");
892 LOG(VB_RECORD, LOG_ERR,
LOC +
"LoadSegment: current stream not set.");
911 QString(
"Downloading segment %1 (1 of %2) with %3 behind")
919 QString(
"Downloading segment %1 (%2 of %3)")
931 LOG(VB_RECORD, LOG_DEBUG,
LOC +
"LoadSegment -- no current stream");
942 SegmentContainer::iterator Iseg =
m_segments.begin() +
957 else if (throttle > 8)
961 LOG(VB_RECORD, LOG_INFO,
LOC +
962 QString(
"Throttling -- sleeping %1 secs.")
967 LOG(VB_RECORD, LOG_INFO,
LOC +
"Throttle aborted");
969 LOG(VB_RECORD, LOG_INFO,
LOC +
"Throttle done");
987 LOG(VB_RECORD, LOG_DEBUG,
LOC +
"LoadSegment -- end");
1005 LOG(VB_RECORD, LOG_DEBUG,
LOC +
1006 QString(
"Downloading seq#%1 av.bandwidth:%2 bitrate:%3")
1010 if ((bandwidth > 0) && (hls->
Bitrate() > 0) && (segment.
Duration().count() > 0))
1013 auto estimated_time = std::chrono::milliseconds(size / bandwidth);
1014 if (estimated_time > segment.
Duration())
1016 LOG(VB_RECORD, LOG_WARNING,
LOC +
1017 QString(
"downloading of %1 will take %2ms, "
1018 "which is longer than its playback (%3ms) at %4kB/s")
1020 .arg(estimated_time.count())
1022 .arg(bandwidth / 8000));
1024 LOG(VB_RECORD, LOG_DEBUG,
LOC +
1025 QString(
" sequence:%1").arg(segment.
Sequence()) +
1026 QString(
" bandwidth:%1").arg(bandwidth) +
1027 QString(
" hls->Bitrate:%1").arg(hls->
Bitrate()) +
1028 QString(
" seg.Dur.cnt:%1").arg(segment.
Duration().count()) +
1029 QString(
" est_time:%1").arg(estimated_time.count()));
1033 auto start = nowAsDuration<std::chrono::milliseconds>();
1035#ifdef HLS_USE_MYTHDOWNLOADMANAGER
1037 if (!HLSReader::DownloadURL(segment.
Url(), &buffer))
1039 LOG(VB_RECORD, LOG_ERR,
LOC +
1040 QString(
"%1 failed").arg(segment.
Sequence()));
1041 if (estimated_time * 2 < segment.
Duration())
1043 if (!HLSReader::DownloadURL(segment.
Url(), &buffer))
1045 LOG(VB_RECORD, LOG_ERR,
LOC + QString(
"%1 failed")
1056 LOG(VB_RECORD, LOG_ERR,
LOC + QString(
"%1 failed: %2")
1062 auto downloadduration = nowAsDuration<std::chrono::milliseconds>() - start;
1064 LOG(VB_RECORD, LOG_DEBUG,
LOC +
1065 QString(
"Downloaded segment %1 %2").arg(segment.
Sequence()).arg(segment.
Url().toString()));
1069 if (segment.HasKeyPath())
1071 if (!hls->DecodeData(downloader,
1072 segment.IVLoaded() ? segment.AESIV() : QByteArray(),
1078 LOG(VB_RECORD, LOG_DEBUG,
LOC +
1079 QString(
"Decoded segment sequence %1").arg(segment.
Sequence()));
1082 int64_t segment_len = buffer.size();
1085 if (
m_buffer.size() > segment_len * playlist_size)
1087 LOG(VB_RECORD, LOG_WARNING,
LOC +
1088 QString(
"streambuffer is not reading fast enough. "
1089 "buffer size %1").arg(
m_buffer.size()));
1103 if (
m_buffer.size() >= segment_len * playlist_size * 2)
1105 LOG(VB_RECORD, LOG_WARNING,
LOC +
1106 QString(
"streambuffer is not reading fast enough. "
1107 "buffer size %1. Dropping %2 bytes")
1108 .arg(
m_buffer.size()).arg(segment_len));
1118 hls->
SetBitrate((uint64_t)(((
double)segment_len * 8 * 1000) /
1119 ((
double)segment.
Duration().count())));
1122 if (downloadduration < 1ms)
1123 downloadduration = 1ms;
1126 bandwidth = 8ULL * 1000 * segment_len / downloadduration.count();
1134 QString(
"Sequence %1 took %2ms for %3 bytes, bandwidth:%4kB/s byterate:%5kB/s")
1136 .arg(downloadduration.count())
1138 .arg(bandwidth / 8000)
1170 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)