7#include <QRegularExpression>
8#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
9#include <QStringConverter>
13#include "libmythbase/mythconfig.h"
19#define LOC QString("HLSReader[%1]: ").arg(m_inputId)
26static QUrl
RelativeURI (
const QString& baseString,
const QString& uriString)
28 QUrl base(baseString);
29 QUrl uri(QUrl::fromEncoded(uriString.toLatin1()));
31 return base.resolved(uri);
36 LOG(VB_RECORD, LOG_INFO,
LOC +
"dtor -- start");
38 LOG(VB_RECORD, LOG_INFO,
LOC +
"dtor -- end");
43 LOG(VB_RECORD, LOG_INFO,
LOC + QString(
"Opening '%1'").arg(m3u));
49 LOG(VB_RECORD, LOG_ERR,
LOC +
"Already open");
57#ifdef HLS_USE_MYTHDOWNLOADMANAGER
58 if (!DownloadURL(m3u, &buffer))
60 LOG(VB_RECORD, LOG_ERR,
LOC +
"Open failed.");
67 if (!m3uUrl.isValid())
69 LOG(VB_GENERAL, LOG_ERR,
LOC +
"Invalid url: " + m3u);
72 if (!downloader.
DownloadURL(m3uUrl, &buffer, 30s, 0, 0, &redir))
74 LOG(VB_GENERAL, LOG_ERR,
80 QTextStream text(&buffer);
81#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
82 text.setCodec(
"UTF-8");
84 text.setEncoding(QStringConverter::Utf8);
89 LOG(VB_RECORD, LOG_ERR,
LOC +
90 QString(
"Open '%1': not a valid playlist").arg(m3u));
107 StreamContainer::iterator Istream;
119 LOG(VB_RECORD, LOG_ERR,
LOC +
120 QString(
"Open '%1': Only %2 bitrates, %3 is not a valid index")
136 LOG(VB_RECORD, LOG_ERR,
LOC + QString(
"No stream selected"));
140 LOG(VB_RECORD, LOG_INFO,
LOC +
141 QString(
"Selected stream with %3 bitrate")
152 LOG(VB_RECORD, LOG_INFO,
LOC +
"Open -- end");
158 LOG(VB_RECORD, (quiet ? LOG_DEBUG : LOG_INFO),
LOC +
"Close -- start");
165 StreamContainer::iterator Istream;
177 LOG(VB_RECORD, (quiet ? LOG_DEBUG : LOG_INFO),
LOC +
"Close -- end");
182 LOG(VB_RECORD, (quiet ? LOG_DEBUG : LOG_INFO),
LOC +
"Cancel -- start");
193 LOG(VB_RECORD, LOG_INFO,
LOC +
"Cancel");
201#ifdef HLS_USE_MYTHDOWNLOADMANAGER
202 if (!m_sements.empty())
206 LOG(VB_RECORD, (quiet ? LOG_DEBUG : LOG_INFO),
LOC +
"Cancel -- done");
211 LOG(VB_RECORD, LOG_INFO,
LOC + QString(
"Throttle(%1)")
212 .arg(val ?
"true" :
"false"));
227 LOG(VB_RECORD, LOG_ERR,
LOC +
"Read: no stream selected");
232 LOG(VB_RECORD, LOG_DEBUG,
LOC + QString(
"Read: canceled"));
239 LOG(VB_RECORD, LOG_DEBUG,
LOC + QString(
"Reading %1 of %2 bytes")
242 memcpy(buffer,
m_buffer.constData(), len);
255#ifdef HLS_USE_MYTHDOWNLOADMANAGER
256bool HLSReader::DownloadURL(
const QString &url, QByteArray *buffer)
285 QString line = text.readLine();
286 if (!line.startsWith((
const char*)
"#EXTM3U"))
291 line = text.readLine();
294 LOG(VB_RECORD, LOG_DEBUG,
295 QString(
"IsValidPlaylist: |'%1'").arg(line));
296 if (line.startsWith(QLatin1String(
"#EXT-X-TARGETDURATION")) ||
297 line.startsWith(QLatin1String(
"#EXT-X-STREAM-INF")) ||
298 line.startsWith(QLatin1String(
"#EXT-X-MEDIA")) ||
299 line.startsWith(QLatin1String(
"#EXT-X-KEY")) ||
300 line.startsWith(QLatin1String(
"#EXT-X-ALLOW-CACHE")) ||
301 line.startsWith(QLatin1String(
"#EXT-X-ENDLIST")) ||
302 line.startsWith(QLatin1String(
"#EXT-X-DISCONTINUITY")) ||
303 line.startsWith(QLatin1String(
"#EXT-X-VERSION")))
309 LOG(VB_RECORD, LOG_ERR, QString(
"IsValidPlaylist: false"));
323 LOG(VB_RECORD, LOG_DEBUG,
LOC +
"ParseM3U8 -- begin");
325 QTextStream text(buffer);
327 QString line = text.readLine();
330 LOG(VB_RECORD, LOG_ERR,
LOC +
"ParseM3U8: empty line");
334 if (!line.startsWith(QLatin1String(
"#EXTM3U")))
336 LOG(VB_RECORD, LOG_ERR,
LOC +
337 "ParseM3U8: missing #EXTM3U tag .. aborting");
343 int p = buffer.indexOf(
"#EXT-X-VERSION:");
351 if (buffer.indexOf(
"#EXT-X-STREAM-INF") >= 0)
354 LOG(VB_RECORD, LOG_DEBUG,
LOC +
"Master Playlist");
360 line = text.readLine();
364 LOG(VB_RECORD, LOG_INFO,
LOC + QString(
"|%1").arg(line));
367 if (line.startsWith(QLatin1String(
"#EXT-X-STREAM-INF")))
369 QString uri = text.readLine();
370 if (uri.isNull() || uri.startsWith(QLatin1String(
"#")))
372 LOG(VB_RECORD, LOG_INFO,
LOC +
373 QString(
"ParseM3U8: Invalid EXT-X-STREAM-INF data '%1'")
380 StreamContainer::iterator Istream =
m_streams.find(url);
384 uint64_t bandwidth = 0;
388 id, bandwidth, audio, video))
394 LOG(VB_RECORD, LOG_INFO,
LOC +
395 QString(
"Adding stream %1")
396 .arg(hls->toString()));
402 LOG(VB_RECORD, LOG_INFO,
LOC +
403 QString(
"Already have stream '%1'").arg(url));
411 LOG(VB_RECORD, LOG_DEBUG,
LOC +
"Media Playlist");
414 if (stream ==
nullptr)
417 StreamContainer::iterator Istream =
424 LOG(VB_RECORD, LOG_INFO,
LOC +
425 QString(
"Adding new stream '%1'").arg(
m_m3u8));
432 LOG(VB_RECORD, LOG_INFO,
LOC +
433 QString(
"Updating stream '%1'").arg(hls->
toString()));
437 p = buffer.indexOf(
"#EXT-X-TARGETDURATION:");
451 LOG(VB_RECORD, LOG_INFO,
LOC +
452 QString(
"%1 Media Playlist HLS protocol version: %2")
459 std::chrono::milliseconds segment_duration = 0s;
460 int64_t first_sequence = -1;
461 int64_t sequence_num = 0;
473 line = text.readLine();
476 LOG(VB_RECORD, (
m_debug ? LOG_INFO : LOG_DEBUG),
477 LOC + QString(
"|%1").arg(line));
479 if (line.startsWith(QLatin1String(
"#EXTINF")))
481 int tmp_duration = 0;
486 segment_duration = std::chrono::milliseconds(tmp_duration);
488 else if (line.startsWith(QLatin1String(
"#EXT-X-TARGETDURATION")))
495 else if (line.startsWith(QLatin1String(
"#EXT-X-MEDIA-SEQUENCE")))
499 if (first_sequence < 0)
500 first_sequence = sequence_num;
502 else if (line.startsWith(QLatin1String(
"#EXT-X-MEDIA")))
506 else if (line.startsWith(QLatin1String(
"#EXT-X-KEY")))
518 LOG(VB_RECORD, LOG_ERR,
LOC +
"#EXT-X-KEY needs libcrypto");
522 else if (line.startsWith(QLatin1String(
"#EXT-X-MAP")))
529 else if (line.startsWith(QLatin1String(
"#EXT-X-PROGRAM-DATE-TIME")))
536 else if (line.startsWith(QLatin1String(
"#EXT-X-ALLOW-CACHE")))
538 bool do_cache =
false;
543 else if (line.startsWith(QLatin1String(
"#EXT-X-DISCONTINUITY-SEQUENCE")))
550 else if (line.startsWith(QLatin1String(
"#EXT-X-DISCONTINUITY")))
556 else if (line.startsWith(QLatin1String(
"#EXT-X-INDEPENDENT-SEGMENTS")))
562 else if (line.startsWith(QLatin1String(
"#EXT-X-VERSION")))
569 else if (line.startsWith(QLatin1String(
"#EXT-X-ENDLIST")))
576 else if (!line.startsWith(QLatin1String(
"#")) && !line.isEmpty())
578 if (m_curSeq < 0 || sequence_num >
m_curSeq)
584 if (!aes_iv.isEmpty() || !aes_keypath.isEmpty())
586 LOG(VB_RECORD, LOG_DEBUG,
LOC +
" aes_iv:" + aes_iv +
" aes_keypath:" + aes_keypath);
589 segment.SetKeyPath(aes_keypath);
590 if (!aes_iv.isEmpty() && !segment.SetAESIV(aes_iv))
592 LOG(VB_RECORD, LOG_ERR,
LOC +
"invalid AES IV:" + aes_iv);
598 new_segments.push_back(segment);
606 segment_duration = -1s;
611 LOG(VB_RECORD, LOG_DEBUG,
LOC +
612 QString(
"first_sequence:%1").arg(first_sequence) +
613 QString(
" sequence_num:%1").arg(sequence_num) +
614 QString(
" m_curSeq:%1").arg(
m_curSeq) +
615 QString(
" skipped:%1").arg(skipped));
620 LOG(VB_RECORD, LOG_WARNING,
LOC +
621 QString(
"Sequence number has been reset from %1 to %2")
622 .arg(
m_curSeq).arg(first_sequence));
632 int numseg = new_segments.size();
633 numseg = std::min(numseg, 3);
641 if (new_segments.size() > numseg)
643 int size_before = new_segments.size();
644 SegmentContainer::iterator it = new_segments.begin() + (new_segments.size() - numseg);
645 new_segments.erase(new_segments.begin(), it);
646 LOG(VB_RECORD, LOG_INFO,
LOC +
647 QString(
"Read last %1 segments instead of %2 for near-live")
648 .arg(new_segments.size()).arg(size_before));
651 first_sequence += size_before - new_segments.size();
655 SegmentContainer::iterator Inew = new_segments.begin();
656 SegmentContainer::iterator Iseg =
m_segments.end() - 1;
659 if (!
m_segments.empty() && !new_segments.empty())
661 if ((*Iseg).Sequence() >= first_sequence &&
662 (*Iseg).Sequence() < sequence_num)
666 (*Iseg).Sequence() > first_sequence &&
667 (*Iseg).Sequence() < sequence_num)
670 int64_t diff = (*Iseg).Sequence() - (*Inew).Sequence();
671 if (diff >= 0 && new_segments.size() > diff)
676 for ( ; Iseg !=
m_segments.end(); ++Iseg, ++Inew)
678 if (Inew == new_segments.end())
680 LOG(VB_RECORD, LOG_ERR,
LOC +
681 QString(
"Went off the end with %1 left")
685 if ((*Iseg).Sequence() != (*Inew).Sequence())
687 LOG(VB_RECORD, LOG_ERR,
LOC +
688 QString(
"Sequence non-sequential? %1 != %2")
689 .arg((*Iseg).Sequence())
690 .arg((*Inew).Sequence()));
694 (*Iseg).m_duration = (*Inew).Duration();
695 (*Iseg).m_title = (*Inew).Title();
696 (*Iseg).m_url = (*Inew).Url();
702 for ( ; Inew != new_segments.end(); ++Inew)
709 LOG(VB_RECORD, LOG_INFO,
LOC +
710 QString(
"new_segments.size():%1 ").arg(new_segments.size()) +
712 QString(
"behind:%1 ").arg(behind) +
713 QString(
"max_behind:%1").arg(max_behind));
715 if (behind > max_behind)
717 LOG(VB_RECORD, LOG_WARNING,
LOC +
718 QString(
"Not downloading fast enough! "
719 "%1 segments behind, skipping %2 segments. "
720 "playlist size: %3, queued: %4")
721 .arg(behind).arg(behind - max_behind)
729 Iseg =
m_segments.begin() + (behind - max_behind);
743 LOG(VB_RECORD, LOG_DEBUG,
LOC +
"ParseM3U8 -- end");
752 LOG(VB_RECORD, LOG_DEBUG,
LOC +
753 QString(
"LoadMetaPlaylists stream %1")
756 StreamContainer::iterator Istream;
766 LOG(VB_RECORD, LOG_WARNING,
LOC +
767 QString(
"Falling behind: only %1% buffered").arg(buffered));
768 LOG(VB_RECORD, LOG_DEBUG,
LOC +
769 QString(
"playlist size %1, queued %2")
775 else if (buffered > 85)
778 LOG(VB_RECORD, LOG_DEBUG,
LOC +
779 QString(
"Plenty of bandwidth, downloading %1 of %2")
782 LOG(VB_RECORD, LOG_DEBUG,
LOC +
783 QString(
"playlist size %1, queued %2")
793#ifdef HLS_USE_MYTHDOWNLOADMANAGER
799 if (!m3uUrl.isValid())
801 LOG(VB_GENERAL, LOG_WARNING,
805 if (!downloader.
DownloadURL(m3uUrl, &buffer, 30s, 0, 0, &redir))
807 LOG(VB_GENERAL, LOG_WARNING,
832 uint64_t candidate = 0;
836 if ((*Istream)->Id() != progid)
838 if (bitrate > (*Istream)->Bitrate() &&
839 candidate < (*Istream)->Bitrate())
841 LOG(VB_RECORD, LOG_DEBUG,
LOC +
842 QString(
"candidate stream '%1' bitrate %2 >= %3")
843 .arg(Istream.key()).arg(bitrate).arg((*Istream)->Bitrate()));
851 LOG(VB_RECORD, LOG_INFO,
LOC +
852 QString(
"Switching to a lower bitrate stream %1 -> %2")
853 .arg(bitrate).arg(candidate));
858 LOG(VB_RECORD, LOG_DEBUG,
LOC +
859 QString(
"Already at lowest bitrate %1").arg(bitrate));
867 uint64_t candidate = INT_MAX;
871 if ((*Istream)->Id() != progid)
873 if (bitrate < (*Istream)->Bitrate() &&
874 candidate > (*Istream)->Bitrate())
876 LOG(VB_RECORD, LOG_DEBUG,
LOC +
877 QString(
"candidate stream '%1' bitrate %2 >= %3")
878 .arg(Istream.key()).arg(bitrate).arg((*Istream)->Bitrate()));
886 LOG(VB_RECORD, LOG_INFO,
LOC +
887 QString(
"Switching to a higher bitrate stream %1 -> %2")
888 .arg(bitrate).arg(candidate));
893 LOG(VB_RECORD, LOG_DEBUG,
LOC +
894 QString(
"Already at highest bitrate %1").arg(bitrate));
900 LOG(VB_RECORD, LOG_DEBUG,
LOC +
"LoadSegment -- start");
904 LOG(VB_RECORD, LOG_ERR,
LOC +
"LoadSegment: current stream not set.");
923 QString(
"Downloading segment %1 (1 of %2) with %3 behind")
931 QString(
"Downloading segment %1 (%2 of %3)")
943 LOG(VB_RECORD, LOG_DEBUG,
LOC +
"LoadSegment -- no current stream");
954 SegmentContainer::iterator Iseg =
m_segments.begin() +
969 else if (throttle > 8)
973 LOG(VB_RECORD, LOG_INFO,
LOC +
974 QString(
"Throttling -- sleeping %1 secs.")
979 LOG(VB_RECORD, LOG_INFO,
LOC +
"Throttle aborted");
981 LOG(VB_RECORD, LOG_INFO,
LOC +
"Throttle done");
985 std::this_thread::sleep_for(5ms);
999 LOG(VB_RECORD, LOG_DEBUG,
LOC +
"LoadSegment -- end");
1017 LOG(VB_RECORD, LOG_DEBUG,
LOC +
1018 QString(
"Downloading seq#%1 av.bandwidth:%2 bitrate:%3")
1022 if ((bandwidth > 0) && (hls->
Bitrate() > 0) && (segment.
Duration().count() > 0))
1025 auto estimated_time = std::chrono::milliseconds(size / bandwidth);
1026 if (estimated_time > segment.
Duration())
1028 LOG(VB_RECORD, LOG_WARNING,
LOC +
1029 QString(
"downloading of %1 will take %2ms, "
1030 "which is longer than its playback (%3ms) at %4kB/s")
1032 .arg(estimated_time.count())
1034 .arg(bandwidth / 8000));
1036 LOG(VB_RECORD, LOG_DEBUG,
LOC +
1037 QString(
" sequence:%1").arg(segment.
Sequence()) +
1038 QString(
" bandwidth:%1").arg(bandwidth) +
1039 QString(
" hls->Bitrate:%1").arg(hls->
Bitrate()) +
1040 QString(
" seg.Dur.cnt:%1").arg(segment.
Duration().count()) +
1041 QString(
" est_time:%1").arg(estimated_time.count()));
1045 auto start = nowAsDuration<std::chrono::milliseconds>();
1047#ifdef HLS_USE_MYTHDOWNLOADMANAGER
1049 if (!HLSReader::DownloadURL(segment.
Url(), &buffer))
1051 LOG(VB_RECORD, LOG_ERR,
LOC +
1052 QString(
"%1 failed").arg(segment.
Sequence()));
1053 if (estimated_time * 2 < segment.
Duration())
1055 if (!HLSReader::DownloadURL(segment.
Url(), &buffer))
1057 LOG(VB_RECORD, LOG_ERR,
LOC + QString(
"%1 failed")
1068 LOG(VB_RECORD, LOG_ERR,
LOC + QString(
"%1 failed: %2")
1074 auto downloadduration = nowAsDuration<std::chrono::milliseconds>() - start;
1076 LOG(VB_RECORD, LOG_DEBUG,
LOC +
1077 QString(
"Downloaded segment %1 %2").arg(segment.
Sequence()).arg(segment.
Url().toString()));
1081 if (segment.HasKeyPath())
1083 if (!hls->DecodeData(downloader,
1084 segment.IVLoaded() ? segment.AESIV() : QByteArray(),
1090 LOG(VB_RECORD, LOG_DEBUG,
LOC +
1091 QString(
"Decoded segment sequence %1").arg(segment.
Sequence()));
1094 int64_t segment_len = buffer.size();
1097 if (
m_buffer.size() > segment_len * playlist_size)
1099 LOG(VB_RECORD, LOG_WARNING,
LOC +
1100 QString(
"streambuffer is not reading fast enough. "
1101 "buffer size %1").arg(
m_buffer.size()));
1115 if (
m_buffer.size() >= segment_len * playlist_size * 2)
1117 LOG(VB_RECORD, LOG_WARNING,
LOC +
1118 QString(
"streambuffer is not reading fast enough. "
1119 "buffer size %1. Dropping %2 bytes")
1120 .arg(
m_buffer.size()).arg(segment_len));
1130 hls->
SetBitrate((uint64_t)(((
double)segment_len * 8 * 1000) /
1131 ((
double)segment.
Duration().count())));
1134 if (downloadduration < 1ms)
1135 downloadduration = 1ms;
1138 bandwidth = 8ULL * 1000 * segment_len / downloadduration.count();
1146 QString(
"Sequence %1 took %2ms for %3 bytes, bandwidth:%4kB/s byterate:%5kB/s")
1148 .arg(downloadduration.count())
1150 .arg(bandwidth / 8000)
1182 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)