MythTV master
httplivestreambuffer.cpp
Go to the documentation of this file.
1/*****************************************************************************
2 * httplivestreambuffer.cpp
3 * MythTV
4 *
5 * Created by Jean-Yves Avenard on 6/05/12.
6 * Copyright (c) 2012 Bubblestuff Pty Ltd. All rights reserved.
7 *
8 * Based on httplive.c by Jean-Paul Saman <jpsaman _AT_ videolan _DOT_ org>
9 *
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by
12 * the Free Software Foundation; either version 2 of the License, or
13 * (at your option) any later version.
14 *
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU General Public License for more details.
19 *
20 * You should have received a copy of the GNU General Public License
21 * along with this program; if not, write to the Free Software
22 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
23 *****************************************************************************/
25
26
27// QT
28#include <QObject>
29#include <QString>
30#include <QStringList>
31#include <QtAlgorithms>
32#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
33#include <QStringConverter>
34#endif
35#include <QUrl>
36
37// C++
38#include <algorithm> // for min/max
39#include <array>
40#include <thread>
41
42// libmythbase
43#include "libmythbase/mythconfig.h"
44#include "libmythbase/mthread.h"
47
48extern "C" {
49#include "libavformat/avio.h"
50}
51
52#if CONFIG_LIBCRYPTO
53// encryption related stuff
54#include <openssl/aes.h>
55#include <openssl/evp.h>
56using aesiv_array = std::array<uint8_t,AES_BLOCK_SIZE>;
57
58// 128-bit AES key for HLS segment decryption
59static constexpr uint8_t AES128_KEY_SIZE { 16 };
60struct hls_aes_key_st {
61 std::array<uint8_t,AES128_KEY_SIZE> key;
62};
63using HLS_AES_KEY = struct hls_aes_key_st;
64#endif
65
66#define LOC QString("HLSBuffer: ")
67
68// Constants
69static constexpr int PLAYBACK_MINBUFFER { 2 }; // number of segments to prefetch before playback starts
70static constexpr int8_t PLAYBACK_READAHEAD { 6 }; // number of segments download queue ahead of playback
71static constexpr int8_t PLAYLIST_FAILURE { 6 }; // number of consecutive failures after which
72 // playback will abort
73enum : std::int8_t
74{
76 RET_OK = 0,
77};
78
79/* utility methods */
80
81static QString decoded_URI(const QString &uri)
82{
83 QByteArray ba = uri.toLatin1();
84 QUrl url = QUrl::fromEncoded(ba);
85 return url.toString();
86}
87
88static QString relative_URI(const QString &surl, const QString &spath)
89{
90 QUrl url = QUrl(surl);
91 QUrl path = QUrl(spath);
92
93 if (!path.isRelative())
94 {
95 return spath;
96 }
97 return url.resolved(path).toString();
98}
99
100static std::chrono::microseconds mdate(void)
101{
102 return nowAsDuration<std::chrono::microseconds>();
103}
104
105static bool downloadURL(const QString &url, QByteArray *buffer, QString &finalURL)
106{
108 return mdm->download(url, buffer, false, &finalURL);
109}
110
111static bool downloadURL(const QString &url, QByteArray *buffer)
112{
114 return mdm->download(url, buffer);
115}
116
117static void cancelURL(const QString &url)
118{
120 mdm->cancelDownload(url);
121}
122
123static void cancelURL(const QStringList &urls)
124{
126 mdm->cancelDownload(urls);
127}
128
129/* segment container */
130
132{
133 public:
134 HLSSegment(const std::chrono::seconds mduration, const int id, QString title,
135 QString uri, [[maybe_unused]] QString current_key_path)
136 : m_id(id),
137 m_duration(mduration), // Seconds
138 m_title(std::move(title)),
139 m_url(std::move(uri))
140 {
141#if CONFIG_LIBCRYPTO
142 //NOLINTNEXTLINE(cppcoreguidelines-prefer-member-initializer)
143 m_pszKeyPath = std::move(current_key_path);
144#endif
145 }
146
147 ~HLSSegment() = default;
148
150 {
151 *this = rhs;
152 }
153
155 {
156 if (this == &rhs)
157 return *this;
158 m_id = rhs.m_id;
160 m_bitrate = rhs.m_bitrate;
161 m_url = rhs.m_url;
162 // keep the old data downloaded
163 // m_data = m_data;
164 // m_played = m_played;
165 m_title = rhs.m_title;
166#if CONFIG_LIBCRYPTO
167 m_pszKeyPath = rhs.m_pszKeyPath;
168 memcpy(&m_aeskey, &(rhs.m_aeskey), sizeof(m_aeskey));
169 m_keyloaded = rhs.m_keyloaded;
170#endif
172 return *this;
173 }
174
175 std::chrono::seconds Duration(void) const
176 {
177 return m_duration;
178 }
179
180 int Id(void) const
181 {
182 return m_id;
183 }
184
185 void Lock(void)
186 {
187 m_lock.lock();
188 }
189
190 void Unlock(void)
191 {
192 m_lock.unlock();
193 }
194
195 bool IsEmpty(void) const
196 {
197 return m_data.isEmpty();
198 }
199
200 int32_t Size(void) const
201 {
202 return m_data.size();
203 }
204
205 int Download(void)
206 {
207 // must own lock
208 m_downloading = true;
209 bool ret = downloadURL(m_url, &m_data);
210 m_downloading = false;
211 // didn't succeed, clear buffer
212 if (!ret)
213 {
214 m_data.clear();
215 return RET_ERROR;
216 }
217 return RET_OK;
218 }
219
220 void CancelDownload(void)
221 {
222 if (m_downloading)
223 {
225 QMutexLocker lock(&m_lock);
226 m_downloading = false;
227 }
228 }
229
230 QString Url(void) const
231 {
232 return m_url;
233 }
234
235 int32_t SizePlayed(void) const
236 {
237 return m_played;
238 }
239
240 uint32_t Read(uint8_t *buffer, int32_t length, FILE *fd = nullptr)
241 {
242 int32_t left = m_data.size() - m_played;
243 length = std::min(length, left);
244 if (buffer != nullptr)
245 {
246 memcpy(buffer, m_data.constData() + m_played, length);
247 // write data to disk if required
248 if (fd)
249 {
250 fwrite(m_data.constData() + m_played, length, 1, fd);
251 }
252 }
253 m_played += length;
254 return length;
255 }
256
257 void Reset(void)
258 {
259 m_played = 0;
260 }
261
262 void Clear(void)
263 {
264 m_played = 0;
265 m_data.clear();
266 }
267
268 QString Title(void) const
269 {
270 return m_title;
271 }
272 void SetTitle(const QString &x)
273 {
274 m_title = x;
275 }
279 const char *Data(void) const
280 {
281 return m_data.constData();
282 }
283
284#if CONFIG_LIBCRYPTO
285 int DownloadKey(void)
286 {
287 // must own lock
288 if (m_keyloaded)
289 return RET_OK;
290 QByteArray key;
291 bool ret = downloadURL(m_pszKeyPath, &key);
292 if (!ret || key.size() != AES_BLOCK_SIZE)
293 {
294 if (ret)
295 {
296 LOG(VB_PLAYBACK, LOG_ERR, LOC +
297 QString("The AES key loaded doesn't have the right size (%1)")
298 .arg(key.size()));
299 }
300 else
301 {
302 LOG(VB_PLAYBACK, LOG_ERR, LOC + "Failed to download AES key");
303 }
304 return RET_ERROR;
305 }
306 memcpy(m_aeskey.key.data(), key.constData(), AES128_KEY_SIZE);
307 m_keyloaded = true;
308 return RET_OK;
309 }
310
311 // AES decryption based on OpenSSL example found on
312 // https://wiki.openssl.org/index.php/EVP_Symmetric_Encryption_and_Decryption
313 //
314 static int Decrypt(unsigned char *ciphertext, int ciphertext_len, unsigned char *key,
315 unsigned char *iv, unsigned char *plaintext)
316 {
317 int len = 0;
318
319 int plaintext_len = 0;
320
321 /* Create and initialise the context */
322 EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
323 if(!ctx)
324 {
325 LOG(VB_RECORD, LOG_ERR, LOC + "Failed to create and initialize cipher context");
326 return 0;
327 }
328
329 /*
330 * Initialise the decryption operation. IMPORTANT - ensure you use a key
331 * and IV size appropriate for your cipher
332 * In this example we are using 128 bit AES (i.e. a 128 bit key). The
333 * IV size for *most* modes is the same as the block size. For AES this
334 * is 128 bits
335 */
336 if(1 != EVP_DecryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, key, iv))
337 {
338 LOG(VB_RECORD, LOG_ERR, LOC + "Failed to initialize decryption operation");
339 return 0;
340 }
341
342 /*
343 * Provide the message to be decrypted, and obtain the plaintext output.
344 * EVP_DecryptUpdate can be called multiple times if necessary.
345 */
346 if(1 != EVP_DecryptUpdate(ctx, plaintext, &len, ciphertext, ciphertext_len))
347 {
348 LOG(VB_RECORD, LOG_ERR, LOC + "Failed to decrypt");
349 return 0;
350 }
351 plaintext_len = len;
352
353 /*
354 * Finalise the decryption. Further plaintext bytes may be written at
355 * this stage.
356 */
357 if(1 != EVP_DecryptFinal_ex(ctx, plaintext + len, &len))
358 {
359 LOG(VB_RECORD, LOG_ERR, LOC + "Failed to finalize decryption" +
360 QString(" len:%1").arg(len) +
361 QString(" plaintext_len:%1").arg(plaintext_len) );
362 return 0;
363 }
364 plaintext_len += len;
365
366 /* Clean up */
367 EVP_CIPHER_CTX_free(ctx);
368
369 return plaintext_len;
370 }
371
372 int DecodeData(const aesiv_array IV, bool iv_valid)
373 {
374 /* Decrypt data using AES-128 */
375 aesiv_array iv {};
376 auto *decrypted_data = new uint8_t[m_data.size()];
377 if (!iv_valid)
378 {
379 /*
380 * If the EXT-X-KEY tag does not have the IV attribute, implementations
381 * MUST use the sequence number of the media file as the IV when
382 * encrypting or decrypting that media file. The big-endian binary
383 * representation of the sequence number SHALL be placed in a 16-octet
384 * buffer and padded (on the left) with zeros.
385 */
386 iv[15] = m_id & 0xff;
387 iv[14] = (m_id >> 8) & 0xff;
388 iv[13] = (m_id >> 16) & 0xff;
389 iv[12] = (m_id >> 24) & 0xff;
390 }
391 else
392 {
393 std::ranges::copy(std::as_const(IV), iv.begin());
394 }
395
396 int aeslen = m_data.size() & ~0xf;
397 if (aeslen != m_data.size())
398 {
399 LOG(VB_RECORD, LOG_WARNING, LOC +
400 QString("Data size %1 not multiple of 16 bytes, rounding to %2")
401 .arg(m_data.size()).arg(aeslen));
402 }
403
404 int plaintext_len = Decrypt((unsigned char*)m_data.constData(), aeslen, m_aeskey.key.data(),
405 iv.data(), decrypted_data);
406
407 LOG(VB_RECORD, LOG_INFO, LOC +
408 QString("Segment data.size()):%1 plaintext_len:%2")
409 .arg(m_data.size()).arg(plaintext_len));
410
411 m_data = QByteArray(reinterpret_cast<char*>(decrypted_data), plaintext_len);
412 delete[] decrypted_data;
413
414 return RET_OK;
415 }
416
417 bool HasKeyPath(void) const
418 {
419 return !m_pszKeyPath.isEmpty();
420 }
421
422 bool KeyLoaded(void) const
423 {
424 return m_keyloaded;
425 }
426
427 QString KeyPath(void) const
428 {
429 return m_pszKeyPath;
430 }
431
432 void SetKeyPath(const QString &path)
433 {
434 m_pszKeyPath = path;
435 }
436
437 void CopyAESKey(const HLSSegment &segment)
438 {
439 memcpy(&m_aeskey, &(segment.m_aeskey), sizeof(m_aeskey));
440 m_keyloaded = segment.m_keyloaded;
441 }
442private:
443 HLS_AES_KEY m_aeskey {}; // AES-128 key
444 bool m_keyloaded {false};
445 QString m_pszKeyPath; // URL key path
446#endif
447
448private:
449 int m_id {0}; // unique sequence number
450 std::chrono::seconds m_duration {0s}; // segment duration
451 uint64_t m_bitrate {0}; // bitrate of segment's content (bits per second)
452 QString m_title; // human-readable informative title of the media segment
453
454 QString m_url;
455 QByteArray m_data; // raw data
456 int32_t m_played {0}; // bytes counter of data already read from segment
457 QMutex m_lock;
458 bool m_downloading {false};
459};
460
461/* stream class */
462
464{
465 public:
466 HLSStream(const int mid, const uint64_t bitrate, QString uri)
467 : m_id(mid),
468 m_bitrate(bitrate),
469 m_url(std::move(uri))
470 {
471#if CONFIG_LIBCRYPTO
472 m_aesIv.fill(0);
473#endif
474 }
475
476 HLSStream(const HLSStream &rhs, bool copy = true)
477 {
478 (*this) = rhs;
479 if (!copy)
480 return;
481 // copy all the segments across
482 for (auto *old : std::as_const(m_segments))
483 {
484 auto *segment = new HLSSegment(*old);
485 AppendSegment(segment);
486 }
487 }
488
490 {
491 for (const auto & segment : std::as_const(m_segments))
492 delete segment;
493 }
494
496 {
497 if (this == &rhs)
498 return *this;
499 // do not copy segments
500 m_id = rhs.m_id;
501 m_version = rhs.m_version;
504 m_bitrate = rhs.m_bitrate;
505 m_size = rhs.m_size;
507 m_live = rhs.m_live;
508 m_url = rhs.m_url;
509 m_cache = rhs.m_cache;
510#if CONFIG_LIBCRYPTO
511 m_keypath = rhs.m_keypath;
512 m_ivloaded = rhs.m_ivloaded;
513 m_aesIv = rhs.m_aesIv;
514#endif
515 return *this;
516 }
517
518 static bool IsGreater(const HLSStream *s1, const HLSStream *s2)
519 {
520 return s1->Bitrate() > s2->Bitrate();
521 }
522
523 bool operator<(const HLSStream &b) const
524 {
525 return this->Bitrate() < b.Bitrate();
526 }
527
528 bool operator>(const HLSStream &b) const
529 {
530 return this->Bitrate() > b.Bitrate();
531 }
532
538 uint64_t Size(bool force = false)
539 {
540 if (m_size > 0 && !force)
541 return m_size;
542 QMutexLocker lock(&m_lock);
543
544 int64_t size = 0;
545 int count = NumSegments();
546
547 for (int i = 0; i < count; i++)
548 {
549 HLSSegment *segment = GetSegment(i);
550 segment->Lock();
551 if (segment->Size() > 0)
552 {
553 size += (int64_t)segment->Size();
554 }
555 else
556 {
557 size += segment->Duration().count() * Bitrate() / 8;
558 }
559 segment->Unlock();
560 }
561 m_size = size;
562 return m_size;
563 }
564
565 std::chrono::seconds Duration(void)
566 {
567 QMutexLocker lock(&m_lock);
568 return m_duration;
569 }
570
571 void Clear(void)
572 {
573 m_segments.clear();
574 }
575
576 int NumSegments(void) const
577 {
578 return m_segments.size();
579 }
580
582 {
583 // must own lock
584 m_segments.append(segment);
585 }
586
587 HLSSegment *GetSegment(const int wanted) const
588 {
589 int count = NumSegments();
590 if (count <= 0)
591 return nullptr;
592 if ((wanted < 0) || (wanted >= count))
593 return nullptr;
594 return m_segments[wanted];
595 }
596
597 HLSSegment *FindSegment(const int id, int *segnum = nullptr) const
598 {
599 int count = NumSegments();
600 if (count <= 0)
601 return nullptr;
602 for (int n = 0; n < count; n++)
603 {
604 HLSSegment *segment = GetSegment(n);
605 if (segment == nullptr)
606 break;
607 if (segment->Id() == id)
608 {
609 if (segnum != nullptr)
610 {
611 *segnum = n;
612 }
613 return segment;
614 }
615 }
616 return nullptr;
617 }
618
619 void AddSegment(const std::chrono::seconds duration, const QString &title, const QString &uri)
620 {
621 QMutexLocker lock(&m_lock);
622 QString psz_uri = relative_URI(m_url, uri);
623 int id = NumSegments() + m_startsequence;
624#if !CONFIG_LIBCRYPTO
625 QString m_keypath;
626#endif
627 auto *segment = new HLSSegment(duration, id, title, psz_uri, m_keypath);
628 AppendSegment(segment);
629 m_duration += duration;
630 }
631
632 void RemoveSegment(HLSSegment *segment, bool willdelete = true)
633 {
634 QMutexLocker lock(&m_lock);
635 m_duration -= segment->Duration();
636 if (willdelete)
637 {
638 delete segment;
639 }
640 int count = NumSegments();
641 if (count <= 0)
642 return;
643 for (int n = 0; n < count; n++)
644 {
645 HLSSegment *old = GetSegment(n);
646 if (old == segment)
647 {
648 m_segments.removeAt(n);
649 break;
650 }
651 }
652 }
653
654 void RemoveSegment(int segnum, bool willdelete = true)
655 {
656 QMutexLocker lock(&m_lock);
657 HLSSegment *segment = GetSegment(segnum);
658 if (segment != nullptr)
659 {
660 m_duration -= segment->Duration();
661 if (willdelete)
662 delete segment;
663 }
664 m_segments.removeAt(segnum);
665 }
666
667 void RemoveListSegments(QHash<HLSSegment*,bool> &table)
668 {
669 for (auto it = table.begin(); it != table.end(); ++it)
670 {
671 bool todelete = *it;
672 HLSSegment *p = it.key();
673 RemoveSegment(p, todelete);
674 }
675 }
676
677 int DownloadSegmentData(int segnum, uint64_t &bandwidth, int stream)
678 {
679 HLSSegment *segment = GetSegment(segnum);
680 if (segment == nullptr)
681 return RET_ERROR;
682
683 segment->Lock();
684 if (!segment->IsEmpty())
685 {
686 /* Segment already downloaded */
687 segment->Unlock();
688 return RET_OK;
689 }
690
691 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
692 QString("started download of segment %1 [%2/%3] using stream %4")
693 .arg(segnum).arg(segment->Id()).arg(NumSegments()+m_startsequence)
694 .arg(stream));
695
696 /* sanity check - can we download this segment on time? */
697 if ((bandwidth > 0) && (m_bitrate > 0))
698 {
699 uint64_t size = (segment->Duration().count() * m_bitrate); /* bits */
700 auto estimated = std::chrono::seconds(size / bandwidth);
701 if (estimated > segment->Duration())
702 {
703 LOG(VB_PLAYBACK, LOG_INFO, LOC +
704 QString("downloading of segment %1 [id:%2] will take %3s, "
705 "which is longer than its playback (%4s) at %5bit/s")
706 .arg(segnum)
707 .arg(segment->Id())
708 .arg(estimated.count())
709 .arg(segment->Duration().count())
710 .arg(bandwidth));
711 }
712 }
713
714 std::chrono::microseconds start = mdate();
715 if (segment->Download() != RET_OK)
716 {
717 LOG(VB_PLAYBACK, LOG_ERR, LOC +
718 QString("downloaded segment %1 [id:%2] from stream %3 failed")
719 .arg(segnum).arg(segment->Id()).arg(m_id));
720 segment->Unlock();
721 return RET_ERROR;
722 }
723
724 std::chrono::microseconds downloadduration = mdate() - start;
725 if (m_bitrate == 0 && segment->Duration() > 0s)
726 {
727 /* Try to estimate the bandwidth for this stream */
728 m_bitrate = (uint64_t)(((double)segment->Size() * 8) /
729 ((double)segment->Duration().count()));
730 }
731
732#if CONFIG_LIBCRYPTO
733 /* If the segment is encrypted, decode it */
734 if (segment->HasKeyPath())
735 {
736 /* Do we have loaded the key ? */
737 if (!segment->KeyLoaded())
738 {
739 if (ManageSegmentKeys() != RET_OK)
740 {
741 LOG(VB_PLAYBACK, LOG_ERR, LOC +
742 "couldn't retrieve segment AES-128 key");
743 segment->Unlock();
744 return RET_OK;
745 }
746 }
747 if (segment->DecodeData(m_aesIv, m_ivloaded) != RET_OK)
748 {
749 segment->Unlock();
750 return RET_ERROR;
751 }
752 }
753#endif
754 segment->Unlock();
755
756 downloadduration = std::max(1us, downloadduration);
757 bandwidth = segment->Size() * 8ULL * 1000000ULL / downloadduration.count(); /* bits / s */
758 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
759 QString("downloaded segment %1 [id:%2] took %3ms for %4 bytes: bandwidth:%5kiB/s")
760 .arg(segnum)
761 .arg(segment->Id())
762 .arg(duration_cast<std::chrono::milliseconds>(downloadduration).count())
763 .arg(segment->Size())
764 .arg(bandwidth / 8192.0));
765
766 return RET_OK;
767 }
768 int Id(void) const
769 {
770 return m_id;
771 }
772 int Version(void) const
773 {
774 return m_version;
775 }
776 void SetVersion(int x)
777 {
778 m_version = x;
779 }
780 int StartSequence(void) const
781 {
782 return m_startsequence;
783 }
785 {
786 m_startsequence = x;
787 }
788 std::chrono::seconds TargetDuration(void) const
789 {
790 return m_targetduration;
791 }
792 void SetTargetDuration(std::chrono::seconds x)
793 {
795 }
796 uint64_t Bitrate(void) const
797 {
798 return m_bitrate;
799 }
800 bool Cache(void) const
801 {
802 return m_cache;
803 }
804 void SetCache(bool x)
805 {
806 m_cache = x;
807 }
808 bool Live(void) const
809 {
810 return m_live;
811 }
812 void SetLive(bool x)
813 {
814 m_live = x;
815 }
816 void Lock(void)
817 {
818 m_lock.lock();
819 }
820 void Unlock(void)
821 {
822 m_lock.unlock();
823 }
824 QString Url(void) const
825 {
826 return m_url;
827 }
828 void UpdateWith(const HLSStream &upd)
829 {
830 QMutexLocker lock(&m_lock);
833 m_cache = upd.m_cache;
834 }
835 void Cancel(void)
836 {
837 QMutexLocker lock(&m_lock);
838 for (const auto & segment : std::as_const(m_segments))
839 {
840 if (segment)
841 {
842 segment->CancelDownload();
843 }
844 }
845 }
846
847#if CONFIG_LIBCRYPTO
852 int ManageSegmentKeys() const
853 {
854 HLSSegment *seg = nullptr;
855 HLSSegment *prev_seg = nullptr;
856 int count = NumSegments();
857
858 for (int i = 0; i < count; i++)
859 {
860 prev_seg = seg;
861 seg = GetSegment(i);
862 if (seg == nullptr )
863 continue;
864 if (!seg->HasKeyPath())
865 continue; /* No key to load ? continue */
866 if (seg->KeyLoaded())
867 continue; /* The key is already loaded */
868
869 /* if the key has not changed, and already available from previous segment,
870 * try to copy it, and don't load the key */
871 if (prev_seg && prev_seg->KeyLoaded() &&
872 (seg->KeyPath() == prev_seg->KeyPath()))
873 {
874 seg->CopyAESKey(*prev_seg);
875 continue;
876 }
877 if (seg->DownloadKey() != RET_OK)
878 return RET_ERROR;
879 }
880 return RET_OK;
881 }
882 bool SetAESIV(QString line)
883 {
884 /*
885 * If the EXT-X-KEY tag has the IV attribute, implementations MUST use
886 * the attribute value as the IV when encrypting or decrypting with that
887 * key. The value MUST be interpreted as a 128-bit hexadecimal number
888 * and MUST be prefixed with 0x or 0X.
889 */
890 if (!line.startsWith(QLatin1String("0x"), Qt::CaseInsensitive))
891 return false;
892 if (line.size() % 2)
893 {
894 // not even size, pad with front 0
895 line.insert(2, QLatin1String("0"));
896 }
897#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
898 int padding = std::max(0, AES_BLOCK_SIZE - (line.size() - 2));
899#else
900 int padding = std::max(static_cast<qsizetype>(0), AES_BLOCK_SIZE - (line.size() - 2));
901#endif
902 QByteArray ba = QByteArray(padding, 0x0);
903 ba.append(QByteArray::fromHex(QByteArray(line.toLatin1().constData() + 2)));
904 std::ranges::copy(std::as_const(ba), m_aesIv.begin());
905 m_ivloaded = true;
906 return true;
907 }
908 aesiv_array AESIV(void)
909 {
910 return m_aesIv;
911 }
912 void SetKeyPath(const QString &x)
913 {
914 m_keypath = x;
915 }
916
917private:
918 QString m_keypath; // URL path of the encrypted key
919 bool m_ivloaded {false};
920 aesiv_array m_aesIv {0}; // IV used when decypher the block
921#endif
922
923private:
924 int m_id {0}; // program id
925 int m_version {1}; // protocol version should be 1
926 int m_startsequence {0}; // media starting sequence number
927 std::chrono::seconds m_targetduration {-1s}; // maximum duration per segment
928 uint64_t m_bitrate {0LL}; // bitrate of stream content (bits per second)
929 uint64_t m_size {0LL}; // stream length is calculated by taking the sum
930 // foreach segment of (segment->duration * hls->bitrate/8)
931 std::chrono::seconds m_duration {0s}; // duration of the stream
932 bool m_live {true};
933
934 QList<HLSSegment*> m_segments; // list of segments
935 QString m_url; // uri to m3u8
936 QMutex m_lock;
937 bool m_cache {true};// allow caching
938};
939
940// Playback Stream Information
942{
943public:
944 HLSPlayback(void) = default;
945
946 /* offset is only used from main thread, no need for locking */
947 uint64_t Offset(void) const
948 {
949 return m_offset;
950 }
951 void SetOffset(uint64_t val)
952 {
953 m_offset = val;
954 }
955 void AddOffset(uint64_t val)
956 {
957 m_offset += val;
958 }
959 int Stream(void)
960 {
961 QMutexLocker lock(&m_lock);
962 return m_stream;
963 }
964 void SetStream(int val)
965 {
966 QMutexLocker lock(&m_lock);
967 m_stream = val;
968 }
969 int Segment(void)
970 {
971 QMutexLocker lock(&m_lock);
972 return m_segment;
973 }
974 void SetSegment(int val)
975 {
976 QMutexLocker lock(&m_lock);
977 m_segment = val;
978 }
979 int IncrSegment(void)
980 {
981 QMutexLocker lock(&m_lock);
982 return ++m_segment;
983 }
984
985private:
986 uint64_t m_offset {0}; // current offset in media
987 int m_stream {0}; // current HLSStream
988 int m_segment {0}; // current segment for playback
989 QMutex m_lock;
990};
991
992// Stream Download Thread
993class StreamWorker : public MThread
994{
995public:
996 StreamWorker(HLSRingBuffer *parent, int startup, int buffer) : MThread("HLSStream"),
997 m_parent(parent), m_segment(startup), m_buffer(buffer)
998 {
999 }
1000 void Cancel(void)
1001 {
1002 m_interrupted = true;
1003 Wakeup();
1004 m_lock.lock();
1005 // Interrupt on-going downloads of all segments
1006 int streams = m_parent->NumStreams();
1007 for (int i = 0; i < streams; i++)
1008 {
1009 HLSStream *hls = m_parent->GetStream(i);
1010 if (hls)
1011 {
1012 hls->Cancel();
1013 }
1014 }
1015 m_lock.unlock();
1016 wait();
1017 }
1019 {
1020 QMutexLocker lock(&m_lock);
1021 return m_stream;
1022 }
1023 int Segment(void)
1024 {
1025 QMutexLocker lock(&m_lock);
1026 return m_segment;
1027 }
1028 void Seek(int val)
1029 {
1030 m_lock.lock();
1031 m_segment = val;
1032 m_lock.unlock();
1033 Wakeup();
1034 }
1035 bool IsAtEnd(bool lock = false)
1036 {
1037 if (lock)
1038 {
1039 m_lock.lock();
1040 }
1041 int count = m_parent->NumSegments();
1042 bool ret = m_segment >= count;
1043 if (lock)
1044 {
1045 m_lock.unlock();
1046 }
1047 return ret;
1048 }
1049
1053 bool GotBufferedSegments(int from, int count) const
1054 {
1055 if (from + count > m_parent->NumSegments())
1056 return false;
1057
1058 for (int i = from; i < from + count; i++)
1059 {
1060 if (StreamForSegment(i, false) < 0)
1061 {
1062 return false;
1063 }
1064 }
1065 return true;
1066 }
1067
1068 int CurrentPlaybackBuffer(bool lock = true)
1069 {
1070 if (lock)
1071 {
1072 m_lock.lock();
1073 }
1074 int ret = m_segment - m_parent->m_playback->Segment();
1075 if (lock)
1076 {
1077 m_lock.unlock();
1078 }
1079 return ret;
1080 }
1082 {
1083 return m_parent->NumSegments() - m_segment;
1084 }
1085 void SetBuffer(int val)
1086 {
1087 QMutexLocker lock(&m_lock);
1088 m_buffer = val;
1089 }
1090 void AddSegmentToStream(int segnum, int stream)
1091 {
1092 if (m_interrupted)
1093 return;
1094 QMutexLocker lock(&m_lock);
1095 m_segmap.insert(segnum, stream);
1096 }
1098 {
1099 QMutexLocker lock(&m_lock);
1100 m_segmap.remove(segnum);
1101 }
1102
1107 int StreamForSegment(int segmentid, bool lock = true) const
1108 {
1109 if (lock)
1110 {
1111 m_lock.lock();
1112 }
1113 int ret = 0;
1114 if (!m_segmap.contains(segmentid))
1115 {
1116 ret = -1; // we never downloaded that segment on any streams
1117 }
1118 else
1119 {
1120 ret = m_segmap[segmentid];
1121 }
1122 if (lock)
1123 {
1124 m_lock.unlock();
1125 }
1126 return ret;
1127 }
1128
1129 void Wakeup(void)
1130 {
1131 // send a wake signal
1132 m_waitcond.wakeAll();
1133 }
1134 void WaitForSignal(std::chrono::milliseconds time = std::chrono::milliseconds::max())
1135 {
1136 // must own lock
1137 m_waitcond.wait(&m_lock, time.count());
1138 }
1139 void Lock(void)
1140 {
1141 m_lock.lock();
1142 }
1143 void Unlock(void)
1144 {
1145 m_lock.unlock();
1146 }
1147 int64_t Bandwidth(void) const
1148 {
1149 return m_bandwidth;
1150 }
1151 double AverageNewBandwidth(int64_t bandwidth)
1152 {
1153 m_sumbandwidth += bandwidth;
1156 return m_bandwidth;
1157 }
1158
1159protected:
1160 void run(void) override // MThread
1161 {
1162 RunProlog();
1163
1164 int retries = 0;
1165 while (!m_interrupted)
1166 {
1167 /*
1168 * we can go into waiting if:
1169 * - not live and download is more than 3 segments ahead of playback
1170 * - we are at the end of the stream
1171 */
1172 Lock();
1174 if (hls == nullptr)
1175 {
1176 // an irrevocable error has occured. exit
1177 Wakeup();
1178 break;
1179 }
1180
1181 int dnldsegment = m_segment;
1182 int playsegment = m_parent->m_playback->Segment();
1183 if ((!hls->Live() && (playsegment < dnldsegment - m_buffer)) ||
1184 IsAtEnd())
1185 {
1186 /* wait until
1187 * 1- got interrupted
1188 * 2- we are less than 6 segments ahead of playback
1189 * 3- got asked to seek to a particular segment */
1190 while (!m_interrupted && (m_segment == dnldsegment) &&
1191 (((m_segment - playsegment) > m_buffer) || IsAtEnd()))
1192 {
1193 WaitForSignal();
1194 // do we have new segments available added by PlaylistWork?
1195 if (hls->Live() && !IsAtEnd())
1196 break;
1197 playsegment = m_parent->m_playback->Segment();
1198 }
1199 dnldsegment = m_segment;
1200 }
1201 Unlock();
1202
1203 if (m_interrupted)
1204 {
1205 Wakeup();
1206 break;
1207 }
1208 // have we already downloaded the required segment?
1209 if (StreamForSegment(dnldsegment) < 0)
1210 {
1211 uint64_t bw = m_bandwidth;
1212 int err = hls->DownloadSegmentData(dnldsegment, bw, m_stream);
1213 if (m_interrupted)
1214 {
1215 // interrupt early
1216 Wakeup();
1217 break;
1218 }
1219 bw = AverageNewBandwidth(bw);
1220 if (err != RET_OK)
1221 {
1222 retries++;
1223 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1224 QString("download failed, retry #%1").arg(retries));
1225 if (retries == 1) // first error
1226 continue; // will retry immediately
1227 std::this_thread::sleep_for(500ms);
1228 if (retries == 2) // and retry once again
1229 continue;
1230 if (!m_parent->m_meta) // NOLINT(bugprone-branch-clone)
1231 {
1232 // no other stream to default to, skip packet
1233 retries = 0;
1234 }
1235 else
1236 {
1237 // TODO: should switch to another stream
1238 retries = 0;
1239 }
1240 }
1241 else
1242 {
1243 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1244 QString("download completed, %1 segments ahead")
1245 .arg(CurrentLiveBuffer()));
1246 AddSegmentToStream(dnldsegment, m_stream);
1247 if (m_parent->m_meta && hls->Bitrate() != bw)
1248 {
1249 int newstream = BandwidthAdaptation(hls->Id(), bw);
1250
1251 if (newstream >= 0 && newstream != m_stream)
1252 {
1253 LOG(VB_PLAYBACK, LOG_INFO, LOC +
1254 QString("switching to %1 bitrate %2 stream; changing "
1255 "from stream %3 to stream %4")
1256 .arg(bw >= hls->Bitrate() ? "faster" : "lower")
1257 .arg(bw).arg(m_stream).arg(newstream));
1258 m_stream = newstream;
1259 }
1260 }
1261 }
1262 }
1263 Lock();
1264 if (dnldsegment == m_segment) // false if seek was called
1265 {
1266 m_segment++;
1267 }
1268 Unlock();
1269 // Signal we're done
1270 Wakeup();
1271 }
1272
1273 RunEpilog();
1274 }
1275
1276 int BandwidthAdaptation(int progid, uint64_t &bandwidth) const
1277 {
1278 int candidate = -1;
1279 uint64_t bw = bandwidth;
1280 uint64_t bw_candidate = 0;
1281
1282 int count = m_parent->NumStreams();
1283 for (int n = 0; n < count; n++)
1284 {
1285 /* Select best bandwidth match */
1286 HLSStream *hls = m_parent->GetStream(n);
1287 if (hls == nullptr)
1288 break;
1289
1290 /* only consider streams with the same PROGRAM-ID */
1291 if (hls->Id() == progid)
1292 {
1293 if ((bw >= hls->Bitrate()) &&
1294 (bw_candidate < hls->Bitrate()))
1295 {
1296 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1297 QString("candidate stream %1 bitrate %2 >= %3")
1298 .arg(n).arg(bw).arg(hls->Bitrate()));
1299 bw_candidate = hls->Bitrate();
1300 candidate = n; /* possible candidate */
1301 }
1302 }
1303 }
1304 bandwidth = bw_candidate;
1305 return candidate;
1306 }
1307
1308private:
1310 volatile bool m_interrupted {false};
1311 // measured average download bandwidth (bits per second)
1312 int64_t m_bandwidth {0};
1313 int m_stream {0};// current HLSStream
1314 int m_segment; // current segment for downloading
1315 int m_buffer; // buffer kept between download and playback
1316 QMap<int,int> m_segmap; // segment with streamid used for download
1317 mutable QMutex m_lock;
1318 QWaitCondition m_waitcond;
1319 double m_sumbandwidth {0.0};
1321};
1322
1323// Playlist Refresh Thread
1325{
1326public:
1327 PlaylistWorker(HLSRingBuffer *parent, std::chrono::milliseconds wait) : MThread("HLSStream"),
1328 m_parent(parent), m_wakeup(wait) {}
1329 void Cancel()
1330 {
1331 m_interrupted = true;
1332 Wakeup();
1333 m_lock.lock();
1334 // Interrupt on-going downloads of all stream playlists
1335 int streams = m_parent->NumStreams();
1336 QStringList listurls;
1337 listurls.reserve(streams);
1338 for (int i = 0; i < streams; i++)
1339 {
1340 HLSStream *hls = m_parent->GetStream(i);
1341 if (hls)
1342 {
1343 listurls.append(hls->Url());
1344 }
1345 }
1346 m_lock.unlock();
1347 cancelURL(listurls);
1348 wait();
1349 }
1350
1351 void Wakeup(void)
1352 {
1353 m_lock.lock();
1354 m_wokenup = true;
1355 m_lock.unlock();
1356 // send a wake signal
1357 m_waitcond.wakeAll();
1358 }
1359 void WaitForSignal(std::chrono::milliseconds time = std::chrono::milliseconds::max())
1360 {
1361 // must own lock
1362 m_waitcond.wait(&m_lock, time.count());
1363 }
1364 void Lock(void)
1365 {
1366 m_lock.lock();
1367 }
1368 void Unlock(void)
1369 {
1370 m_lock.unlock();
1371 }
1372
1373protected:
1374 void run(void) override // MThread
1375 {
1376 RunProlog();
1377
1379 bool live = hls ? hls->Live() : false;
1380 double wait = 0.5;
1381 double factor = live ? 1.0 : 2.0;
1382
1383 QWaitCondition mcond;
1384
1385 while (!m_interrupted)
1386 {
1387 if (m_parent->m_streamworker == nullptr)
1388 {
1389 // streamworker not running
1390 LOG(VB_PLAYBACK, LOG_ERR, LOC +
1391 "StreamWorker not running, aborting live playback");
1392 m_interrupted = true;
1393 break;
1394 }
1395
1396 Lock();
1397 if (!m_wokenup)
1398 {
1399 std::chrono::milliseconds waittime = std::max(100ms, m_wakeup);
1400 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1401 QString("PlayListWorker refreshing in %1s")
1402 .arg(duration_cast<std::chrono::seconds>(waittime).count()));
1403 WaitForSignal(waittime);
1404 }
1405 m_wokenup = false;
1406 Unlock();
1407
1408 /* reload the m3u8 */
1409 if (ReloadPlaylist() != RET_OK)
1410 {
1411 /* No change in playlist, then backoff */
1412 m_retries++;
1413 if (m_retries == 1) wait = 0.5;
1414 else if (m_retries == 2) wait = 1;
1415 else if (m_retries >= 3) wait = 2;
1416
1417 // If we haven't been able to reload the playlist after x times
1418 // it probably means the stream got deleted, so abort
1420 {
1421 LOG(VB_PLAYBACK, LOG_ERR, LOC +
1422 QString("reloading the playlist failed after %1 attempts."
1423 "aborting.").arg(PLAYLIST_FAILURE));
1424 m_parent->m_error = true;
1425 }
1426
1427 /* Can we afford to backoff? */
1429 {
1430 if (m_retries == 1)
1431 continue; // restart immediately if it's the first try
1432 m_retries = 0;
1433 wait = 0.5;
1434 }
1435 }
1436 else
1437 {
1438 // make streamworker process things
1440 m_retries = 0;
1441 wait = 0.5;
1442 }
1443
1444 if (hls == nullptr)
1445 {
1446 // an irrevocable error has occured. exit
1447 LOG(VB_PLAYBACK, LOG_ERR, LOC +
1448 "unable to retrieve current stream, aborting live playback");
1449 m_interrupted = true;
1450 break;
1451 }
1452
1453 /* determine next time to update playlist */
1454 m_wakeup = duration_cast<std::chrono::milliseconds>(
1455 hls->TargetDuration() * wait * factor);
1456 }
1457
1458 RunEpilog();
1459 }
1460
1461private:
1466 {
1467 auto *streams = new StreamsList;
1468
1469 LOG(VB_PLAYBACK, LOG_INFO, LOC + "reloading HLS live meta playlist");
1470
1471 if (GetHTTPLiveMetaPlaylist(streams) != RET_OK)
1472 {
1473 LOG(VB_PLAYBACK, LOG_ERR, LOC + "reloading playlist failed");
1474 m_parent->FreeStreamsList(streams);
1475 return RET_ERROR;
1476 }
1477
1478 /* merge playlists */
1479 int count = streams->size();
1480 for (int n = 0; n < count; n++)
1481 {
1482 HLSStream *hls_new = m_parent->GetStream(n, streams);
1483 if (hls_new == nullptr)
1484 continue;
1485
1486 HLSStream *hls_old = m_parent->FindStream(hls_new);
1487 if (hls_old == nullptr)
1488 { /* new hls stream - append */
1489 m_parent->m_streams.append(hls_new);
1490 LOG(VB_PLAYBACK, LOG_INFO, LOC +
1491 QString("new HLS stream appended (id=%1, bitrate=%2)")
1492 .arg(hls_new->Id()).arg(hls_new->Bitrate()));
1493 }
1494 else if (UpdatePlaylist(hls_new, hls_old) != RET_OK)
1495 {
1496 LOG(VB_PLAYBACK, LOG_ERR, LOC +
1497 QString("failed updating HLS stream (id=%1, bandwidth=%2)")
1498 .arg(hls_new->Id()).arg(hls_new->Bitrate()));
1499 m_parent->FreeStreamsList(streams);
1500 return RET_ERROR;
1501 }
1502 }
1503 delete streams;
1504 return RET_OK;
1505 }
1506
1507 static int UpdatePlaylist(HLSStream *hls_new, HLSStream *hls)
1508 {
1509 int count = hls_new->NumSegments();
1510
1511 LOG(VB_PLAYBACK, LOG_INFO, LOC +
1512 QString("updated hls stream (program-id=%1, bitrate=%2) has %3 segments")
1513 .arg(hls_new->Id()).arg(hls_new->Bitrate()).arg(count));
1514 QHash<HLSSegment*,bool> table;
1515
1516 for (int n = 0; n < count; n++)
1517 {
1518 HLSSegment *p = hls_new->GetSegment(n);
1519 if (p == nullptr)
1520 return RET_ERROR;
1521
1522 hls->Lock();
1523 HLSSegment *segment = hls->FindSegment(p->Id());
1524 if (segment)
1525 {
1526 segment->Lock();
1527 /* they should be the same */
1528 if ((p->Id() != segment->Id()) ||
1529 (p->Duration() != segment->Duration()) ||
1530 (p->Url() != segment->Url()))
1531 {
1532 LOG(VB_PLAYBACK, LOG_WARNING, LOC +
1533 QString("existing segment found with different content - resetting"));
1534 LOG(VB_PLAYBACK, LOG_WARNING, LOC +
1535 QString("- id: new=%1, old=%2")
1536 .arg(p->Id()).arg(segment->Id()));
1537 LOG(VB_PLAYBACK, LOG_WARNING, LOC +
1538 QString("- duration: new=%1, old=%2")
1539 .arg(p->Duration().count()).arg(segment->Duration().count()));
1540 LOG(VB_PLAYBACK, LOG_WARNING, LOC +
1541 QString("- file: new=%1 old=%2")
1542 .arg(p->Url(), segment->Url()));
1543
1544 /* Resetting content */
1545 *segment = *p;
1546 }
1547 // mark segment to be removed from new stream, and deleted
1548 table.insert(p, true);
1549 segment->Unlock();
1550 }
1551 else
1552 {
1553 int last = hls->NumSegments() - 1;
1554 HLSSegment *l = hls->GetSegment(last);
1555 if (l == nullptr)
1556 {
1557 hls->Unlock();
1558 return RET_ERROR;
1559 }
1560
1561 if ((l->Id() + 1) != p->Id())
1562 {
1563 LOG(VB_PLAYBACK, LOG_ERR, LOC +
1564 QString("gap in id numbers found: new=%1 expected %2")
1565 .arg(p->Id()).arg(l->Id()+1));
1566 }
1567 hls->AppendSegment(p);
1568 LOG(VB_PLAYBACK, LOG_INFO, LOC +
1569 QString("- segment %1 appended")
1570 .arg(p->Id()));
1571 // segment was moved to another stream, so do not delete it
1572 table.insert(p, false);
1573 }
1574 hls->Unlock();
1575 }
1576 hls_new->RemoveListSegments(table);
1577
1578 /* update meta information */
1579 hls->UpdateWith(*hls_new);
1580 return RET_OK;
1581 }
1582
1584 {
1585 int err = RET_ERROR;
1586
1587 /* Duplicate HLS stream META information */
1588 for (int i = 0; i < m_parent->m_streams.size() && !m_interrupted; i++)
1589 {
1590 auto *src = m_parent->GetStream(i);
1591 if (src == nullptr)
1592 return RET_ERROR;
1593
1594 auto *dst = new HLSStream(*src);
1595 streams->append(dst);
1596
1597 /* Download playlist file from server */
1598 QByteArray buffer;
1599 if (!downloadURL(dst->Url(), &buffer) || m_interrupted)
1600 {
1601 return RET_ERROR;
1602 }
1603 /* Parse HLS m3u8 content. */
1604 err = m_parent->ParseM3U8(&buffer, streams);
1605 }
1606 m_parent->SanitizeStreams(streams);
1607 return err;
1608 }
1609
1610 // private variable members
1612 bool m_interrupted {false};
1613 std::chrono::milliseconds m_wakeup; // next reload time
1614 int m_retries {0}; // number of consecutive failures
1615 bool m_wokenup {false};
1616 QMutex m_lock;
1617 QWaitCondition m_waitcond;
1618};
1619
1620HLSRingBuffer::HLSRingBuffer(const QString &lfilename) :
1622 m_playback(new HLSPlayback())
1623{
1624 m_startReadAhead = false;
1625 HLSRingBuffer::OpenFile(lfilename);
1626}
1627
1628HLSRingBuffer::HLSRingBuffer(const QString &lfilename, bool open) :
1630 m_playback(new HLSPlayback())
1631{
1632 m_startReadAhead = false;
1633 if (open)
1634 {
1635 HLSRingBuffer::OpenFile(lfilename);
1636 }
1637}
1638
1640{
1642
1643 QWriteLocker lock(&m_rwLock);
1644
1645 m_killed = true;
1646
1647 if (m_playlistworker)
1648 {
1650 delete m_playlistworker;
1651 }
1652 // stream worker must be deleted after playlist worker
1653 if (m_streamworker)
1654 {
1656 delete m_streamworker;
1657 }
1659 delete m_playback;
1660 if (m_fd)
1661 {
1662 fclose(m_fd);
1663 }
1664}
1665
1667{
1668 /* Free hls streams */
1669 for (int i = 0; i < streams->size(); i++)
1670 {
1671 HLSStream *hls = GetStream(i, streams);
1672 delete hls;
1673 }
1674 if (streams != &m_streams)
1675 {
1676 delete streams;
1677 }
1678}
1679
1681{
1682 int stream = m_streamworker->StreamForSegment(segnum);
1683 if (stream < 0)
1684 {
1685 return GetCurrentStream();
1686 }
1687 return GetStream(stream);
1688}
1689
1690HLSStream *HLSRingBuffer::GetStream(const int wanted, const StreamsList *streams) const
1691{
1692 if (streams == nullptr)
1693 {
1694 streams = &m_streams;
1695 }
1696 int count = streams->size();
1697 if (count <= 0)
1698 return nullptr;
1699 if ((wanted < 0) || (wanted >= count))
1700 return nullptr;
1701 return streams->at(wanted);
1702}
1703
1705{
1706 return GetStream(0, streams);
1707}
1708
1710{
1711 if (streams == nullptr)
1712 {
1713 streams = &m_streams;
1714 }
1715 int count = streams->size();
1716 if (count <= 0)
1717 return nullptr;
1718 count--;
1719 return GetStream(count, streams);
1720}
1721
1723 const StreamsList *streams) const
1724{
1725 if (streams == nullptr)
1726 {
1727 streams = &m_streams;
1728 }
1729 int count = streams->size();
1730 for (int n = 0; n < count; n++)
1731 {
1732 HLSStream *hls = GetStream(n, streams);
1733 if (hls)
1734 {
1735 /* compare */
1736 if ((hls->Id() == hls_new->Id()) &&
1737 ((hls->Bitrate() == hls_new->Bitrate()) ||
1738 (hls_new->Bitrate() == 0)))
1739 {
1740 return hls;
1741 }
1742 }
1743 }
1744 return nullptr;
1745}
1746
1751{
1752 if (!m_streamworker)
1753 {
1754 return nullptr;
1755 }
1757}
1758
1760{
1761 if (!s || s->size() < 7)
1762 return false;
1763
1764 if (!s->startsWith((const char*)"#EXTM3U"))
1765 return false;
1766
1767 QTextStream stream(s);
1768 /* Parse stream and search for
1769 * EXT-X-TARGETDURATION or EXT-X-STREAM-INF tag, see
1770 * http://tools.ietf.org/html/draft-pantos-http-live-streaming-04#page-8 */
1771 while (true)
1772 {
1773 QString line = stream.readLine();
1774 if (line.isNull())
1775 break;
1776 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1777 QString("IsHTTPLiveStreaming: %1").arg(line));
1778 if (line.startsWith(QLatin1String("#EXT-X-TARGETDURATION")) ||
1779 line.startsWith(QLatin1String("#EXT-X-MEDIA-SEQUENCE")) ||
1780 line.startsWith(QLatin1String("#EXT-X-KEY")) ||
1781 line.startsWith(QLatin1String("#EXT-X-ALLOW-CACHE")) ||
1782 line.startsWith(QLatin1String("#EXT-X-ENDLIST")) ||
1783 line.startsWith(QLatin1String("#EXT-X-STREAM-INF")) ||
1784 line.startsWith(QLatin1String("#EXT-X-DISCONTINUITY")) ||
1785 line.startsWith(QLatin1String("#EXT-X-VERSION")))
1786 {
1787 return true;
1788 }
1789 }
1790 return false;
1791}
1792
1794{
1795 bool isHLS = false;
1796
1797 // Do a peek on the URL to test the format
1799
1800 AVIOContext* context = nullptr;
1801 int ret = avio_open(&context, filename.toLatin1().constData(), AVIO_FLAG_READ);
1802 if (ret >= 0)
1803 {
1804 std::array<uint8_t,1024> buffer {};
1805 ret = avio_read(context, buffer.data(), buffer.size());
1806 if (ret > 0)
1807 {
1808 QByteArray ba((const char*)buffer.data(), ret);
1809 isHLS = IsHTTPLiveStreaming(&ba);
1810 }
1811 avio_closep(&context);
1812 }
1813 else
1814 {
1815 // couldn't peek, rely on URL analysis
1816 QUrl url { filename };
1817 isHLS =
1818 url.path().endsWith(QLatin1String("m3u8"), Qt::CaseInsensitive) ||
1819 url.query( QUrl::FullyEncoded ).contains(QLatin1String("m3u8"), Qt::CaseInsensitive);
1820 }
1821 return isHLS;
1822}
1823
1824/* Parsing */
1825QString HLSRingBuffer::ParseAttributes(const QString &line, const char *attr)
1826{
1827 int p = line.indexOf(QLatin1String(":"));
1828 if (p < 0)
1829 return {};
1830
1831 QStringList list = line.mid(p+1).split(',');
1832 for (const auto& it : std::as_const(list))
1833 {
1834 QString arg = it.trimmed();
1835 if (arg.startsWith(attr))
1836 {
1837 int pos = arg.indexOf(QLatin1String("="));
1838 if (pos < 0)
1839 continue;
1840 return arg.mid(pos+1);
1841 }
1842 }
1843 return {};
1844}
1845
1850int HLSRingBuffer::ParseDecimalValue(const QString &line, int &target)
1851{
1852 int p = line.indexOf(QLatin1String(":"));
1853 if (p < 0)
1854 return RET_ERROR;
1855 int i = p + 1;
1856 for ( ; i < line.size(); i++)
1857 if (!line[i].isNumber())
1858 break;
1859 if (i == p + 1)
1860 return RET_ERROR;
1861#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
1862 target = line.midRef(p+1, i - p - 1).toInt();
1863#else
1864 target = QStringView(line).mid(p+1, i - p - 1).toInt();
1865#endif
1866 return RET_OK;
1867}
1868
1869int HLSRingBuffer::ParseSegmentInformation(const HLSStream *hls, const QString &line,
1870 int &duration, QString &title)
1871{
1872 /*
1873 * #EXTINF:<duration>,<title>
1874 *
1875 * "duration" is an integer that specifies the duration of the media
1876 * file in seconds. Durations SHOULD be rounded to the nearest integer.
1877 * The remainder of the line following the comma is the title of the
1878 * media file, which is an optional human-readable informative title of
1879 * the media segment
1880 */
1881 int p = line.indexOf(QLatin1String(":"));
1882 if (p < 0)
1883 return RET_ERROR;
1884
1885 QStringList list = line.mid(p+1).split(',');
1886
1887 /* read duration */
1888 if (list.isEmpty())
1889 {
1890 return RET_ERROR;
1891 }
1892 const QString& val = list[0];
1893
1894 if (hls->Version() < 3)
1895 {
1896 bool ok = false;
1897 duration = val.toInt(&ok);
1898 if (!ok)
1899 {
1900 duration = -1;
1901 return RET_ERROR;
1902 }
1903 }
1904 else
1905 {
1906 bool ok = false;
1907 double d = val.toDouble(&ok);
1908 if (!ok)
1909 {
1910 duration = -1;
1911 return RET_ERROR;
1912 }
1913 if (d - ((int)d) >= 0.5)
1914 duration = ((int)d) + 1;
1915 else
1916 duration = ((int)d);
1917 }
1918
1919 if (list.size() >= 2)
1920 {
1921 title = list[1];
1922 }
1923
1924 /* Ignore the rest of the line */
1925 return RET_OK;
1926}
1927
1928int HLSRingBuffer::ParseTargetDuration(HLSStream *hls, const QString &line)
1929{
1930 /*
1931 * #EXT-X-TARGETDURATION:<s>
1932 *
1933 * where s is an integer indicating the target duration in seconds.
1934 */
1935 int duration = -1;
1936
1937 if (ParseDecimalValue(line, duration) != RET_OK)
1938 {
1939 LOG(VB_PLAYBACK, LOG_ERR, LOC + "expected #EXT-X-TARGETDURATION:<s>");
1940 return RET_ERROR;
1941 }
1942 hls->SetTargetDuration(std::chrono::seconds(duration));
1943 return RET_OK;
1944}
1945
1946HLSStream *HLSRingBuffer::ParseStreamInformation(const QString &line, const QString &uri) const
1947{
1948 /*
1949 * #EXT-X-STREAM-INF:[attribute=value][,attribute=value]*
1950 * <URI>
1951 */
1952 int id = 0;
1953 QString attr;
1954
1955 attr = ParseAttributes(line, "PROGRAM-ID");
1956 if (attr.isNull())
1957 {
1958 LOG(VB_PLAYBACK, LOG_INFO, LOC + "#EXT-X-STREAM-INF: expected PROGRAM-ID=<value>, using -1");
1959 id = -1;
1960 }
1961 else
1962 {
1963 id = attr.toInt();
1964 }
1965
1966 attr = ParseAttributes(line, "BANDWIDTH");
1967 if (attr.isNull())
1968 {
1969 LOG(VB_PLAYBACK, LOG_ERR, LOC + "#EXT-X-STREAM-INF: expected BANDWIDTH=<value>");
1970 return nullptr;
1971 }
1972 uint64_t bw = attr.toInt();
1973
1974 if (bw == 0)
1975 {
1976 LOG(VB_PLAYBACK, LOG_ERR, LOC + "#EXT-X-STREAM-INF: bandwidth cannot be 0");
1977 return nullptr;
1978 }
1979
1980 LOG(VB_PLAYBACK, LOG_INFO, LOC +
1981 QString("bandwidth adaptation detected (program-id=%1, bandwidth=%2")
1982 .arg(id).arg(bw));
1983
1984 QString psz_uri = relative_URI(m_m3u8, uri);
1985
1986 return new HLSStream(id, bw, psz_uri);
1987}
1988
1989int HLSRingBuffer::ParseMediaSequence(HLSStream *hls, const QString &line)
1990{
1991 /*
1992 * #EXT-X-MEDIA-SEQUENCE:<number>
1993 *
1994 * A Playlist file MUST NOT contain more than one EXT-X-MEDIA-SEQUENCE
1995 * tag. If the Playlist file does not contain an EXT-X-MEDIA-SEQUENCE
1996 * tag then the sequence number of the first URI in the playlist SHALL
1997 * be considered to be 0.
1998 */
1999 int sequence = 0;
2000
2001 if (ParseDecimalValue(line, sequence) != RET_OK)
2002 {
2003 LOG(VB_PLAYBACK, LOG_ERR, LOC + "expected #EXT-X-MEDIA-SEQUENCE:<s>");
2004 return RET_ERROR;
2005 }
2006
2007 if (hls->StartSequence() > 0 && !hls->Live())
2008 {
2009 LOG(VB_PLAYBACK, LOG_ERR, LOC +
2010 QString("EXT-X-MEDIA-SEQUENCE already present in playlist (new=%1, old=%2)")
2011 .arg(sequence).arg(hls->StartSequence()));
2012 }
2013 hls->SetStartSequence(sequence);
2014 return RET_OK;
2015}
2016
2017
2018int HLSRingBuffer::ParseKey(HLSStream *hls, const QString &line)
2019{
2020 /*
2021 * #EXT-X-KEY:METHOD=<method>[,URI="<URI>"][,IV=<IV>]
2022 *
2023 * The METHOD attribute specifies the encryption method. Two encryption
2024 * methods are defined: NONE and AES-128.
2025 */
2026 int err = 0;
2027 QString attr = ParseAttributes(line, "METHOD");
2028 if (attr.isNull())
2029 {
2030 LOG(VB_PLAYBACK, LOG_ERR, LOC + "#EXT-X-KEY: expected METHOD=<value>");
2031 return RET_ERROR;
2032 }
2033
2034 if (attr.startsWith(QLatin1String("NONE")))
2035 {
2036 QString uri = ParseAttributes(line, "URI");
2037 if (!uri.isNull())
2038 {
2039 LOG(VB_PLAYBACK, LOG_ERR, LOC + "#EXT-X-KEY: URI not expected");
2040 err = RET_ERROR;
2041 }
2042 /* IV is only supported in version 2 and above */
2043 if (hls->Version() >= 2)
2044 {
2045 QString iv = ParseAttributes(line, "IV");
2046 if (!iv.isNull())
2047 {
2048 LOG(VB_PLAYBACK, LOG_ERR, LOC + "#EXT-X-KEY: IV not expected");
2049 err = RET_ERROR;
2050 }
2051 }
2052 }
2053#if CONFIG_LIBCRYPTO
2054 else if (attr.startsWith(QLatin1String("AES-128")))
2055 {
2056 QString uri;
2057 QString iv;
2058 if (!m_aesmsg)
2059 {
2060 LOG(VB_PLAYBACK, LOG_INFO, LOC +
2061 "playback of AES-128 encrypted HTTP Live media detected.");
2062 m_aesmsg = true;
2063 }
2064 uri = ParseAttributes(line, "URI");
2065 if (uri.isNull())
2066 {
2067 LOG(VB_PLAYBACK, LOG_ERR, LOC +
2068 "#EXT-X-KEY: URI not found for encrypted HTTP Live media in AES-128");
2069 return RET_ERROR;
2070 }
2071
2072 /* Url is between quotes, remove them */
2073 hls->SetKeyPath(decoded_URI(uri.remove(QChar(QLatin1Char('"')))));
2074
2075 iv = ParseAttributes(line, "IV");
2076 if (!iv.isNull() && !hls->SetAESIV(iv))
2077 {
2078 LOG(VB_PLAYBACK, LOG_ERR, LOC + "invalid IV");
2079 err = RET_ERROR;
2080 }
2081 }
2082#endif
2083 else
2084 {
2085 LOG(VB_PLAYBACK, LOG_ERR, LOC +
2086 "invalid encryption type, only NONE "
2087#if CONFIG_LIBCRYPTO
2088 "and AES-128 are supported"
2089#else
2090 "is supported."
2091#endif
2092 );
2093 err = RET_ERROR;
2094 }
2095 return err;
2096}
2097
2098int HLSRingBuffer::ParseProgramDateTime(HLSStream */*hls*/, const QString &line)
2099{
2100 /*
2101 * #EXT-X-PROGRAM-DATE-TIME:<YYYY-MM-DDThh:mm:ssZ>
2102 */
2103 LOG(VB_PLAYBACK, LOG_WARNING, LOC +
2104 QString("tag not supported: #EXT-X-PROGRAM-DATE-TIME %1")
2105 .arg(line));
2106 return RET_OK;
2107}
2108
2109int HLSRingBuffer::ParseAllowCache(HLSStream *hls, const QString &line)
2110{
2111 /*
2112 * The EXT-X-ALLOW-CACHE tag indicates whether the client MAY or MUST
2113 * NOT cache downloaded media files for later replay. It MAY occur
2114 * anywhere in the Playlist file; it MUST NOT occur more than once. The
2115 * EXT-X-ALLOW-CACHE tag applies to all segments in the playlist. Its
2116 * format is:
2117 *
2118 * #EXT-X-ALLOW-CACHE:<YES|NO>
2119 */
2120 int pos = line.indexOf(QLatin1String(":"));
2121 if (pos < 0)
2122 return RET_ERROR;
2123 QString answer = line.mid(pos+1, 3);
2124 if (answer.size() < 2)
2125 {
2126 LOG(VB_PLAYBACK, LOG_ERR, LOC + "#EXT-X-ALLOW-CACHE, ignoring ...");
2127 return RET_ERROR;
2128 }
2129 hls->SetCache(!answer.startsWith(QLatin1String("NO")));
2130 return RET_OK;
2131}
2132
2133int HLSRingBuffer::ParseVersion(const QString &line, int &version)
2134{
2135 /*
2136 * The EXT-X-VERSION tag indicates the compatibility version of the
2137 * Playlist file. The Playlist file, its associated media, and its
2138 * server MUST comply with all provisions of the most-recent version of
2139 * this document describing the protocol version indicated by the tag
2140 * value.
2141 *
2142 * Its format is:
2143 *
2144 * #EXT-X-VERSION:<n>
2145 */
2146
2147 if (ParseDecimalValue(line, version) != RET_OK)
2148 {
2149 LOG(VB_PLAYBACK, LOG_ERR, LOC +
2150 "#EXT-X-VERSION: no protocol version found, should be version 1.");
2151 return RET_ERROR;
2152 }
2153
2154 if (version <= 0 || version > 3)
2155 {
2156 LOG(VB_PLAYBACK, LOG_ERR, LOC +
2157 QString("#EXT-X-VERSION should be version 1, 2 or 3 iso %1")
2158 .arg(version));
2159 return RET_ERROR;
2160 }
2161 return RET_OK;
2162}
2163
2165{
2166 /*
2167 * The EXT-X-ENDLIST tag indicates that no more media files will be
2168 * added to the Playlist file. It MAY occur anywhere in the Playlist
2169 * file; it MUST NOT occur more than once. Its format is:
2170 */
2171 hls->SetLive(false);
2172 LOG(VB_PLAYBACK, LOG_INFO, LOC + "video on demand (vod) mode");
2173 return RET_OK;
2174}
2175
2176int HLSRingBuffer::ParseDiscontinuity(HLSStream */*hls*/, const QString &line)
2177{
2178 /* Not handled, never seen so far */
2179 LOG(VB_PLAYBACK, LOG_DEBUG, LOC + QString("#EXT-X-DISCONTINUITY %1").arg(line));
2180 return RET_OK;
2181}
2182
2183int HLSRingBuffer::ParseM3U8(const QByteArray *buffer, StreamsList *streams)
2184{
2192 if (streams == nullptr)
2193 {
2194 streams = &m_streams;
2195 }
2196 QTextStream stream(*buffer);
2197#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
2198 stream.setCodec("UTF-8");
2199#else
2200 stream.setEncoding(QStringConverter::Utf8);
2201#endif
2202
2203 QString line = stream.readLine();
2204 if (line.isNull())
2205 return RET_ERROR;
2206
2207 if (!line.startsWith(QLatin1String("#EXTM3U")))
2208 {
2209 LOG(VB_PLAYBACK, LOG_ERR, LOC + "missing #EXTM3U tag .. aborting");
2210 return RET_ERROR;
2211 }
2212
2213 /* What is the version ? */
2214 int version = 1;
2215 int p = buffer->indexOf("#EXT-X-VERSION:");
2216 if (p >= 0)
2217 {
2218 stream.seek(p);
2219 QString psz_version = stream.readLine();
2220 if (psz_version.isNull())
2221 return RET_ERROR;
2222 int ret = ParseVersion(psz_version, version);
2223 if (ret != RET_OK)
2224 {
2225 LOG(VB_GENERAL, LOG_WARNING, LOC +
2226 "#EXT-X-VERSION: no protocol version found, assuming version 1.");
2227 version = 1;
2228 }
2229 }
2230
2231 /* Is it a meta index file ? */
2232 bool meta = buffer->indexOf("#EXT-X-STREAM-INF") >= 0;
2233
2234 int err = RET_OK;
2235
2236 if (meta)
2237 {
2238 LOG(VB_PLAYBACK, LOG_INFO, LOC + "Meta playlist");
2239
2240 /* M3U8 Meta Index file */
2241 stream.seek(0); // rewind
2242 while (!m_killed)
2243 {
2244 line = stream.readLine();
2245 if (line.isNull())
2246 break;
2247
2248 if (line.startsWith(QLatin1String("#EXT-X-STREAM-INF")))
2249 {
2250 m_meta = true;
2251 QString uri = stream.readLine();
2252 if (uri.isNull())
2253 {
2254 err = RET_ERROR;
2255 break;
2256 }
2257 if (uri.startsWith(QLatin1String("#")))
2258 {
2259 LOG(VB_GENERAL, LOG_INFO, LOC +
2260 QString("Skipping invalid stream-inf: %1")
2261 .arg(uri));
2262 }
2263 else
2264 {
2265 HLSStream *hls = ParseStreamInformation(line, decoded_URI(uri));
2266 if (hls)
2267 {
2268 /* Download playlist file from server */
2269 QByteArray buf;
2270 bool ret = downloadURL(hls->Url(), &buf);
2271 if (!ret)
2272 {
2273 LOG(VB_GENERAL, LOG_INFO, LOC +
2274 QString("Skipping invalid stream, couldn't download: %1")
2275 .arg(hls->Url()));
2276 delete hls;
2277 continue;
2278 }
2279 streams->append(hls);
2280 // One last chance to abort early
2281 if (m_killed)
2282 {
2283 err = RET_ERROR;
2284 break;
2285 }
2286 /* Parse HLS m3u8 content. */
2287 err = ParseM3U8(&buf, streams);
2288 if (err != RET_OK)
2289 break;
2290 hls->SetVersion(version);
2291 }
2292 }
2293 }
2294 }
2295 }
2296 else
2297 {
2298 HLSStream *hls = nullptr;
2299 if (m_meta)
2300 {
2301 hls = GetLastStream(streams);
2302 }
2303 else
2304 {
2305 /* No Meta playlist used */
2306 hls = new HLSStream(0, 0, m_m3u8);
2307 streams->append(hls);
2308 /* Get TARGET-DURATION first */
2309 p = buffer->indexOf("#EXT-X-TARGETDURATION:");
2310 if (p >= 0)
2311 {
2312 stream.seek(p);
2313 QString psz_duration = stream.readLine();
2314 if (psz_duration.isNull())
2315 return RET_ERROR;
2316 err = ParseTargetDuration(hls, psz_duration);
2317 if (err != RET_OK)
2318 return err;
2319 }
2320 /* Store version */
2321 hls->SetVersion(version);
2322 }
2323 LOG(VB_PLAYBACK, LOG_INFO, LOC +
2324 QString("%1 Playlist HLS protocol version: %2")
2325 .arg(hls->Live() ? "Live": "VOD").arg(version));
2326
2327 // rewind
2328 stream.seek(0);
2329 /* */
2330 std::chrono::seconds segment_duration = -1s;
2331 QString title;
2332 while (err == RET_OK)
2333 {
2334 /* Next line */
2335 line = stream.readLine();
2336 if (line.isNull())
2337 break;
2338 LOG(VB_PLAYBACK, LOG_DEBUG, LOC + QString("ParseM3U8: %1")
2339 .arg(line));
2340
2341 if (line.startsWith(QLatin1String("#EXTINF")))
2342 {
2343 int tmp = -1;
2344 err = ParseSegmentInformation(hls, line, tmp, title);
2345 segment_duration = std::chrono::seconds(tmp);
2346 }
2347 else if (line.startsWith(QLatin1String("#EXT-X-TARGETDURATION")))
2348 {
2349 err = ParseTargetDuration(hls, line);
2350 }
2351 else if (line.startsWith(QLatin1String("#EXT-X-MEDIA-SEQUENCE")))
2352 {
2353 err = ParseMediaSequence(hls, line);
2354 }
2355 else if (line.startsWith(QLatin1String("#EXT-X-KEY")))
2356 {
2357 err = ParseKey(hls, line);
2358 }
2359 else if (line.startsWith(QLatin1String("#EXT-X-PROGRAM-DATE-TIME")))
2360 {
2361 err = ParseProgramDateTime(hls, line);
2362 }
2363 else if (line.startsWith(QLatin1String("#EXT-X-ALLOW-CACHE")))
2364 {
2365 err = ParseAllowCache(hls, line);
2366 }
2367 else if (line.startsWith(QLatin1String("#EXT-X-DISCONTINUITY")))
2368 {
2369 err = ParseDiscontinuity(hls, line);
2370 }
2371 else if (line.startsWith(QLatin1String("#EXT-X-VERSION")))
2372 {
2373 int version2 = 0;
2374 err = ParseVersion(line, version2);
2375 hls->SetVersion(version2);
2376 }
2377 else if (line.startsWith(QLatin1String("#EXT-X-ENDLIST")))
2378 {
2379 err = ParseEndList(hls);
2380 }
2381 else if (!line.startsWith(QLatin1String("#")) && !line.isEmpty())
2382 {
2383 hls->AddSegment(segment_duration, title, decoded_URI(line));
2384 segment_duration = -1s; /* reset duration */
2385 title = "";
2386 }
2387 }
2388 }
2389 return err;
2390}
2391
2392// stream content functions
2397{
2398 int retries = 0;
2399 std::chrono::microseconds starttime = mdate();
2400 LOG(VB_PLAYBACK, LOG_INFO, LOC +
2401 QString("Starting Prefetch for %2 segments")
2402 .arg(count));
2405 while (!m_error && !m_killed && (retries < 20) &&
2406 (m_streamworker->CurrentPlaybackBuffer(false) < count) &&
2408 {
2410 retries++;
2411 }
2413 LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("Finished Prefetch (%1s)")
2414 .arg(duration_cast<std::chrono::seconds>(mdate() - starttime).count()));
2415 // we waited more than 10s abort
2416 if (retries >= 10)
2417 return RET_ERROR;
2418 return RET_OK;
2419}
2420
2422{
2423 bool live = hls->Live();
2424 /* sanity check */
2425 if ((m_streamworker->CurrentPlaybackBuffer() == 0) &&
2426 (!m_streamworker->IsAtEnd(true) || live))
2427 {
2428 LOG(VB_PLAYBACK, LOG_WARNING, LOC + "playback will stall");
2429 }
2431 (!m_streamworker->IsAtEnd(true) || live))
2432 {
2433 LOG(VB_PLAYBACK, LOG_WARNING, LOC + "playback in danger of stalling");
2434 }
2435 else if (live && m_streamworker->IsAtEnd(true) &&
2437 {
2438 LOG(VB_PLAYBACK, LOG_WARNING, LOC + "playback will exit soon, starving for data");
2439 }
2440}
2441
2447HLSSegment *HLSRingBuffer::GetSegment(int segnum, std::chrono::milliseconds timeout)
2448{
2449 HLSSegment *segment = nullptr;
2450 int stream = m_streamworker->StreamForSegment(segnum);
2451 if (stream < 0)
2452 {
2453 // we haven't downloaded that segment, request it
2454 // we should never be into this condition for normal playback
2455 m_streamworker->Seek(segnum);
2457 /* Wait for download to be finished */
2458 LOG(VB_PLAYBACK, LOG_WARNING, LOC +
2459 LOC + QString("waiting to get segment %1")
2460 .arg(segnum));
2461 int retries = 0;
2462 while (!m_error && (stream < 0) && (retries < 10))
2463 {
2465 stream = m_streamworker->StreamForSegment(segnum, false);
2466 retries++;
2467 }
2469 if (stream < 0)
2470 return nullptr;
2471 }
2472 HLSStream *hls = GetStream(stream);
2473 hls->Lock();
2474 segment = hls->GetSegment(segnum);
2475 hls->Unlock();
2476 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
2477 QString("GetSegment %1 [%2] stream[%3] (bitrate:%4)")
2478 .arg(segnum).arg(segment->Id()).arg(stream).arg(hls->Bitrate()));
2479 SanityCheck(hls);
2480 return segment;
2481}
2482
2484{
2485 return m_streams.size();
2486}
2487
2489{
2490 HLSStream *hls = GetStream(0);
2491 if (hls == nullptr)
2492 return 0;
2493 hls->Lock();
2494 int count = hls->NumSegments();
2495 hls->Unlock();
2496 return count;
2497}
2498
2499int HLSRingBuffer::ChooseSegment(int stream) const
2500{
2501 /* Choose a segment to start which is no closer than
2502 * 3 times the target duration from the end of the playlist.
2503 */
2504 int wanted = 0;
2505 int segid = 0;
2506 std::chrono::seconds wanted_duration = 0s;
2507 int count = NumSegments();
2508 int i = count - 1;
2509
2510 HLSStream *hls = GetStream(stream);
2511 while(i >= 0)
2512 {
2513 HLSSegment *segment = hls->GetSegment(i);
2514 if (segment == nullptr)
2515 continue;
2516
2517 if (segment->Duration() > hls->TargetDuration())
2518 {
2519 LOG(VB_PLAYBACK, LOG_WARNING, LOC +
2520 QString("EXTINF:%1 duration is larger than EXT-X-TARGETDURATION:%2")
2521 .arg(segment->Duration().count()).arg(hls->TargetDuration().count()));
2522 }
2523
2524 wanted_duration += segment->Duration();
2525 if (wanted_duration >= 3 * hls->TargetDuration())
2526 {
2527 /* Start point found */
2528 wanted = i;
2529 segid = segment->Id();
2530 break;
2531 }
2532 i-- ;
2533 }
2534
2535 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
2536 QString("Choose segment %1/%2 [%3]")
2537 .arg(wanted).arg(count).arg(segid));
2538 return wanted;
2539}
2540
2546{
2547 // no lock is required as, at this stage, no threads have either been started
2548 // or we are working on a stream list unique to PlaylistWorker
2549 if (streams == nullptr)
2550 {
2551 streams = &m_streams;
2552 }
2553 QMap<int,int> idstart;
2554 // Find the highest starting sequence for each stream
2555 for (int n = streams->size() - 1 ; n >= 0; n--)
2556 {
2557 HLSStream *hls = GetStream(n, streams);
2558 if (hls == nullptr)
2559 continue;
2560 if (hls->NumSegments() == 0)
2561 {
2562 streams->removeAt(n);
2563 continue; // remove it
2564 }
2565
2566 int id = hls->Id();
2567 int start = hls->StartSequence();
2568 if (!idstart.contains(id))
2569 {
2570 idstart.insert(id, start);
2571 }
2572 int start2 = idstart.value(id);
2573 if (start > start2)
2574 {
2575 idstart.insert(id, start);
2576 }
2577 }
2578 // Find the highest starting sequence for each stream
2579 for (int n = 0; n < streams->size(); n++)
2580 {
2581 HLSStream *hls = GetStream(n, streams);
2582 if (hls == nullptr)
2583 continue;
2584 int id = hls->Id();
2585 int seq = hls->StartSequence();
2586 int newstart= idstart.value(id);
2587 int todrop = newstart - seq;
2588 if (todrop == 0)
2589 {
2590 // perfect, leave it alone
2591 continue;
2592 }
2593 if (todrop >= hls->NumSegments() || todrop < 0)
2594 {
2595 LOG(VB_PLAYBACK, LOG_ERR, LOC +
2596 QString("stream %1 [id=%2] can't be properly adjusted, ignoring")
2597 .arg(n).arg(hls->Id()));
2598 continue;
2599 }
2600 for (int i = 0; i < todrop; i++)
2601 {
2602 hls->RemoveSegment(0);
2603 }
2604 hls->SetStartSequence(newstart);
2605 }
2606}
2607
2616bool HLSRingBuffer::OpenFile(const QString &lfilename, std::chrono::milliseconds /*retry_ms*/)
2617{
2618 QWriteLocker lock(&m_rwLock);
2619
2620 m_safeFilename = lfilename;
2621 m_filename = lfilename;
2622 QString finalURL;
2623
2624 QByteArray buffer;
2625 if (!downloadURL(m_filename, &buffer, finalURL))
2626 {
2627 LOG(VB_PLAYBACK, LOG_ERR, LOC +
2628 QString("Couldn't open URL %1").arg(m_filename));
2629 return false; // can't download file
2630 }
2631 if (m_filename != finalURL)
2632 {
2633 LOG(VB_PLAYBACK, LOG_INFO, LOC +
2634 QString("Redirected %1 -> %2 ").arg(m_filename, finalURL));
2635 m_filename = finalURL;
2636 }
2637 if (!IsHTTPLiveStreaming(&buffer))
2638 {
2639 LOG(VB_PLAYBACK, LOG_ERR, LOC +
2640 QString("%1 isn't a HTTP Live Streaming URL").arg(m_filename));
2641 return false;
2642 }
2643 // let's go
2645 LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("HTTP Live Streaming (%1)")
2646 .arg(m_m3u8));
2647
2648 /* Parse HLS m3u8 content. */
2649 if (ParseM3U8(&buffer, &m_streams) != RET_OK || m_streams.isEmpty())
2650 {
2651 LOG(VB_PLAYBACK, LOG_ERR, LOC +
2652 QString("An error occurred reading M3U8 playlist (%1)").arg(m_filename));
2653 m_error = true;
2654 return false;
2655 }
2656
2658
2659 /* HLS standard doesn't provide any guaranty about streams
2660 being sorted by bitrate, so we sort them, higher bitrate being first */
2661 // QList doesn't play well with std::ranges
2662 // NOLINTNEXTLINE(modernize-use-ranges)
2663 std::sort(m_streams.begin(), m_streams.end(), HLSStream::IsGreater);
2664
2665 // if we want as close to live. We should be selecting a further segment
2666 // m_live ? ChooseSegment(0) : 0;
2667// if (m_live && m_startup < 0)
2668// {
2669// LOG(VB_PLAYBACK, LOG_WARNING, LOC +
2670// "less data than 3 times 'target duration' available for "
2671// "live playback, playback may stall");
2672// m_startup = 0;
2673// }
2674 m_startup = 0;
2676
2679
2680 if (Prefetch(std::min(NumSegments(), PLAYBACK_MINBUFFER)) != RET_OK)
2681 {
2682 LOG(VB_PLAYBACK, LOG_ERR, LOC +
2683 "fetching first segment failed or didn't complete within 10s.");
2684 m_error = true;
2685 return false;
2686 }
2687
2688 // set bitrate value used to calculate the size of the stream
2689 HLSStream *hls = GetCurrentStream();
2690 m_bitrate = hls->Bitrate();
2691
2692 // Set initial seek position (relative to m_startup)
2694
2695 /* Initialize HLS live stream thread */
2696 //if (m_live) // commented out as some streams are marked as VOD, yet
2697 // aren't, they are updated over time
2698 {
2699 m_playlistworker = new PlaylistWorker(this, 0ms);
2701 }
2702
2703 return true;
2704}
2705
2706bool HLSRingBuffer::SaveToDisk(const QString &filename, int segstart, int segend)
2707{
2708 // download it all
2709 FILE *fp = fopen(filename.toLatin1().constData(), "w");
2710 if (fp == nullptr)
2711 return false;
2712 int count = NumSegments();
2713 if (segend < 0)
2714 {
2715 segend = count;
2716 }
2717 for (int i = segstart; i < segend; i++)
2718 {
2719 HLSSegment *segment = GetSegment(i);
2720 if (segment == nullptr)
2721 {
2722 LOG(VB_PLAYBACK, LOG_ERR, LOC +
2723 QString("downloading %1 failed").arg(i));
2724 }
2725 else
2726 {
2727 LOG(VB_PLAYBACK, LOG_INFO, LOC +
2728 QString("download of %1 succeeded")
2729 .arg(i));
2730 fwrite(segment->Data(), segment->Size(), 1, fp);
2731 fflush(fp);
2732 }
2733 }
2734 fclose(fp);
2735 return true;
2736}
2737
2738int64_t HLSRingBuffer::SizeMedia(void) const
2739{
2740 if (m_error)
2741 return -1;
2742
2743 HLSStream *hls = GetCurrentStream();
2744 if (nullptr == hls)
2745 return -1;
2746 int64_t size = hls->Duration().count() * m_bitrate / 8;
2747
2748 return size;
2749}
2750
2756{
2757 HLSStream *hls = GetCurrentStream();
2758 bool live = hls ? hls->Live() : false;
2759
2760 // last seek was to end of media, we are just in seek mode so do not wait
2761 if (m_seektoend)
2762 return;
2763
2765 (!live && m_streamworker->IsAtEnd()))
2766 {
2767 return;
2768 }
2769
2770 // danger of getting to the end... pause until we have some more
2771 LOG(VB_PLAYBACK, LOG_INFO, LOC +
2772 QString("pausing until we get sufficient data buffered"));
2775 while (!m_error && !m_interrupted &&
2776 (m_streamworker->CurrentPlaybackBuffer(false) < 2) &&
2777 (live || !m_streamworker->IsAtEnd()))
2778 {
2780 }
2782}
2783
2785{
2786 if (m_error)
2787 return -1;
2788
2789 int used = 0;
2790 int i_read = sz;
2791
2793 if (m_interrupted)
2794 {
2795 LOG(VB_PLAYBACK, LOG_DEBUG, LOC + QString("interrupted"));
2796 return 0;
2797 }
2798
2799 while (i_read > 0 && !m_interrupted)
2800 {
2801 int segnum = m_playback->Segment();
2802 if (segnum >= NumSegments())
2803 {
2804 m_playback->AddOffset(used);
2805 return used;
2806 }
2807 int stream = m_streamworker->StreamForSegment(segnum);
2808 if (stream < 0)
2809 {
2810 // we haven't downloaded this segment yet, likely that it was
2811 // dropped (livetv?)
2813 continue;
2814 }
2815 HLSStream *hls = GetStream(stream);
2816 if (hls == nullptr)
2817 break;
2818 HLSSegment *segment = hls->GetSegment(segnum);
2819 if (segment == nullptr)
2820 break;
2821
2822 segment->Lock();
2823 if (segment->SizePlayed() == segment->Size())
2824 {
2825 if (!hls->Cache() || hls->Live())
2826 {
2827 segment->Clear();
2829 }
2830 else
2831 {
2832 segment->Reset();
2833 }
2834
2836 segment->Unlock();
2837
2838 /* signal download thread we're about to use a new segment */
2840 continue;
2841 }
2842
2843 if (segment->SizePlayed() == 0)
2844 {
2845 LOG(VB_PLAYBACK, LOG_INFO, LOC +
2846 QString("started reading segment %1 [id:%2] from stream %3 (%4 buffered)")
2847 .arg(segnum).arg(segment->Id()).arg(stream)
2849 }
2850
2851 int32_t len = segment->Read((uint8_t*)data + used, i_read, m_fd);
2852 used += len;
2853 i_read -= len;
2854 segment->Unlock();
2855 }
2856
2857 if (m_interrupted)
2858 LOG(VB_PLAYBACK, LOG_DEBUG, LOC + QString("interrupted"));
2859
2860 m_playback->AddOffset(used);
2861 return used;
2862}
2863
2869{
2870 int segnum = m_playback->Segment();
2871 int stream = m_streamworker->StreamForSegment(segnum);
2872 if (stream < 0)
2873 {
2874 return 0;
2875 }
2876 HLSStream *hls = GetStream(stream);
2877 if (hls == nullptr)
2878 {
2879 return 0;
2880 }
2881 HLSSegment *segment = hls->GetSegment(segnum);
2882 if (segment == nullptr)
2883 {
2884 return 0;
2885 }
2886 auto byterate = (uint64_t)(((double)segment->Size()) /
2887 ((double)segment->Duration().count()));
2888
2889 return (int)((size * 1000.0) / byterate);
2890}
2891
2893{
2894 QReadLocker lock(&m_rwLock);
2895 return SizeMedia();
2896}
2897
2898long long HLSRingBuffer::SeekInternal(long long pos, int whence)
2899{
2900 if (m_error)
2901 return -1;
2902
2903 if (!IsSeekingAllowed())
2904 {
2905 return m_playback->Offset();
2906 }
2907
2908 std::chrono::microseconds starting = mdate();
2909
2910 QWriteLocker lock(&m_posLock);
2911
2912 int totalsize = SizeMedia();
2913 int64_t where = 0;
2914 switch (whence)
2915 {
2916 case SEEK_CUR:
2917 // return current location, nothing to do
2918 if (pos == 0)
2919 {
2920 return m_playback->Offset();
2921 }
2922 where = m_playback->Offset() + pos;
2923 break;
2924 case SEEK_END:
2925 where = SizeMedia() - pos;
2926 break;
2927 case SEEK_SET:
2928 default:
2929 where = pos;
2930 break;
2931 }
2932
2933 // We determine the duration at which it was really attempting to seek to
2934 auto postime = secondsFromFloat((where * 8.0) / m_bitrate);
2935 int count = NumSegments();
2936 int segnum = m_playback->Segment();
2937 HLSStream *hls = GetStreamForSegment(segnum);
2938
2939 /* restore current segment's file position indicator to 0 */
2940 HLSSegment *segment = hls->GetSegment(segnum);
2941 if (segment != nullptr)
2942 {
2943 segment->Lock();
2944 segment->Reset();
2945 segment->Unlock();
2946 }
2947
2948 if (where > totalsize)
2949 {
2950 // we're at the end, never let seek after last 3 segments
2951 postime -= hls->TargetDuration() * 3;
2952 if (postime < 0s)
2953 {
2954 postime = 0s;
2955 }
2956 }
2957
2958 // Find segment containing position
2959 std::chrono::seconds starttime = 0s;
2960 std::chrono::seconds endtime = 0s;
2961 for (int n = m_startup; n < count; n++)
2962 {
2963 hls = GetStreamForSegment(n);
2964 if (hls == nullptr)
2965 {
2966 // error, should never happen, irrecoverable error
2967 return -1;
2968 }
2969 segment = hls->GetSegment(n);
2970 if (segment == nullptr)
2971 {
2972 // stream doesn't contain segment error can't continue,
2973 // unknown error
2974 return -1;
2975 }
2976 endtime += segment->Duration();
2977 if (postime < endtime)
2978 {
2979 segnum = n;
2980 break;
2981 }
2982 starttime = endtime;
2983 }
2984
2985 /*
2986 * Live Mode exception:
2987 * FFmpeg seek to the last segment in order to determine the size of the video
2988 * so do not allow seeking to the last segment if in live mode as we don't care
2989 * about the size
2990 * Also do not allow to seek before the current playback segment as segment
2991 * has been cleared from memory
2992 * We only let determine the size if the bandwidth would allow fetching the
2993 * the segments in less than 5s
2994 */
2995 if (hls->Live() && (segnum >= count - 1 || segnum < m_playback->Segment()) &&
2996 ((hls->TargetDuration() * hls->Bitrate() / m_streamworker->Bandwidth()) > 5s))
2997 {
2998 return m_playback->Offset();
2999 }
3000 m_seektoend = segnum >= count - 1;
3001
3002 m_playback->SetSegment(segnum);
3003
3004 m_streamworker->Seek(segnum);
3005 m_playback->SetOffset(postime.count() * m_bitrate / 8);
3006
3008
3009 /* Wait for download to be finished and to buffer 3 segment */
3010 LOG(VB_PLAYBACK, LOG_INFO, LOC +
3011 QString("seek to segment %1").arg(segnum));
3012
3013 // see if we've already got the segment, and at least 2 buffered after
3014 // then no need to wait for streamworker
3015 while (!m_error && !m_interrupted &&
3016 (!m_streamworker->GotBufferedSegments(segnum, 2) &&
3017 (m_streamworker->CurrentPlaybackBuffer(false) < 2) &&
3019 {
3021 }
3022 if (m_interrupted)
3023 LOG(VB_PLAYBACK, LOG_DEBUG, LOC + QString("interrupted"));
3024
3026
3027 // now seek within found segment
3028 int stream = m_streamworker->StreamForSegment(segnum);
3029 if (stream < 0)
3030 {
3031 // segment didn't get downloaded (timeout?)
3032 LOG(VB_PLAYBACK, LOG_ERR, LOC +
3033 QString("seek error: segment %1 should have been downloaded, but didn't."
3034 " Playback will stall")
3035 .arg(segnum));
3036 }
3037 else
3038 {
3039 if (segment == nullptr) // can never happen, make coverity happy
3040 {
3041 // stream doesn't contain segment error can't continue,
3042 // unknown error
3043 return -1;
3044 }
3045 int32_t skip = ((postime - starttime) * segment->Size()) / segment->Duration();
3046 segment->Read(nullptr, skip);
3047 }
3048 LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("seek completed in %1s")
3049 .arg(duration_cast<std::chrono::seconds>(mdate() - starting).count()));
3050
3051 return m_playback->Offset();
3052}
3053
3055{
3056 if (m_error)
3057 return 0;
3058 return m_playback->Offset();
3059}
3060
3062{
3063 return !m_error && !m_streams.isEmpty() && NumSegments() > 0;
3064}
3065
3067{
3068 QMutexLocker lock(&m_lock);
3069
3070 // segment didn't get downloaded (timeout?)
3071 LOG(VB_PLAYBACK, LOG_DEBUG, LOC + QString("requesting interrupt"));
3072 m_interrupted = true;
3073}
3074
3076{
3077 QMutexLocker lock(&m_lock);
3078
3079 // segment didn't get downloaded (timeout?)
3080 LOG(VB_PLAYBACK, LOG_DEBUG, LOC + QString("requesting restart"));
3081 m_interrupted = false;
3082}
static constexpr uint8_t AES128_KEY_SIZE
Definition: HLSStream.h:20
void SetStream(int val)
void SetOffset(uint64_t val)
HLSPlayback(void)=default
uint64_t Offset(void) const
void SetSegment(int val)
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
HLSPlayback * m_playback
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)
volatile bool m_interrupted
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)
~HLSSegment()=default
int32_t Size(void) const
QString Title(void) const
bool IsEmpty(void) const
QString Url(void) const
HLSSegment(const std::chrono::seconds mduration, const int id, QString title, QString uri, QString current_key_path)
int Id(void) const
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
bool Live(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)
int Version(void) const
HLSStream(const int mid, const uint64_t bitrate, QString uri)
void RemoveListSegments(QHash< HLSSegment *, bool > &table)
void SetLive(bool x)
QString Url(void) const
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 Cache(void) const
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,...
void SetCache(bool x)
int Id(void) const
std::chrono::seconds m_targetduration
void SetVersion(int x)
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.
Definition: mthread.h:49
void RunProlog(void)
Sets up a thread, call this if you reimplement run().
Definition: mthread.cpp:180
void start(QThread::Priority p=QThread::InheritPriority)
Tell MThread to start running the thread in the near future.
Definition: mthread.cpp:267
void RunEpilog(void)
Cleans up a thread's resources, call this if you reimplement run().
Definition: mthread.cpp:193
bool wait(std::chrono::milliseconds time=std::chrono::milliseconds::max())
Wait for the MThread to exit, with a maximum timeout.
Definition: mthread.cpp:284
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.
void KillReadAheadThread(void)
Stops the read-ahead thread, and waits for it to stop.
QReadWriteLock m_posLock
static void AVFormatInitNetwork(void)
QReadWriteLock m_rwLock
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.
HLSRingBuffer * m_parent
PlaylistWorker(HLSRingBuffer *parent, std::chrono::milliseconds wait)
void WaitForSignal(std::chrono::milliseconds time=std::chrono::milliseconds::max())
QWaitCondition m_waitcond
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]
void SetBuffer(int val)
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
HLSRingBuffer * m_parent
volatile bool m_interrupted
QMap< int, int > m_segmap
int64_t Bandwidth(void) const
void RemoveSegmentFromStream(int segnum)
unsigned int uint
Definition: compat.h:60
static QString decoded_URI(const QString &uri)
#define LOC
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::chrono::seconds secondsFromFloat(T value)
Helper function for convert a floating point number to a duration.
Definition: mythchrono.h:69
MythDownloadManager * GetMythDownloadManager(void)
Gets the pointer to the MythDownloadManager singleton.
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
@ kMythBufferHLS
static int startup()
MBASE_PUBLIC long long copy(QFile &dst, QFile &src, uint block_size=0)
Copies src file to dst file.
string version
Definition: giantbomb.py:185
int FILE
Definition: mythburn.py:137
std::array< uint8_t, AES128_KEY_SIZE > key
Definition: HLSStream.h:22