diff --git a/mythtv/libs/libmythtv/HLS/m3u.cpp b/mythtv/libs/libmythtv/HLS/m3u.cpp index 06c37e70ad1..7dbc6c173f7 100644 --- a/mythtv/libs/libmythtv/HLS/m3u.cpp +++ b/mythtv/libs/libmythtv/HLS/m3u.cpp @@ -1,11 +1,15 @@ +#include #include #include +#include "libmythbase/mythdate.h" #include "libmythbase/mythlogging.h" #include "HLS/m3u.h" namespace M3U { + static const QRegularExpression kQuotes{"^\"|\"$"}; + QString DecodedURI(const QString& uri) { QByteArray ba = uri.toLatin1(); @@ -116,10 +120,15 @@ namespace M3U return true; } + // EXT-X-STREAM-INF + // bool ParseStreamInformation(const QString& line, const QString& url, const QString& loc, - int& id, uint64_t& bandwidth) + int& id, + uint64_t& bandwidth, + QString& audio, + QString& video) { LOG(VB_RECORD, LOG_INFO, loc + QString("Parsing stream from %1").arg(url)); @@ -161,13 +170,65 @@ namespace M3U return false; } + // AUDIO + // + // The value is a quoted-string. It MUST match the value of the + // GROUP-ID attribute of an EXT-X-MEDIA tag elsewhere in the Master + // Playlist whose TYPE attribute is AUDIO. It indicates the set of + // audio Renditions that SHOULD be used when playing the + // presentation. See Section 4.3.4.2.1. + // + // The AUDIO attribute is OPTIONAL. + audio = ParseAttributes(line, "AUDIO"); + if (!audio.isEmpty()) + { + audio.replace(M3U::kQuotes, ""); + LOG(VB_RECORD, LOG_INFO, loc + + QString("#EXT-X-STREAM-INF: attribute AUDIO=%1").arg(audio)); + } + + // The VIDEO attribute is OPTIONAL. + video = ParseAttributes(line, "VIDEO"); + if (!video.isEmpty()) + { + video.replace(M3U::kQuotes, ""); + LOG(VB_RECORD, LOG_INFO, loc + + QString("#EXT-X-STREAM-INF: attribute VIDEO=%1").arg(video)); + } + + LOG(VB_RECORD, LOG_INFO, loc + - QString("bandwidth adaptation detected (program-id=%1, bandwidth=%2") + QString("bandwidth adaptation detected (program-id=%1, bandwidth=%2)") .arg(id).arg(bandwidth)); return true; } + // EXT-X-MEDIA + // + bool ParseMedia(const QString& line, + const QString& loc, + QString& media_type, + QString& group_id, + QString& uri, + QString& name) + { + LOG(VB_RECORD, LOG_INFO, loc + QString("Parsing EXT-X-MEDIA line")); + + media_type = ParseAttributes(line, "TYPE"); + group_id = ParseAttributes(line, "GROUP-ID"); + uri = ParseAttributes(line, "URI"); + name = ParseAttributes(line, "NAME"); + + // Remove string quotes + group_id.replace(M3U::kQuotes, ""); + uri.replace(M3U::kQuotes, ""); + name.replace(M3U::kQuotes, ""); + + return true; + } + + bool ParseTargetDuration(const QString& line, const QString& loc, int& duration) { @@ -176,8 +237,6 @@ namespace M3U * * where s is an integer indicating the target duration in seconds. */ - duration = -1; - if (!ParseDecimalValue(line, duration)) { LOG(VB_RECORD, LOG_ERR, loc + "expected #EXT-X-TARGETDURATION:"); @@ -188,17 +247,22 @@ namespace M3U } bool ParseSegmentInformation(int version, const QString& line, - uint& duration, QString& title, + int& duration, QString& title, const QString& loc) { /* * #EXTINF:, * - * "duration" is an integer that specifies the duration of the media - * file in seconds. Durations SHOULD be rounded to the nearest integer. - * The remainder of the line following the comma is the title of the - * media file, which is an optional human-readable informative title of - * the media segment + * where duration is a decimal-floating-point or decimal-integer number + * (as described in Section 4.2) that specifies the duration of the + * Media Segment in seconds. Durations SHOULD be decimal-floating- + * point, with enough accuracy to avoid perceptible error when segment + * durations are accumulated. However, if the compatibility version + * number is less than 3, durations MUST be integers. Durations that + * are reported as integers SHOULD be rounded to the nearest integer. + * The remainder of the line following the comma is an optional human- + * readable informative title of the Media Segment expressed as UTF-8 + * text. */ int p = line.indexOf(QLatin1String(":")); if (p < 0) @@ -220,36 +284,35 @@ namespace M3U return false; } + // Duration in ms + bool ok = false; const QString& val = list[0]; - if (version < 3) { - bool ok = false; - duration = val.toInt(&ok); - if (!ok) + int duration_seconds = val.toInt(&ok); + if (ok) + { + duration = duration_seconds * 1000; + } + else { - duration = -1; LOG(VB_RECORD, LOG_ERR, loc + QString("ParseSegmentInformation: invalid duration in '%1'") .arg(line)); + return false; } } else { - bool ok = false; double d = val.toDouble(&ok); if (!ok) { - duration = -1; LOG(VB_RECORD, LOG_ERR, loc + QString("ParseSegmentInformation: invalid duration in '%1'") .arg(line)); return false; } - if ((d) - ((int)d) >= 0.5) - duration = ((int)d) + 1; - else - duration = ((int)d); + duration = static_cast<int>(d * 1000); } if (list.size() >= 2) @@ -276,7 +339,6 @@ namespace M3U if (!ParseDecimalValue(line, sequence_num)) { LOG(VB_RECORD, LOG_ERR, loc + "expected #EXT-X-MEDIA-SEQUENCE:<s>"); - sequence_num = 0; return false; } @@ -307,7 +369,7 @@ namespace M3U if (attr.startsWith(QLatin1String("NONE"))) { QString uri = ParseAttributes(line, "URI"); - if (!uri.isNull()) + if (!uri.isEmpty()) { LOG(VB_RECORD, LOG_ERR, loc + "#EXT-X-KEY: URI not expected"); return false; @@ -315,8 +377,8 @@ namespace M3U /* IV is only supported in version 2 and above */ if (version >= 2) { - iv = ParseAttributes(line, "IV"); - if (!iv.isNull()) + QString parsed_iv = ParseAttributes(line, "IV"); + if (!parsed_iv.isEmpty()) { LOG(VB_RECORD, LOG_ERR, loc + "#EXT-X-KEY: IV not expected"); return false; @@ -344,6 +406,9 @@ namespace M3U /* Url is between quotes, remove them */ path = DecodedURI(uri.remove(QChar(QLatin1Char('"')))); iv = ParseAttributes(line, "IV"); + + LOG(VB_RECORD, LOG_DEBUG, QString("M3U::ParseKey #EXT-X-KEY: %1").arg(line)); + LOG(VB_RECORD, LOG_DEBUG, QString("M3U::ParseKey path:%1 IV:%2").arg(path).arg(iv)); } else if (attr.startsWith(QLatin1String("SAMPLE-AES"))) { @@ -371,15 +436,54 @@ namespace M3U return true; } + bool ParseMap(const QString &line, + const QString &loc, + QString &uri) + { + /* + * #EXT-X-MAP:<attribute-list> + * + * The EXT-X-MAP tag specifies how to obtain the Media Initialization + * Section (Section 3) required to parse the applicable Media Segments. + * It applies to every Media Segment that appears after it in the + * Playlist until the next EXT-X-MAP tag or until the end of the + * Playlist. + * + * The following attributes are defined: + * + * URI + * The value is a quoted-string containing a URI that identifies a + * resource that contains the Media Initialization Section. This + * attribute is REQUIRED. + */ + uri = ParseAttributes(line, "URI"); + if (uri.isEmpty()) + { + LOG(VB_RECORD, LOG_ERR, loc + + QString("Attribute URI not present in: #EXT-X-MAP %1") + .arg(line)); + return false; + } + return true; + } + bool ParseProgramDateTime(const QString& line, const QString& loc, - QDateTime &/*date*/) + QDateTime &dt) { /* * #EXT-X-PROGRAM-DATE-TIME:<YYYY-MM-DDThh:mm:ssZ> */ - LOG(VB_RECORD, LOG_DEBUG, loc + - QString("tag not supported: #EXT-X-PROGRAM-DATE-TIME %1") - .arg(line)); + int p = line.indexOf(QLatin1String(":")); + if (p < 0) + { + LOG(VB_RECORD, LOG_ERR, loc + + QString("ParseProgramDateTime: Missing ':' in '%1'") + .arg(line)); + return false; + } + + QString dt_string = line.mid(p+1); + dt = MythDate::fromString(dt_string); return true; } diff --git a/mythtv/libs/libmythtv/HLS/m3u.h b/mythtv/libs/libmythtv/HLS/m3u.h index a3badacf722..8de26682735 100644 --- a/mythtv/libs/libmythtv/HLS/m3u.h +++ b/mythtv/libs/libmythtv/HLS/m3u.h @@ -17,16 +17,28 @@ namespace M3U bool ParseStreamInformation(const QString& line, const QString& url, const QString& loc, - int& id, uint64_t& bandwidth); + int& id, + uint64_t& bandwidth, + QString& audio, + QString& video); + bool ParseMedia(const QString& line, + const QString& loc, + QString& media_type, + QString& group_id, + QString& uri, + QString& name); bool ParseTargetDuration(const QString& line, const QString& loc, int& duration); bool ParseSegmentInformation(int version, const QString& line, - uint& duration, QString& title, + int& duration, QString& title, const QString& loc); bool ParseMediaSequence(int64_t & sequence_num, const QString& line, const QString& loc); bool ParseKey(int version, const QString& line, bool& aesmsg, const QString& loc, QString &path, QString &iv); + bool ParseMap(const QString &line, + const QString &loc, + QString &uri); bool ParseProgramDateTime(const QString& line, const QString& loc, QDateTime &date); bool ParseAllowCache(const QString& line, const QString& loc, diff --git a/mythtv/libs/libmythtv/iptvtuningdata.h b/mythtv/libs/libmythtv/iptvtuningdata.h index dc1e73856bc..40aafaa03fc 100644 --- a/mythtv/libs/libmythtv/iptvtuningdata.h +++ b/mythtv/libs/libmythtv/iptvtuningdata.h @@ -205,6 +205,7 @@ class MTV_PUBLIC IPTVTuningData return (m_protocol == http_ts); } + // An http(s) URL is invalid if it cannot be downloaded void GuessProtocol(void) { if (!m_dataUrl.isValid()) @@ -215,10 +216,19 @@ class MTV_PUBLIC IPTVTuningData m_protocol = IPTVTuningData::rtp; else if (m_dataUrl.scheme() == "rtsp") m_protocol = IPTVTuningData::rtsp; - else if (((m_dataUrl.scheme() == "http") || (m_dataUrl.scheme() == "https")) && IsHLSPlaylist()) - m_protocol = IPTVTuningData::http_hls; else if ((m_dataUrl.scheme() == "http") || (m_dataUrl.scheme() == "https")) - m_protocol = IPTVTuningData::http_ts; + { + QByteArray buffer; + if (CanReadHTTP(buffer)) + { + if (IsHLSPlaylist(buffer)) + m_protocol = IPTVTuningData::http_hls; + else + m_protocol = IPTVTuningData::http_ts; + } + else + m_protocol = IPTVTuningData::inValid; + } else m_protocol = IPTVTuningData::inValid; } @@ -229,25 +239,28 @@ class MTV_PUBLIC IPTVTuningData } protected: - bool IsHLSPlaylist(void) const - { - if (QCoreApplication::instance() == nullptr) - { - LOG(VB_GENERAL, LOG_ERR, QString("IsHLSPlaylist - No QCoreApplication!!")); - return false; - } - MythSingleDownload downloader; + // Read first part of the http(s) URL. + // This is done to test if we can download from this URL + // and 2000 bytes is enough to determine in IsHLSPlaylist + // if the file is an HLS playlist or not. + // + bool CanReadHTTP(QByteArray &buffer) const + { QString url = m_dataUrl.toString(); - QByteArray buffer; - downloader.DownloadURL(url, &buffer, 5s, 0, 10000); + MythSingleDownload downloader; + downloader.DownloadURL(url, &buffer, 5s, 0, 2000); if (buffer.isEmpty()) { - LOG(VB_GENERAL, LOG_ERR, QString("IsHLSPlaylist - Open Failed:%1 url:%2") + LOG(VB_GENERAL, LOG_ERR, QString("CanReadHTTP - Failed, error:%1 url:%2") .arg(downloader.ErrorString(), url)); return false; } + return true; + } + bool IsHLSPlaylist(QByteArray &buffer) const + { QTextStream text(&buffer); #if QT_VERSION < QT_VERSION_CHECK(6,0,0) text.setCodec("UTF-8"); diff --git a/mythtv/libs/libmythtv/recorders/HLS/HLSPlaylistWorker.cpp b/mythtv/libs/libmythtv/recorders/HLS/HLSPlaylistWorker.cpp index 11bffdcbf78..659efff59e0 100644 --- a/mythtv/libs/libmythtv/recorders/HLS/HLSPlaylistWorker.cpp +++ b/mythtv/libs/libmythtv/recorders/HLS/HLSPlaylistWorker.cpp @@ -38,7 +38,7 @@ void HLSPlaylistWorker::run(void) RunProlog(); auto *downloader = new MythSingleDownload; - + m_wokenup = true; // Otherwise always false and then we start with 1 second delay while (!m_cancel) { m_lock.lock(); diff --git a/mythtv/libs/libmythtv/recorders/HLS/HLSReader.cpp b/mythtv/libs/libmythtv/recorders/HLS/HLSReader.cpp index bc653d2adee..ec97c081841 100644 --- a/mythtv/libs/libmythtv/recorders/HLS/HLSReader.cpp +++ b/mythtv/libs/libmythtv/recorders/HLS/HLSReader.cpp @@ -2,6 +2,7 @@ #include <unistd.h> #include <QtGlobal> +#include <QRegularExpression> #if QT_VERSION >= QT_VERSION_CHECK(6,0,0) #include <QStringConverter> #endif @@ -281,11 +282,11 @@ bool HLSReader::IsValidPlaylist(QTextStream & text) LOG(VB_RECORD, LOG_DEBUG, QString("IsValidPlaylist: |'%1'").arg(line)); if (line.startsWith(QLatin1String("#EXT-X-TARGETDURATION")) || - line.startsWith(QLatin1String("#EXT-X-MEDIA-SEQUENCE")) || + line.startsWith(QLatin1String("#EXT-X-STREAM-INF")) || + line.startsWith(QLatin1String("#EXT-X-MEDIA")) || line.startsWith(QLatin1String("#EXT-X-KEY")) || line.startsWith(QLatin1String("#EXT-X-ALLOW-CACHE")) || line.startsWith(QLatin1String("#EXT-X-ENDLIST")) || - line.startsWith(QLatin1String("#EXT-X-STREAM-INF")) || line.startsWith(QLatin1String("#EXT-X-DISCONTINUITY")) || line.startsWith(QLatin1String("#EXT-X-VERSION"))) { @@ -349,6 +350,8 @@ bool HLSReader::ParseM3U8(const QByteArray& buffer, HLSRecStream* stream) break; LOG(VB_RECORD, LOG_INFO, LOC + QString("|%1").arg(line)); + + // EXT-X-STREAM-INF if (line.startsWith(QLatin1String("#EXT-X-STREAM-INF"))) { QString uri = text.readLine(); @@ -367,8 +370,10 @@ bool HLSReader::ParseM3U8(const QByteArray& buffer, HLSRecStream* stream) { int id = 0; uint64_t bandwidth = 0; + QString audio; + QString video; if (!M3U::ParseStreamInformation(line, url, StreamURL(), - id, bandwidth)) + id, bandwidth, audio, video)) break; auto *hls = new HLSRecStream(id, bandwidth, url, m_segmentBase); @@ -439,11 +444,14 @@ bool HLSReader::ParseM3U8(const QByteArray& buffer, HLSRecStream* stream) text.seek(0); QString title; // From playlist, #EXTINF:<duration>,<title> - std::chrono::seconds segment_duration = -1s; // From playlist, e.g. #EXTINF:10.24, + std::chrono::milliseconds segment_duration = 0s;// From playlist, e.g. #EXTINF:10.24, int64_t first_sequence = -1; // Sequence number of first segment to be recorded int64_t sequence_num = 0; // Sequence number of next segment to be read int skipped = 0; // Segments skipped, sequence number at or below current - +#ifdef USING_LIBCRYPTO + QString aes_keypath; // AES key path + QString aes_iv; // AES IV value +#endif SegmentContainer new_segments; // All segments read from Media Playlist QMutexLocker lock(&m_seqLock); @@ -458,12 +466,12 @@ bool HLSReader::ParseM3U8(const QByteArray& buffer, HLSRecStream* stream) if (line.startsWith(QLatin1String("#EXTINF"))) { - uint tmp_duration = -1; + int tmp_duration = 0; if (!M3U::ParseSegmentInformation(hls->Version(), line, tmp_duration, title, StreamURL())) return false; - segment_duration = std::chrono::seconds(tmp_duration); + segment_duration = std::chrono::milliseconds(tmp_duration); } else if (line.startsWith(QLatin1String("#EXT-X-TARGETDURATION"))) { @@ -488,28 +496,30 @@ bool HLSReader::ParseM3U8(const QByteArray& buffer, HLSRecStream* stream) #ifdef USING_LIBCRYPTO QString path; QString iv; - if (!M3U::ParseKey(hls->Version(), line, m_aesMsg, StreamURL(), + if (!M3U::ParseKey(hls->Version(), line, m_aesMsg, LOC, path, iv)) return false; - if (!path.isEmpty()) - hls->SetKeyPath(path); - if (!iv.isNull() && !hls->SetAESIV(iv)) - { - LOG(VB_RECORD, LOG_ERR, LOC + "invalid IV"); - return false; - } -#else + aes_keypath = path; + aes_iv = iv; +#else // USING_LIBCRYPTO LOG(VB_RECORD, LOG_ERR, LOC + "#EXT-X-KEY needs libcrypto"); return false; -#endif +#endif // USING_LIBCRYPTO + } + else if (line.startsWith(QLatin1String("#EXT-X-MAP"))) + { + QString uri; + if (!M3U::ParseMap(line, StreamURL(), uri)) + return false; + hls->SetMapUri(uri); } else if (line.startsWith(QLatin1String("#EXT-X-PROGRAM-DATE-TIME"))) { - QDateTime date; - if (!M3U::ParseProgramDateTime(line, StreamURL(), date)) + QDateTime dt; + if (!M3U::ParseProgramDateTime(line, StreamURL(), dt)) return false; - // Not handled yet + hls->SetDateTime(dt); } else if (line.startsWith(QLatin1String("#EXT-X-ALLOW-CACHE"))) { @@ -555,9 +565,25 @@ bool HLSReader::ParseM3U8(const QByteArray& buffer, HLSRecStream* stream) { if (m_curSeq < 0 || sequence_num > m_curSeq) { - new_segments.push_back - (HLSRecSegment(sequence_num, segment_duration, title, - RelativeURI(hls->SegmentBaseUrl(), line))); + HLSRecSegment segment = + HLSRecSegment(sequence_num, segment_duration, title, + RelativeURI(hls->SegmentBaseUrl(), line)); +#ifdef USING_LIBCRYPTO + if (!aes_iv.isEmpty() || !aes_keypath.isEmpty()) + { + LOG(VB_RECORD, LOG_DEBUG, LOC + " aes_iv:" + aes_iv + " aes_keypath:" + aes_keypath); + } + + segment.SetKeyPath(aes_keypath); + if (!aes_iv.isEmpty() && !segment.SetAESIV(aes_iv)) + { + LOG(VB_RECORD, LOG_ERR, LOC + "invalid AES IV:" + aes_iv); + } + + aes_keypath.clear(); + aes_iv.clear(); +#endif // USING_LIBCRYPTO + new_segments.push_back(segment); } else { @@ -571,7 +597,7 @@ bool HLSReader::ParseM3U8(const QByteArray& buffer, HLSRecStream* stream) } LOG(VB_RECORD, LOG_DEBUG, LOC + - QString(" first_sequence:%1").arg(first_sequence) + + QString("first_sequence:%1").arg(first_sequence) + QString(" sequence_num:%1").arg(sequence_num) + QString(" m_curSeq:%1").arg(m_curSeq) + QString(" skipped:%1").arg(skipped)); @@ -899,7 +925,7 @@ bool HLSReader::LoadSegments(MythSingleDownload& downloader) return false; } - long throttle = DownloadSegmentData(downloader,hls,seg,m_playlistSize); + long throttle = DownloadSegmentData(downloader, hls, seg, m_playlistSize); m_seqLock.lock(); if (throttle < 0) @@ -965,29 +991,35 @@ uint HLSReader::PercentBuffered(void) const int HLSReader::DownloadSegmentData(MythSingleDownload& downloader, HLSRecStream* hls, - const HLSRecSegment& segment, int playlist_size) + HLSRecSegment& segment, int playlist_size) { uint64_t bandwidth = hls->AverageBandwidth(); LOG(VB_RECORD, LOG_DEBUG, LOC + - QString("Downloading seq#%1 av.bandwidth:%2 bitrate %3") + QString("Downloading seq#%1 av.bandwidth:%2 bitrate:%3") .arg(segment.Sequence()).arg(bandwidth).arg(hls->Bitrate())); /* sanity check - can we download this segment on time? */ if ((bandwidth > 0) && (hls->Bitrate() > 0) && (segment.Duration().count() > 0)) { uint64_t size = (segment.Duration().count() * hls->Bitrate()); /* bits */ - auto estimated_time = std::chrono::seconds(size / bandwidth); + auto estimated_time = std::chrono::milliseconds(size / bandwidth); if (estimated_time > segment.Duration()) { LOG(VB_RECORD, LOG_WARNING, LOC + - QString("downloading of %1 will take %2s, " - "which is longer than its playback (%3s) at %4KiB/s") + QString("downloading of %1 will take %2ms, " + "which is longer than its playback (%3ms) at %4kB/s") .arg(segment.Sequence()) .arg(estimated_time.count()) .arg(segment.Duration().count()) - .arg(bandwidth / 8192)); + .arg(bandwidth / 8000)); } + LOG(VB_RECORD, LOG_DEBUG, LOC + + QString(" sequence:%1").arg(segment.Sequence()) + + QString(" bandwidth:%1").arg(bandwidth) + + QString(" hls->Bitrate:%1").arg(hls->Bitrate()) + + QString(" seg.Dur.cnt:%1").arg(segment.Duration().count()) + + QString(" est_time:%1").arg(estimated_time.count())); } QByteArray buffer; @@ -1022,17 +1054,24 @@ int HLSReader::DownloadSegmentData(MythSingleDownload& downloader, auto downloadduration = nowAsDuration<std::chrono::milliseconds>() - start; + LOG(VB_RECORD, LOG_DEBUG, LOC + + QString("Downloaded segment %1 %2").arg(segment.Sequence()).arg(segment.Url().toString())); + #ifdef USING_LIBCRYPTO /* If the segment is encrypted, decode it */ if (segment.HasKeyPath()) { - if (!hls->DecodeData(downloader, hls->IVLoaded() ? hls->AESIV() : QByteArray(), + if (!hls->DecodeData(downloader, + segment.IVLoaded() ? segment.AESIV() : QByteArray(), segment.KeyPath(), - buffer, segment.Sequence())) + buffer, + segment.Sequence())) return 0; - } -#endif + LOG(VB_RECORD, LOG_DEBUG, LOC + + QString("Decoded segment sequence %1").arg(segment.Sequence())); + } +#endif // USING_LIBCRYPTO int64_t segment_len = buffer.size(); m_bufLock.lock(); @@ -1069,7 +1108,7 @@ int HLSReader::DownloadSegmentData(MythSingleDownload& downloader, if (hls->Bitrate() == 0 && segment.Duration() > 0s) { /* Try to estimate the bandwidth for this stream */ - hls->SetBitrate((uint64_t)(((double)segment_len * 8) / + hls->SetBitrate((uint64_t)(((double)segment_len * 8 * 1000) / ((double)segment.Duration().count()))); } @@ -1081,16 +1120,16 @@ int HLSReader::DownloadSegmentData(MythSingleDownload& downloader, hls->AverageBandwidth(bandwidth); if (segment.Duration() > 0s) { - hls->SetCurrentByteRate(segment_len/segment.Duration().count()); + hls->SetCurrentByteRate(segment_len * 1000 / segment.Duration().count()); } LOG(VB_RECORD, (m_debug ? LOG_INFO : LOG_DEBUG), LOC + - QString("%1 took %2ms for %3 bytes: bandwidth:%4KiB/s byterate:%5KiB/s") + QString("Sequence %1 took %2ms for %3 bytes, bandwidth:%4kB/s byterate:%5kB/s") .arg(segment.Sequence()) .arg(downloadduration.count()) .arg(segment_len) - .arg(bandwidth / 8192.0) - .arg(hls->CurrentByteRate() / 1024.0)); + .arg(bandwidth / 8000) + .arg(hls->CurrentByteRate() / 1000)); return m_slowCnt; } diff --git a/mythtv/libs/libmythtv/recorders/HLS/HLSReader.h b/mythtv/libs/libmythtv/recorders/HLS/HLSReader.h index f2b3c4db547..b9f15dbb64b 100644 --- a/mythtv/libs/libmythtv/recorders/HLS/HLSReader.h +++ b/mythtv/libs/libmythtv/recorders/HLS/HLSReader.h @@ -57,7 +57,6 @@ class MTV_PUBLIC HLSReader { QMutexLocker lock(&m_streamLock); m_curstream = nullptr; } void ResetSequence(void) { m_curSeq = -1; } - QString StreamURL(void) const { return QString("%1").arg(m_curstream ? m_curstream->M3U8Url() : ""); } @@ -89,7 +88,7 @@ class MTV_PUBLIC HLSReader // Downloading int DownloadSegmentData(MythSingleDownload& downloader, HLSRecStream* hls, - const HLSRecSegment& segment, int playlist_size); + HLSRecSegment& segment, int playlist_size); // Debug void EnableDebugging(void); diff --git a/mythtv/libs/libmythtv/recorders/HLS/HLSSegment.cpp b/mythtv/libs/libmythtv/recorders/HLS/HLSSegment.cpp index 8bd1b25c0fd..581232f6d8f 100644 --- a/mythtv/libs/libmythtv/recorders/HLS/HLSSegment.cpp +++ b/mythtv/libs/libmythtv/recorders/HLS/HLSSegment.cpp @@ -18,7 +18,7 @@ HLSRecSegment::HLSRecSegment(const HLSRecSegment& rhs) operator=(rhs); } -HLSRecSegment::HLSRecSegment(int seq, std::chrono::seconds duration, +HLSRecSegment::HLSRecSegment(int seq, std::chrono::milliseconds duration, QString title, QUrl uri) : m_sequence(seq), m_duration(duration), @@ -28,19 +28,6 @@ HLSRecSegment::HLSRecSegment(int seq, std::chrono::seconds duration, LOG(VB_RECORD, LOG_DEBUG, LOC + "ctor"); } -HLSRecSegment::HLSRecSegment(int seq, std::chrono::seconds duration, QString title, - QUrl uri, [[maybe_unused]] const QString& current_key_path) - : m_sequence(seq), - m_duration(duration), - m_title(std::move(title)), - m_url(std::move(uri)) -{ - LOG(VB_RECORD, LOG_DEBUG, LOC + "ctor"); -#ifdef USING_LIBCRYPTO - m_psz_key_path = current_key_path; -#endif -} - HLSRecSegment& HLSRecSegment::operator=(const HLSRecSegment& rhs) { if (&rhs != this) @@ -51,7 +38,9 @@ HLSRecSegment& HLSRecSegment::operator=(const HLSRecSegment& rhs) m_title = rhs.m_title; m_url = rhs.m_url; #ifdef USING_LIBCRYPTO - m_psz_key_path = rhs.m_psz_key_path; + m_keypath = rhs.m_keypath; + m_ivLoaded = rhs.m_ivLoaded; + m_aesIV = rhs.m_aesIV; #endif } return *this; @@ -67,3 +56,38 @@ QString HLSRecSegment::toString(void) const return QString("[%1] '%2' @ '%3' for %4") .arg(m_sequence).arg(m_title, m_url.toString(), QString::number(m_duration.count())); } + +#ifdef USING_LIBCRYPTO +bool HLSRecSegment::SetAESIV(QString line) +{ + LOG(VB_RECORD, LOG_INFO, LOC + "SetAESIV line:"+ line); + + /* + * If the EXT-X-KEY tag has the IV attribute, implementations MUST use + * the attribute value as the IV when encrypting or decrypting with that + * key. The value MUST be interpreted as a 128-bit hexadecimal number + * and MUST be prefixed with 0x or 0X. + */ + if (!line.startsWith(QLatin1String("0x"), Qt::CaseInsensitive)) + { + LOG(VB_RECORD, LOG_ERR, LOC + "SetAESIV does not start with 0x"); + return false; + } + + if (line.size() % 2) + { + // not even size, pad with front 0 + line.insert(2, QLatin1String("0")); + } +#if QT_VERSION < QT_VERSION_CHECK(6,0,0) + int padding = std::max(0, AES_BLOCK_SIZE - (line.size() - 2)); +#else + int padding = std::max(0LL, AES_BLOCK_SIZE - (line.size() - 2)); +#endif + QByteArray ba = QByteArray(padding, 0x0); + ba.append(QByteArray::fromHex(QByteArray(line.toLatin1().constData() + 2))); + m_aesIV = ba; + m_ivLoaded = true; + return true; +} +#endif // USING_LIBCRYPTO diff --git a/mythtv/libs/libmythtv/recorders/HLS/HLSSegment.h b/mythtv/libs/libmythtv/recorders/HLS/HLSSegment.h index e63bbc9e4ee..a8d1adc6f86 100644 --- a/mythtv/libs/libmythtv/recorders/HLS/HLSSegment.h +++ b/mythtv/libs/libmythtv/recorders/HLS/HLSSegment.h @@ -3,10 +3,16 @@ #include <cstdint> +#ifdef USING_LIBCRYPTO +// encryption related stuff +#include <openssl/aes.h> +#endif // USING_LIBCRYPTO + #include <QString> #include <QUrl> #include "libmythbase/mythchrono.h" +#include "libmythbase/mythsingledownload.h" class HLSRecSegment { @@ -15,10 +21,8 @@ class HLSRecSegment HLSRecSegment(void); HLSRecSegment(const HLSRecSegment& rhs); - HLSRecSegment(int seq, std::chrono::seconds duration, QString title, + HLSRecSegment(int seq, std::chrono::milliseconds duration, QString title, QUrl uri); - HLSRecSegment(int seq, std::chrono::seconds duration, QString title, - QUrl uri, const QString& current_key_path); ~HLSRecSegment(); HLSRecSegment& operator=(const HLSRecSegment& rhs); @@ -28,30 +32,34 @@ class HLSRecSegment int64_t Sequence(void) const { return m_sequence; } QString Title(void) const { return m_title; } QUrl Url(void) const { return m_url; } - std::chrono::seconds Duration(void) const { return m_duration; } + std::chrono::milliseconds Duration(void) const { return m_duration; } QString toString(void) const; #ifdef USING_LIBCRYPTO - bool DownloadKey(void); - bool DecodeData(const uint8_t *IV, QByteArray& data); - bool HasKeyPath(void) const { return !m_psz_key_path.isEmpty(); } - QString KeyPath(void) const { return m_psz_key_path; } - void SetKeyPath(const QString& path) { m_psz_key_path = path; } -#endif + public: + bool SetAESIV(QString line); + bool IVLoaded(void) const { return m_ivLoaded; } - protected: - int64_t m_sequence {0}; // unique sequence number - std::chrono::seconds m_duration {0s}; // segment duration - uint64_t m_bitrate {0}; // bitrate of segment's content (bits per second) - QString m_title; // human-readable informative title of - // the media segment + QByteArray AESIV(void) { return m_aesIV; } + bool HasKeyPath(void) const { return !m_keypath.isEmpty(); } + QString KeyPath(void) const { return m_keypath; } + void SetKeyPath(const QString& x) { m_keypath = x; } +#endif // USING_LIBCRYPTO + protected: + int64_t m_sequence {0}; // unique sequence number + std::chrono::milliseconds m_duration {0ms}; // segment duration + uint64_t m_bitrate {0}; // bitrate of segment's content (bits per second) + QString m_title; // human-readable informative title of the media segment QUrl m_url; #ifdef USING_LIBCRYPTO - QString m_psz_key_path; // URL key path -#endif + private: + QString m_keypath; // URL path of the encrypted key + bool m_ivLoaded {false}; + QByteArray m_aesIV {AES_BLOCK_SIZE,0}; // IV used when decypher the block +#endif // USING_LIBCRYPTO }; diff --git a/mythtv/libs/libmythtv/recorders/HLS/HLSStream.cpp b/mythtv/libs/libmythtv/recorders/HLS/HLSStream.cpp index 59e276b4019..1e5fa48d9e1 100644 --- a/mythtv/libs/libmythtv/recorders/HLS/HLSStream.cpp +++ b/mythtv/libs/libmythtv/recorders/HLS/HLSStream.cpp @@ -22,13 +22,12 @@ HLSRecStream::HLSRecStream(int seq, uint64_t bitrate, QString m3u8_url, QString HLSRecStream::~HLSRecStream(void) { LOG(VB_RECORD, LOG_DEBUG, LOC + "dtor"); - #ifdef USING_LIBCRYPTO AESKeyMap::iterator Iaes; for (Iaes = m_aesKeys.begin(); Iaes != m_aesKeys.end(); ++Iaes) delete *Iaes; -#endif +#endif // USING_LIBCRYPTO } QString HLSRecStream::toString(void) const @@ -65,6 +64,9 @@ bool HLSRecStream::DownloadKey(MythSingleDownload& downloader, } AES_set_decrypt_key((const unsigned char*)key.constData(), 128, aeskey); + + LOG(VB_RECORD, LOG_DEBUG, LOC + "Downloaded AES key"); + return true; } @@ -72,6 +74,13 @@ bool HLSRecStream::DecodeData(MythSingleDownload& downloader, const QByteArray& IV, const QString& keypath, QByteArray& data, int64_t sequence) { + LOG(VB_RECORD, LOG_DEBUG, QString("HLSRecStream::DecodeData ") + + QString(" %1").arg(!IV.isEmpty() ? + QString(" IV[%1]:%2").arg(IV.size()-1).arg(IV[IV.size()-1]) : QString(" IV isEmpty")) + + QString(" IV.size():%1").arg(IV.size()) + + QString(" keypath:%1..%2").arg(keypath.left(20)).arg(keypath.right(20)) + + QString(" sequence:%1").arg(sequence)); + AESKeyMap::iterator Ikey = m_aesKeys.find(keypath); if (Ikey == m_aesKeys.end()) { @@ -157,32 +166,3 @@ bool HLSRecStream::operator>(const HLSRecStream &b) const { return this->Bitrate() > b.Bitrate(); } - -#ifdef USING_LIBCRYPTO -bool HLSRecStream::SetAESIV(QString line) -{ - /* - * If the EXT-X-KEY tag has the IV attribute, implementations MUST use - * the attribute value as the IV when encrypting or decrypting with that - * key. The value MUST be interpreted as a 128-bit hexadecimal number - * and MUST be prefixed with 0x or 0X. - */ - if (!line.startsWith(QLatin1String("0x"), Qt::CaseInsensitive)) - return false; - if (line.size() % 2) - { - // not even size, pad with front 0 - line.insert(2, QLatin1String("0")); - } -#if QT_VERSION < QT_VERSION_CHECK(6,0,0) - int padding = std::max(0, AES_BLOCK_SIZE - (line.size() - 2)); -#else - int padding = std::max(0LL, AES_BLOCK_SIZE - (line.size() - 2)); -#endif - QByteArray ba = QByteArray(padding, 0x0); - ba.append(QByteArray::fromHex(QByteArray(line.toLatin1().constData() + 2))); - m_aesIV = ba; - m_ivLoaded = true; - return true; -} -#endif // USING_LIBCRYPTO diff --git a/mythtv/libs/libmythtv/recorders/HLS/HLSStream.h b/mythtv/libs/libmythtv/recorders/HLS/HLSStream.h index 31c7cd396a6..0fec2915fbc 100644 --- a/mythtv/libs/libmythtv/recorders/HLS/HLSStream.h +++ b/mythtv/libs/libmythtv/recorders/HLS/HLSStream.h @@ -41,11 +41,14 @@ class HLSRecStream void SetCurrentByteRate(uint64_t byterate) { m_curByteRate = byterate; } bool Cache(void) const { return m_cache; } void SetCache(bool x) { m_cache = x; } + void SetDateTime(QDateTime &dt) { m_dateTime = dt; } bool Live(void) const { return m_live; } void SetLive(bool x) { m_live = x; } QString M3U8Url(void) const { return m_m3u8Url; } QString SegmentBaseUrl(void) const { return m_segmentBaseUrl; } void SetSegmentBaseUrl(const QString &n) { m_segmentBaseUrl = n; } + QString MapUri(void) const { return m_mapUri; } + void SetMapUri(const QString& x) { m_mapUri = x; } std::chrono::seconds Duration(void) const; uint NumCachedSegments(void) const; @@ -67,11 +70,6 @@ class HLSRecStream bool DecodeData(MythSingleDownload& downloader, const QByteArray& IV, const QString& keypath, QByteArray& data, int64_t sequence); - bool SetAESIV(QString line); - bool IVLoaded(void) const { return m_ivLoaded; } - - QByteArray AESIV(void) { return m_aesIV; } - void SetKeyPath(const QString& x) { m_keypath = x; } #endif // USING_LIBCRYPTO protected: @@ -79,12 +77,12 @@ class HLSRecStream private: int m_id; // program id - int m_version {1}; // protocol version should be 1 + int m_version; // HLS protocol version std::chrono::seconds m_targetDuration {-1s}; // maximum duration per segment uint64_t m_curByteRate {0}; - uint64_t m_bitrate; // bitrate of stream content (bits per second) - std::chrono::seconds m_duration {0s}; // duration of the stream - int m_discontSeq {0}; // Discontinuity sequence number + uint64_t m_bitrate {0}; // bitrate of stream content (bits per second) + std::chrono::seconds m_duration {0s}; // duration of the stream + int m_discontSeq {0}; // discontinuity sequence number bool m_live {true}; int64_t m_bandwidth {0}; // measured average download bandwidth (bits/second) double m_sumBandwidth {0.0}; @@ -94,14 +92,14 @@ class HLSRecStream QString m_segmentBaseUrl; // uri to base for relative segments (m3u8 redirect target) mutable QMutex m_lock; bool m_cache {false}; // allow caching + QDateTime m_dateTime; // #EXT-X-PROGRAM-DATE-TIME int m_retries {0}; + QString m_mapUri; // URI of Media Initialisation Sequence + #ifdef USING_LIBCRYPTO private: - QString m_keypath; // URL path of the encrypted key - bool m_ivLoaded {false}; - QByteArray m_aesIV {AES_BLOCK_SIZE,0};// IV used when decypher the block - AESKeyMap m_aesKeys; // AES-128 keys by path + AESKeyMap m_aesKeys; // AES-128 keys by path #endif // USING_LIBCRYPTO }; diff --git a/mythtv/libs/libmythtv/recorders/hlsstreamhandler.cpp b/mythtv/libs/libmythtv/recorders/hlsstreamhandler.cpp index dc6811486aa..2d85b6b19b1 100644 --- a/mythtv/libs/libmythtv/recorders/hlsstreamhandler.cpp +++ b/mythtv/libs/libmythtv/recorders/hlsstreamhandler.cpp @@ -16,8 +16,8 @@ #define LOC QString("HLSSH[%1](%2): ").arg(m_inputId).arg(m_device) // BUFFER_SIZE is a multiple of TS_SIZE -static constexpr qint64 TS_SIZE { 188 }; -static constexpr qint64 BUFFER_SIZE { 512 * TS_SIZE }; +static constexpr qint64 TS_SIZE { 188 }; +static constexpr qint64 BUFFER_SIZE { 2048 * TS_SIZE }; QMap<QString,HLSStreamHandler*> HLSStreamHandler::s_hlshandlers; QMap<QString,uint> HLSStreamHandler::s_hlshandlers_refcnt; @@ -100,14 +100,14 @@ void HLSStreamHandler::Return(HLSStreamHandler* & ref, int inputid) HLSStreamHandler::HLSStreamHandler(const IPTVTuningData& tuning, int inputid) : IPTVStreamHandler(tuning, inputid) { - LOG(VB_GENERAL, LOG_INFO, LOC + "ctor"); + LOG(VB_RECORD, LOG_DEBUG, LOC + "ctor"); m_hls = new HLSReader(m_inputId); m_readbuffer = new uint8_t[BUFFER_SIZE]; } HLSStreamHandler::~HLSStreamHandler(void) { - LOG(VB_CHANNEL, LOG_INFO, LOC + "dtor"); + LOG(VB_RECORD, LOG_DEBUG, LOC + "dtor"); Stop(); delete m_hls; delete[] m_readbuffer; @@ -122,7 +122,7 @@ void HLSStreamHandler::run(void) int nil_cnt = 0; std::chrono::milliseconds open_sleep = 500ms; - LOG(VB_GENERAL, LOG_INFO, LOC + "run() -- begin"); + LOG(VB_RECORD, LOG_INFO, LOC + "run() -- begin"); SetRunning(true, false, false); @@ -223,5 +223,5 @@ void HLSStreamHandler::run(void) SetRunning(false, false, false); RunEpilog(); - LOG(VB_GENERAL, LOG_INFO, LOC + "run() -- done"); + LOG(VB_RECORD, LOG_DEBUG, LOC + "run() -- done"); }