31#include <QtAlgorithms>
32#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
33#include <QStringConverter>
47#include "libavformat/avio.h"
52#include <openssl/aes.h>
53#include <openssl/evp.h>
54using aesiv_array = std::array<uint8_t,AES_BLOCK_SIZE>;
59 std::array<uint8_t,AES128_KEY_SIZE>
key;
64#define LOC QString("HLSBuffer: ")
81 QByteArray ba = uri.toLatin1();
82 QUrl url = QUrl::fromEncoded(ba);
83 return url.toString();
86static QString
relative_URI(
const QString &surl,
const QString &spath)
88 QUrl url = QUrl(surl);
89 QUrl path = QUrl(spath);
91 if (!path.isRelative())
95 return url.resolved(path).toString();
98static std::chrono::microseconds
mdate(
void)
100 return nowAsDuration<std::chrono::microseconds>();
103static bool downloadURL(
const QString &url, QByteArray *buffer, QString &finalURL)
106 return mdm->
download(url, buffer,
false, &finalURL);
132 HLSSegment(
const std::chrono::seconds mduration,
const int id, QString title,
133 QString uri, [[maybe_unused]] QString current_key_path)
141 m_pszKeyPath = std::move(current_key_path);
165 m_pszKeyPath = rhs.m_pszKeyPath;
166 memcpy(&m_aeskey, &(rhs.m_aeskey),
sizeof(m_aeskey));
167 m_keyloaded = rhs.m_keyloaded;
223 QMutexLocker lock(&
m_lock);
238 uint32_t
Read(uint8_t *buffer, int32_t length,
FILE *fd =
nullptr)
241 length = std::min(length, left);
242 if (buffer !=
nullptr)
279 return m_data.constData();
283 int DownloadKey(
void)
290 if (!ret || key.size() != AES_BLOCK_SIZE)
294 LOG(VB_PLAYBACK, LOG_ERR,
LOC +
295 QString(
"The AES key loaded doesn't have the right size (%1)")
300 LOG(VB_PLAYBACK, LOG_ERR,
LOC +
"Failed to download AES key");
312 static int Decrypt(
unsigned char *ciphertext,
int ciphertext_len,
unsigned char *key,
313 unsigned char *iv,
unsigned char *plaintext)
317 int plaintext_len = 0;
320 EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
323 LOG(VB_RECORD, LOG_ERR,
LOC +
"Failed to create and initialize cipher context");
334 if(1 != EVP_DecryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, key, iv))
336 LOG(VB_RECORD, LOG_ERR,
LOC +
"Failed to initialize decryption operation");
344 if(1 != EVP_DecryptUpdate(ctx, plaintext, &len, ciphertext, ciphertext_len))
346 LOG(VB_RECORD, LOG_ERR,
LOC +
"Failed to decrypt");
355 if(1 != EVP_DecryptFinal_ex(ctx, plaintext + len, &len))
357 LOG(VB_RECORD, LOG_ERR,
LOC +
"Failed to finalize decryption" +
358 QString(
" len:%1").arg(len) +
359 QString(
" plaintext_len:%1").arg(plaintext_len) );
362 plaintext_len += len;
365 EVP_CIPHER_CTX_free(ctx);
367 return plaintext_len;
370 int DecodeData(
const aesiv_array IV,
bool iv_valid)
374 auto *decrypted_data =
new uint8_t[
m_data.size()];
384 iv[15] =
m_id & 0xff;
385 iv[14] = (
m_id >> 8) & 0xff;
386 iv[13] = (
m_id >> 16) & 0xff;
387 iv[12] = (
m_id >> 24) & 0xff;
391 std::copy(IV.cbegin(), IV.cend(), iv.begin());
394 int aeslen =
m_data.size() & ~0xf;
395 if (aeslen !=
m_data.size())
397 LOG(VB_RECORD, LOG_WARNING,
LOC +
398 QString(
"Data size %1 not multiple of 16 bytes, rounding to %2")
399 .arg(
m_data.size()).arg(aeslen));
402 int plaintext_len = Decrypt((
unsigned char*)
m_data.constData(), aeslen, m_aeskey.key.data(),
403 iv.data(), decrypted_data);
405 LOG(VB_RECORD, LOG_INFO,
LOC +
406 QString(
"Segment data.size()):%1 plaintext_len:%2")
407 .arg(
m_data.size()).arg(plaintext_len));
409 m_data = QByteArray(
reinterpret_cast<char*
>(decrypted_data), plaintext_len);
410 delete[] decrypted_data;
415 bool HasKeyPath(
void)
const
417 return !m_pszKeyPath.isEmpty();
420 bool KeyLoaded(
void)
const
425 QString KeyPath(
void)
const
430 void SetKeyPath(
const QString &path)
437 memcpy(&m_aeskey, &(segment.m_aeskey),
sizeof(m_aeskey));
438 m_keyloaded = segment.m_keyloaded;
442 bool m_keyloaded {
false};
443 QString m_pszKeyPath;
464 HLSStream(
const int mid,
const uint64_t bitrate, QString uri)
489 for (
const auto & segment : std::as_const(
m_segments))
509 m_keypath = rhs.m_keypath;
510 m_ivloaded = rhs.m_ivloaded;
511 m_aesIv = rhs.m_aesIv;
540 QMutexLocker lock(&
m_lock);
545 for (
int i = 0; i < count; i++)
549 if (segment->
Size() > 0)
551 size += (int64_t)segment->
Size();
565 QMutexLocker lock(&
m_lock);
590 if ((wanted < 0) || (wanted >= count))
600 for (
int n = 0; n < count; n++)
603 if (segment ==
nullptr)
605 if (segment->
Id() ==
id)
607 if (segnum !=
nullptr)
617 void AddSegment(
const std::chrono::seconds duration,
const QString &title,
const QString &uri)
619 QMutexLocker lock(&
m_lock);
625 auto *segment =
new HLSSegment(duration,
id, title, psz_uri, m_keypath);
632 QMutexLocker lock(&
m_lock);
641 for (
int n = 0; n < count; n++)
654 QMutexLocker lock(&
m_lock);
656 if (segment !=
nullptr)
667 for (
auto it = table.begin(); it != table.end(); ++it)
678 if (segment ==
nullptr)
689 LOG(VB_PLAYBACK, LOG_DEBUG,
LOC +
690 QString(
"started download of segment %1 [%2/%3] using stream %4")
698 auto estimated = std::chrono::seconds(size / bandwidth);
699 if (estimated > segment->
Duration())
701 LOG(VB_PLAYBACK, LOG_INFO,
LOC +
702 QString(
"downloading of segment %1 [id:%2] will take %3s, "
703 "which is longer than its playback (%4s) at %5bit/s")
706 .arg(estimated.count())
712 std::chrono::microseconds start =
mdate();
715 LOG(VB_PLAYBACK, LOG_ERR,
LOC +
716 QString(
"downloaded segment %1 [id:%2] from stream %3 failed")
717 .arg(segnum).arg(segment->
Id()).arg(
m_id));
722 std::chrono::microseconds downloadduration =
mdate() - start;
727 ((double)segment->
Duration().count()));
732 if (segment->HasKeyPath())
735 if (!segment->KeyLoaded())
737 if (ManageSegmentKeys() !=
RET_OK)
739 LOG(VB_PLAYBACK, LOG_ERR,
LOC +
740 "couldn't retrieve segment AES-128 key");
745 if (segment->DecodeData(m_aesIv, m_ivloaded) !=
RET_OK)
754 downloadduration = std::max(1us, downloadduration);
755 bandwidth = segment->
Size() * 8ULL * 1000000ULL / downloadduration.count();
756 LOG(VB_PLAYBACK, LOG_DEBUG,
LOC +
757 QString(
"downloaded segment %1 [id:%2] took %3ms for %4 bytes: bandwidth:%5kiB/s")
760 .arg(duration_cast<std::chrono::milliseconds>(downloadduration).count())
761 .arg(segment->
Size())
762 .arg(bandwidth / 8192.0));
828 QMutexLocker lock(&
m_lock);
835 QMutexLocker lock(&
m_lock);
836 for (
const auto & segment : std::as_const(
m_segments))
840 segment->CancelDownload();
850 int ManageSegmentKeys()
const
856 for (
int i = 0; i < count; i++)
862 if (!seg->HasKeyPath())
864 if (seg->KeyLoaded())
869 if (prev_seg && prev_seg->KeyLoaded() &&
870 (seg->KeyPath() == prev_seg->KeyPath()))
872 seg->CopyAESKey(*prev_seg);
875 if (seg->DownloadKey() !=
RET_OK)
880 bool SetAESIV(QString line)
888 if (!line.startsWith(QLatin1String(
"0x"), Qt::CaseInsensitive))
893 line.insert(2, QLatin1String(
"0"));
895#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
896 int padding = std::max(0, AES_BLOCK_SIZE - (line.size() - 2));
898 int padding = std::max(
static_cast<qsizetype
>(0), AES_BLOCK_SIZE - (line.size() - 2));
900 QByteArray ba = QByteArray(padding, 0x0);
901 ba.append(QByteArray::fromHex(QByteArray(line.toLatin1().constData() + 2)));
902 std::copy(ba.cbegin(), ba.cend(), m_aesIv.begin());
906 aesiv_array AESIV(
void)
910 void SetKeyPath(
const QString &x)
917 bool m_ivloaded {
false};
918 aesiv_array m_aesIv {0};
959 QMutexLocker lock(&
m_lock);
964 QMutexLocker lock(&
m_lock);
969 QMutexLocker lock(&
m_lock);
974 QMutexLocker lock(&
m_lock);
979 QMutexLocker lock(&
m_lock);
1005 for (
int i = 0; i < streams; i++)
1018 QMutexLocker lock(&
m_lock);
1023 QMutexLocker lock(&
m_lock);
1056 for (
int i = from; i < from + count; i++)
1085 QMutexLocker lock(&
m_lock);
1092 QMutexLocker lock(&
m_lock);
1097 QMutexLocker lock(&
m_lock);
1132 void WaitForSignal(std::chrono::milliseconds time = std::chrono::milliseconds::max())
1181 if ((!hls->
Live() && (playsegment < dnldsegment -
m_buffer)) ||
1221 LOG(VB_PLAYBACK, LOG_DEBUG,
LOC +
1222 QString(
"download failed, retry #%1").arg(retries));
1241 LOG(VB_PLAYBACK, LOG_DEBUG,
LOC +
1242 QString(
"download completed, %1 segments ahead")
1249 if (newstream >= 0 && newstream !=
m_stream)
1251 LOG(VB_PLAYBACK, LOG_INFO,
LOC +
1252 QString(
"switching to %1 bitrate %2 stream; changing "
1253 "from stream %3 to stream %4")
1254 .arg(bw >= hls->
Bitrate() ?
"faster" :
"lower")
1255 .arg(bw).arg(
m_stream).arg(newstream));
1277 uint64_t bw = bandwidth;
1278 uint64_t bw_candidate = 0;
1281 for (
int n = 0; n < count; n++)
1289 if (hls->
Id() == progid)
1292 (bw_candidate < hls->Bitrate()))
1294 LOG(VB_PLAYBACK, LOG_DEBUG,
LOC +
1295 QString(
"candidate stream %1 bitrate %2 >= %3")
1296 .arg(n).arg(bw).arg(hls->
Bitrate()));
1297 bw_candidate = hls->
Bitrate();
1302 bandwidth = bw_candidate;
1334 QStringList listurls;
1335 for (
int i = 0; i < streams; i++)
1340 listurls.append(hls->
Url());
1356 void WaitForSignal(std::chrono::milliseconds time = std::chrono::milliseconds::max())
1376 bool live = hls ? hls->
Live() :
false;
1378 double factor = live ? 1.0 : 2.0;
1380 QWaitCondition mcond;
1387 LOG(VB_PLAYBACK, LOG_ERR,
LOC +
1388 "StreamWorker not running, aborting live playback");
1396 std::chrono::milliseconds waittime = std::max(100ms,
m_wakeup);
1397 LOG(VB_PLAYBACK, LOG_DEBUG,
LOC +
1398 QString(
"PlayListWorker refreshing in %1s")
1399 .arg(duration_cast<std::chrono::seconds>(waittime).count()));
1418 LOG(VB_PLAYBACK, LOG_ERR,
LOC +
1419 QString(
"reloading the playlist failed after %1 attempts."
1444 LOG(VB_PLAYBACK, LOG_ERR,
LOC +
1445 "unable to retrieve current stream, aborting live playback");
1451 m_wakeup = duration_cast<std::chrono::milliseconds>(
1466 LOG(VB_PLAYBACK, LOG_INFO,
LOC +
"reloading HLS live meta playlist");
1470 LOG(VB_PLAYBACK, LOG_ERR,
LOC +
"reloading playlist failed");
1476 int count = streams->size();
1477 for (
int n = 0; n < count; n++)
1480 if (hls_new ==
nullptr)
1484 if (hls_old ==
nullptr)
1487 LOG(VB_PLAYBACK, LOG_INFO,
LOC +
1488 QString(
"new HLS stream appended (id=%1, bitrate=%2)")
1489 .arg(hls_new->
Id()).arg(hls_new->
Bitrate()));
1493 LOG(VB_PLAYBACK, LOG_ERR,
LOC +
1494 QString(
"failed updating HLS stream (id=%1, bandwidth=%2)")
1495 .arg(hls_new->
Id()).arg(hls_new->
Bitrate()));
1508 LOG(VB_PLAYBACK, LOG_INFO,
LOC +
1509 QString(
"updated hls stream (program-id=%1, bitrate=%2) has %3 segments")
1510 .arg(hls_new->
Id()).arg(hls_new->
Bitrate()).arg(count));
1511 QHash<HLSSegment*,bool> table;
1513 for (
int n = 0; n < count; n++)
1525 if ((
p->Id() != segment->
Id()) ||
1526 (
p->Duration() != segment->
Duration()) ||
1527 (
p->Url() != segment->
Url()))
1529 LOG(VB_PLAYBACK, LOG_WARNING,
LOC +
1530 QString(
"existing segment found with different content - resetting"));
1531 LOG(VB_PLAYBACK, LOG_WARNING,
LOC +
1532 QString(
"- id: new=%1, old=%2")
1533 .arg(
p->Id()).arg(segment->
Id()));
1534 LOG(VB_PLAYBACK, LOG_WARNING,
LOC +
1535 QString(
"- duration: new=%1, old=%2")
1536 .arg(
p->Duration().count()).arg(segment->
Duration().count()));
1537 LOG(VB_PLAYBACK, LOG_WARNING,
LOC +
1538 QString(
"- file: new=%1 old=%2")
1539 .arg(
p->Url(), segment->
Url()));
1545 table.insert(
p,
true);
1558 if ((l->
Id() + 1) !=
p->Id())
1560 LOG(VB_PLAYBACK, LOG_ERR,
LOC +
1561 QString(
"gap in id numbers found: new=%1 expected %2")
1562 .arg(
p->Id()).arg(l->
Id()+1));
1565 LOG(VB_PLAYBACK, LOG_INFO,
LOC +
1566 QString(
"- segment %1 appended")
1569 table.insert(
p,
false);
1592 streams->append(dst);
1666 for (
int i = 0; i < streams->size(); i++)
1689 if (streams ==
nullptr)
1693 int count = streams->size();
1696 if ((wanted < 0) || (wanted >= count))
1698 return streams->at(wanted);
1708 if (streams ==
nullptr)
1712 int count = streams->size();
1722 if (streams ==
nullptr)
1726 int count = streams->size();
1727 for (
int n = 0; n < count; n++)
1733 if ((hls->
Id() == hls_new->
Id()) &&
1758 if (!s || s->size() < 7)
1761 if (!s->startsWith((
const char*)
"#EXTM3U"))
1764 QTextStream stream(s);
1770 QString line = stream.readLine();
1773 LOG(VB_PLAYBACK, LOG_DEBUG,
LOC +
1774 QString(
"IsHTTPLiveStreaming: %1").arg(line));
1775 if (line.startsWith(QLatin1String(
"#EXT-X-TARGETDURATION")) ||
1776 line.startsWith(QLatin1String(
"#EXT-X-MEDIA-SEQUENCE")) ||
1777 line.startsWith(QLatin1String(
"#EXT-X-KEY")) ||
1778 line.startsWith(QLatin1String(
"#EXT-X-ALLOW-CACHE")) ||
1779 line.startsWith(QLatin1String(
"#EXT-X-ENDLIST")) ||
1780 line.startsWith(QLatin1String(
"#EXT-X-STREAM-INF")) ||
1781 line.startsWith(QLatin1String(
"#EXT-X-DISCONTINUITY")) ||
1782 line.startsWith(QLatin1String(
"#EXT-X-VERSION")))
1797 AVIOContext* context =
nullptr;
1798 int ret = avio_open(&context,
filename.toLatin1(), AVIO_FLAG_READ);
1801 std::array<uint8_t,1024> buffer {};
1802 ret = avio_read(context, buffer.data(), buffer.size());
1805 QByteArray ba((
const char*)buffer.data(), ret);
1808 avio_closep(&context);
1815 url.path().endsWith(QLatin1String(
"m3u8"), Qt::CaseInsensitive) ||
1816 url.query( QUrl::FullyEncoded ).contains(QLatin1String(
"m3u8"), Qt::CaseInsensitive);
1824 int p = line.indexOf(QLatin1String(
":"));
1828 QStringList list = line.mid(
p+1).split(
',');
1829 for (
const auto& it : std::as_const(list))
1831 QString arg = it.trimmed();
1832 if (arg.startsWith(attr))
1834 int pos = arg.indexOf(QLatin1String(
"="));
1837 return arg.mid(pos+1);
1849 int p = line.indexOf(QLatin1String(
":"));
1853 for ( ; i < line.size(); i++)
1854 if (!line[i].isNumber())
1858 target = line.mid(
p+1, i -
p - 1).toInt();
1863 int &duration, QString &title)
1874 int p = line.indexOf(QLatin1String(
":"));
1878 QStringList list = line.mid(
p+1).split(
',');
1885 const QString& val = list[0];
1890 duration = val.toInt(&ok);
1900 double d = val.toDouble(&ok);
1906 if ((
d) - ((
int)
d) >= 0.5)
1907 duration = ((int)
d) + 1;
1909 duration = ((int)
d);
1912 if (list.size() >= 2)
1932 LOG(VB_PLAYBACK, LOG_ERR,
LOC +
"expected #EXT-X-TARGETDURATION:<s>");
1951 LOG(VB_PLAYBACK, LOG_INFO,
LOC +
"#EXT-X-STREAM-INF: expected PROGRAM-ID=<value>, using -1");
1962 LOG(VB_PLAYBACK, LOG_ERR,
LOC +
"#EXT-X-STREAM-INF: expected BANDWIDTH=<value>");
1965 uint64_t bw = attr.toInt();
1969 LOG(VB_PLAYBACK, LOG_ERR,
LOC +
"#EXT-X-STREAM-INF: bandwidth cannot be 0");
1973 LOG(VB_PLAYBACK, LOG_INFO,
LOC +
1974 QString(
"bandwidth adaptation detected (program-id=%1, bandwidth=%2")
1996 LOG(VB_PLAYBACK, LOG_ERR,
LOC +
"expected #EXT-X-MEDIA-SEQUENCE:<s>");
2002 LOG(VB_PLAYBACK, LOG_ERR,
LOC +
2003 QString(
"EXT-X-MEDIA-SEQUENCE already present in playlist (new=%1, old=%2)")
2023 LOG(VB_PLAYBACK, LOG_ERR,
LOC +
"#EXT-X-KEY: expected METHOD=<value>");
2027 if (attr.startsWith(QLatin1String(
"NONE")))
2032 LOG(VB_PLAYBACK, LOG_ERR,
LOC +
"#EXT-X-KEY: URI not expected");
2041 LOG(VB_PLAYBACK, LOG_ERR,
LOC +
"#EXT-X-KEY: IV not expected");
2047 else if (attr.startsWith(QLatin1String(
"AES-128")))
2053 LOG(VB_PLAYBACK, LOG_INFO,
LOC +
2054 "playback of AES-128 encrypted HTTP Live media detected.");
2060 LOG(VB_PLAYBACK, LOG_ERR,
LOC +
2061 "#EXT-X-KEY: URI not found for encrypted HTTP Live media in AES-128");
2066 hls->SetKeyPath(
decoded_URI(uri.remove(QChar(QLatin1Char(
'"')))));
2069 if (!iv.isNull() && !hls->SetAESIV(iv))
2071 LOG(VB_PLAYBACK, LOG_ERR,
LOC +
"invalid IV");
2079 LOG(VB_PLAYBACK, LOG_ERR,
LOC +
2080 "invalid encryption type, only NONE "
2082 "and AES-128 are supported"
2101 LOG(VB_PLAYBACK, LOG_WARNING,
LOC +
2102 QString(
"tag not supported: #EXT-X-PROGRAM-DATE-TIME %1")
2118 int pos = line.indexOf(QLatin1String(
":"));
2121 QString answer = line.mid(pos+1, 3);
2122 if (answer.size() < 2)
2124 LOG(VB_PLAYBACK, LOG_ERR,
LOC +
"#EXT-X-ALLOW-CACHE, ignoring ...");
2127 hls->
SetCache(!answer.startsWith(QLatin1String(
"NO")));
2147 LOG(VB_PLAYBACK, LOG_ERR,
LOC +
2148 "#EXT-X-VERSION: no protocol version found, should be version 1.");
2152 if (version <= 0 || version > 3)
2154 LOG(VB_PLAYBACK, LOG_ERR,
LOC +
2155 QString(
"#EXT-X-VERSION should be version 1, 2 or 3 iso %1")
2170 LOG(VB_PLAYBACK, LOG_INFO,
LOC +
"video on demand (vod) mode");
2177 LOG(VB_PLAYBACK, LOG_DEBUG,
LOC + QString(
"#EXT-X-DISCONTINUITY %1").arg(line));
2190 if (streams ==
nullptr)
2194 QTextStream stream(*buffer);
2195#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
2196 stream.setCodec(
"UTF-8");
2198 stream.setEncoding(QStringConverter::Utf8);
2201 QString line = stream.readLine();
2205 if (!line.startsWith(QLatin1String(
"#EXTM3U")))
2207 LOG(VB_PLAYBACK, LOG_ERR,
LOC +
"missing #EXTM3U tag .. aborting");
2213 int p = buffer->indexOf(
"#EXT-X-VERSION:");
2217 QString psz_version = stream.readLine();
2218 if (psz_version.isNull())
2223 LOG(VB_GENERAL, LOG_WARNING,
LOC +
2224 "#EXT-X-VERSION: no protocol version found, assuming version 1.");
2230 bool meta = buffer->indexOf(
"#EXT-X-STREAM-INF") >= 0;
2236 LOG(VB_PLAYBACK, LOG_INFO,
LOC +
"Meta playlist");
2242 line = stream.readLine();
2246 if (line.startsWith(QLatin1String(
"#EXT-X-STREAM-INF")))
2249 QString uri = stream.readLine();
2255 if (uri.startsWith(QLatin1String(
"#")))
2257 LOG(VB_GENERAL, LOG_INFO,
LOC +
2258 QString(
"Skipping invalid stream-inf: %1")
2271 LOG(VB_GENERAL, LOG_INFO,
LOC +
2272 QString(
"Skipping invalid stream, couldn't download: %1")
2277 streams->append(hls);
2303 streams->append(hls);
2305 p = buffer->indexOf(
"#EXT-X-TARGETDURATION:");
2309 QString psz_duration = stream.readLine();
2310 if (psz_duration.isNull())
2317 LOG(VB_PLAYBACK, LOG_INFO,
LOC +
2318 QString(
"%1 Playlist HLS protocol version: %2")
2324 std::chrono::seconds segment_duration = -1s;
2329 line = stream.readLine();
2332 LOG(VB_PLAYBACK, LOG_DEBUG,
LOC + QString(
"ParseM3U8: %1")
2335 if (line.startsWith(QLatin1String(
"#EXTINF")))
2339 segment_duration = std::chrono::seconds(
tmp);
2341 else if (line.startsWith(QLatin1String(
"#EXT-X-TARGETDURATION")))
2345 else if (line.startsWith(QLatin1String(
"#EXT-X-MEDIA-SEQUENCE")))
2349 else if (line.startsWith(QLatin1String(
"#EXT-X-KEY")))
2353 else if (line.startsWith(QLatin1String(
"#EXT-X-PROGRAM-DATE-TIME")))
2357 else if (line.startsWith(QLatin1String(
"#EXT-X-ALLOW-CACHE")))
2361 else if (line.startsWith(QLatin1String(
"#EXT-X-DISCONTINUITY")))
2365 else if (line.startsWith(QLatin1String(
"#EXT-X-VERSION")))
2371 else if (line.startsWith(QLatin1String(
"#EXT-X-ENDLIST")))
2375 else if (!line.startsWith(QLatin1String(
"#")) && !line.isEmpty())
2378 segment_duration = -1s;
2394 std::chrono::microseconds starttime =
mdate();
2395 LOG(VB_PLAYBACK, LOG_INFO,
LOC +
2396 QString(
"Starting Prefetch for %2 segments")
2408 LOG(VB_PLAYBACK, LOG_INFO,
LOC + QString(
"Finished Prefetch (%1s)")
2409 .arg(duration_cast<std::chrono::seconds>(
mdate() - starttime).count()));
2418 bool live = hls->
Live();
2423 LOG(VB_PLAYBACK, LOG_WARNING,
LOC +
"playback will stall");
2428 LOG(VB_PLAYBACK, LOG_WARNING,
LOC +
"playback in danger of stalling");
2433 LOG(VB_PLAYBACK, LOG_WARNING,
LOC +
"playback will exit soon, starving for data");
2453 LOG(VB_PLAYBACK, LOG_WARNING,
LOC +
2454 LOC + QString(
"waiting to get segment %1")
2457 while (!
m_error && (stream < 0) && (retries < 10))
2471 LOG(VB_PLAYBACK, LOG_DEBUG,
LOC +
2472 QString(
"GetSegment %1 [%2] stream[%3] (bitrate:%4)")
2473 .arg(segnum).arg(segment->
Id()).arg(stream).arg(hls->
Bitrate()));
2501 std::chrono::seconds wanted_duration = 0s;
2509 if (segment ==
nullptr)
2514 LOG(VB_PLAYBACK, LOG_WARNING,
LOC +
2515 QString(
"EXTINF:%1 duration is larger than EXT-X-TARGETDURATION:%2")
2519 wanted_duration += segment->
Duration();
2524 segid = segment->
Id();
2530 LOG(VB_PLAYBACK, LOG_DEBUG,
LOC +
2531 QString(
"Choose segment %1/%2 [%3]")
2532 .arg(wanted).arg(count).arg(segid));
2544 if (streams ==
nullptr)
2548 QMap<int,int> idstart;
2550 for (
int n = streams->size() - 1 ; n >= 0; n--)
2557 streams->removeAt(n);
2563 if (!idstart.contains(
id))
2565 idstart.insert(
id,
start);
2567 int start2 = idstart.value(
id);
2570 idstart.insert(
id,
start);
2574 for (
int n = 0; n < streams->size(); n++)
2581 int newstart= idstart.value(
id);
2582 int todrop = newstart - seq;
2590 LOG(VB_PLAYBACK, LOG_ERR,
LOC +
2591 QString(
"stream %1 [id=%2] can't be properly adjusted, ignoring")
2592 .arg(n).arg(hls->
Id()));
2595 for (
int i = 0; i < todrop; i++)
2622 LOG(VB_PLAYBACK, LOG_ERR,
LOC +
2623 QString(
"Couldn't open URL %1").arg(
m_filename));
2628 LOG(VB_PLAYBACK, LOG_INFO,
LOC +
2629 QString(
"Redirected %1 -> %2 ").arg(
m_filename, finalURL));
2634 LOG(VB_PLAYBACK, LOG_ERR,
LOC +
2635 QString(
"%1 isn't a HTTP Live Streaming URL").arg(
m_filename));
2640 LOG(VB_PLAYBACK, LOG_INFO,
LOC + QString(
"HTTP Live Streaming (%1)")
2646 LOG(VB_PLAYBACK, LOG_ERR,
LOC +
2647 QString(
"An error occurred reading M3U8 playlist (%1)").arg(
m_filename));
2675 LOG(VB_PLAYBACK, LOG_ERR,
LOC +
2676 "fetching first segment failed or didn't complete within 10s.");
2702 FILE *fp = fopen(
filename.toLatin1().constData(),
"w");
2710 for (
int i = segstart; i < segend; i++)
2713 if (segment ==
nullptr)
2715 LOG(VB_PLAYBACK, LOG_ERR,
LOC +
2716 QString(
"downloading %1 failed").arg(i));
2720 LOG(VB_PLAYBACK, LOG_INFO,
LOC +
2721 QString(
"download of %1 succeeded")
2723 fwrite(segment->
Data(), segment->
Size(), 1, fp);
2751 bool live = hls ? hls->
Live() :
false;
2764 LOG(VB_PLAYBACK, LOG_INFO,
LOC +
2765 QString(
"pausing until we get sufficient data buffered"));
2788 LOG(VB_PLAYBACK, LOG_DEBUG,
LOC + QString(
"interrupted"));
2812 if (segment ==
nullptr)
2838 LOG(VB_PLAYBACK, LOG_INFO,
LOC +
2839 QString(
"started reading segment %1 [id:%2] from stream %3 (%4 buffered)")
2840 .arg(segnum).arg(segment->
Id()).arg(stream)
2844 int32_t len = segment->
Read((uint8_t*)data + used, i_read,
m_fd);
2852 LOG(VB_PLAYBACK, LOG_DEBUG,
LOC + QString(
"interrupted"));
2876 if (segment ==
nullptr)
2880 auto byterate = (uint64_t)(((
double)segment->
Size()) /
2881 ((double)segment->
Duration().count()));
2883 return (
int)((size * 1000.0) / byterate);
2902 std::chrono::microseconds starting =
mdate();
2935 if (segment !=
nullptr)
2942 if (where > totalsize)
2953 std::chrono::seconds starttime = 0s;
2954 std::chrono::seconds endtime = 0s;
2964 if (segment ==
nullptr)
2971 if (postime < endtime)
2976 starttime = endtime;
2989 if (hls->
Live() && (segnum >= count - 1 || segnum < m_playback->Segment()) &&
3004 LOG(VB_PLAYBACK, LOG_INFO,
LOC +
3005 QString(
"seek to segment %1").arg(segnum));
3017 LOG(VB_PLAYBACK, LOG_DEBUG,
LOC + QString(
"interrupted"));
3026 LOG(VB_PLAYBACK, LOG_ERR,
LOC +
3027 QString(
"seek error: segment %1 should have been downloaded, but didn't."
3028 " Playback will stall")
3033 if (segment ==
nullptr)
3039 int32_t skip = ((postime - starttime) * segment->
Size()) / segment->
Duration();
3040 segment->
Read(
nullptr, skip);
3042 LOG(VB_PLAYBACK, LOG_INFO,
LOC + QString(
"seek completed in %1s")
3043 .arg(duration_cast<std::chrono::seconds>(
mdate() - starting).count()));
3062 QMutexLocker lock(&
m_lock);
3065 LOG(VB_PLAYBACK, LOG_DEBUG,
LOC + QString(
"requesting interrupt"));
3071 QMutexLocker lock(&
m_lock);
3074 LOG(VB_PLAYBACK, LOG_DEBUG,
LOC + QString(
"requesting restart"));
static constexpr uint8_t AES128_KEY_SIZE
void SetOffset(uint64_t val)
HLSPlayback(void)=default
uint64_t Offset(void) const
void AddOffset(uint64_t val)
bool OpenFile(const QString &lfilename, std::chrono::milliseconds retry_ms=kDefaultOpenTimeout) override
Opens an HTTP Live Stream for reading.
void SanitizeStreams(StreamsList *streams=nullptr)
Streams may not be all starting at the same sequence number, so attempt to align their starting seque...
static int ParseDecimalValue(const QString &line, int &target)
Return the decimal argument in a line of type: blah:<decimal> presence of value <decimal> is compulso...
int64_t m_bitrate
assumed bitrate of playback used for the purpose of calculating length and seek position.
int ParseM3U8(const QByteArray *buffer, StreamsList *streams=nullptr)
long long GetRealFileSizeInternal(void) const override
HLSSegment * GetSegment(int segnum, std::chrono::milliseconds timeout=1s)
Retrieve segment [segnum] from any available streams.
void WaitUntilBuffered(void)
Wait until we have enough segments buffered to allow smooth playback Do not wait if VOD and at end of...
static int ParseSegmentInformation(const HLSStream *hls, const QString &line, int &duration, QString &title)
static int ParseMediaSequence(HLSStream *hls, const QString &line)
friend class StreamWorker
long long GetReadPosition(void) const override
int NumSegments(void) const
HLSRingBuffer(const QString &lfilename)
HLSStream * GetLastStream(const StreamsList *streams=nullptr) const
static QString ParseAttributes(const QString &line, const char *attr)
int ChooseSegment(int stream) const
static int ParseDiscontinuity(HLSStream *hls, const QString &line)
static int ParseAllowCache(HLSStream *hls, const QString &line)
HLSStream * FindStream(const HLSStream *hls_new, const StreamsList *streams=nullptr) const
bool IsOpen(void) const override
int DurationForBytes(uint size)
returns an estimated duration in ms for size amount of data returns 0 if we can't estimate the durati...
static bool TestForHTTPLiveStreaming(const QString &filename)
static int ParseTargetDuration(HLSStream *hls, const QString &line)
~HLSRingBuffer() override
HLSStream * GetCurrentStream(void) const
return the stream we are currently streaming from
int ParseKey(HLSStream *hls, const QString &line)
HLSStream * GetStream(int wanted, const StreamsList *streams=nullptr) const
static int ParseVersion(const QString &line, int &version)
PlaylistWorker * m_playlistworker
void FreeStreamsList(QList< HLSStream * > *streams) const
StreamWorker * m_streamworker
HLSStream * GetStreamForSegment(int segnum) const
static int ParseProgramDateTime(HLSStream *hls, const QString &line)
int Prefetch(int count)
Preferetch the first x segments of the stream.
bool m_seektoend
FFmpeg seek to the end of the stream in order to determine the length of the video.
static bool IsHTTPLiveStreaming(QByteArray *s)
int SafeRead(void *data, uint sz) override
int64_t SizeMedia(void) const
long long SeekInternal(long long pos, int whence) override
HLSStream * ParseStreamInformation(const QString &line, const QString &uri) const
static int ParseEndList(HLSStream *hls)
void SanityCheck(const HLSStream *hls) const
friend class PlaylistWorker
int NumStreams(void) const
HLSStream * GetFirstStream(const StreamsList *streams=nullptr) const
bool SaveToDisk(const QString &filename, int segstart=0, int segend=-1)
bool IsSeekingAllowed(void) override
void CancelDownload(void)
std::chrono::seconds Duration(void) const
HLSSegment(const HLSSegment &rhs)
void SetTitle(const QString &x)
QString Title(void) const
HLSSegment(const std::chrono::seconds mduration, const int id, QString title, QString uri, QString current_key_path)
int32_t SizePlayed(void) const
const char * Data(void) const
provides pointer to raw segment data
HLSSegment & operator=(const HLSSegment &rhs)
std::chrono::seconds m_duration
uint32_t Read(uint8_t *buffer, int32_t length, FILE *fd=nullptr)
std::chrono::seconds m_duration
QList< HLSSegment * > m_segments
void RemoveSegment(HLSSegment *segment, bool willdelete=true)
std::chrono::seconds TargetDuration(void) const
int StartSequence(void) const
void SetTargetDuration(std::chrono::seconds x)
int DownloadSegmentData(int segnum, uint64_t &bandwidth, int stream)
void RemoveSegment(int segnum, bool willdelete=true)
void AddSegment(const std::chrono::seconds duration, const QString &title, const QString &uri)
void AppendSegment(HLSSegment *segment)
HLSStream(const int mid, const uint64_t bitrate, QString uri)
void RemoveListSegments(QHash< HLSSegment *, bool > &table)
void SetStartSequence(int x)
static bool IsGreater(const HLSStream *s1, const HLSStream *s2)
bool operator<(const HLSStream &b) const
std::chrono::seconds Duration(void)
bool operator>(const HLSStream &b) const
int NumSegments(void) const
HLSSegment * FindSegment(const int id, int *segnum=nullptr) const
void UpdateWith(const HLSStream &upd)
uint64_t Size(bool force=false)
Return the estimated size of the stream in bytes if a segment hasn't been downloaded,...
std::chrono::seconds m_targetduration
HLSSegment * GetSegment(const int wanted) const
HLSStream(const HLSStream &rhs, bool copy=true)
uint64_t Bitrate(void) const
HLSStream & operator=(const HLSStream &rhs)
This is a wrapper around QThread that does several additional things.
void RunProlog(void)
Sets up a thread, call this if you reimplement run().
static void usleep(std::chrono::microseconds time)
void start(QThread::Priority p=QThread::InheritPriority)
Tell MThread to start running the thread in the near future.
void RunEpilog(void)
Cleans up a thread's resources, call this if you reimplement run().
bool wait(std::chrono::milliseconds time=std::chrono::milliseconds::max())
Wait for the MThread to exit, with a maximum timeout.
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.
int ReloadPlaylist(void)
Reload playlist.
int GetHTTPLiveMetaPlaylist(StreamsList *streams)
QWaitCondition m_waitcond
static int UpdatePlaylist(HLSStream *hls_new, HLSStream *hls)
void WaitForSignal(std::chrono::milliseconds time=std::chrono::milliseconds::max())
std::chrono::milliseconds m_wakeup
void run(void) override
Runs the Qt event loop unless we have a QRunnable, in which case we run the runnable run instead.
PlaylistWorker(HLSRingBuffer *parent, std::chrono::milliseconds wait)
void WaitForSignal(std::chrono::milliseconds time=std::chrono::milliseconds::max())
QWaitCondition m_waitcond
int CurrentLiveBuffer(void)
double AverageNewBandwidth(int64_t bandwidth)
StreamWorker(HLSRingBuffer *parent, int startup, int buffer)
int BandwidthAdaptation(int progid, uint64_t &bandwidth) const
bool GotBufferedSegments(int from, int count) const
check that we have at least [count] segments buffered from position [from]
bool IsAtEnd(bool lock=false)
void run(void) override
Runs the Qt event loop unless we have a QRunnable, in which case we run the runnable run instead.
int CurrentPlaybackBuffer(bool lock=true)
void AddSegmentToStream(int segnum, int stream)
int StreamForSegment(int segmentid, bool lock=true) const
return the stream used to download a particular segment or -1 if it was never downloaded
QMap< int, int > m_segmap
int64_t Bandwidth(void) const
void RemoveSegmentFromStream(int segnum)
static QString decoded_URI(const QString &uri)
static QString relative_URI(const QString &surl, const QString &spath)
static constexpr int8_t PLAYLIST_FAILURE
static std::chrono::microseconds mdate(void)
static bool downloadURL(const QString &url, QByteArray *buffer, QString &finalURL)
static constexpr int8_t PLAYBACK_READAHEAD
static constexpr int PLAYBACK_MINBUFFER
static void cancelURL(const QString &url)
QList< HLSStream * > StreamsList
static const iso6937table * d
std::enable_if_t< std::is_floating_point_v< T >, std::chrono::seconds > secondsFromFloat(T value)
Helper function for convert a floating point number to a duration.
MythDownloadManager * GetMythDownloadManager(void)
Gets the pointer to the MythDownloadManager singleton.
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
MBASE_PUBLIC long long copy(QFile &dst, QFile &src, uint block_size=0)
Copies src file to dst file.
std::array< uint8_t, AES128_KEY_SIZE > key