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::copy(IV.cbegin(), IV.cend(), 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::copy(ba.cbegin(), ba.cend(), 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 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 for (int i = 0; i < streams; i++)
1338 {
1339 HLSStream *hls = m_parent->GetStream(i);
1340 if (hls)
1341 {
1342 listurls.append(hls->Url());
1343 }
1344 }
1345 m_lock.unlock();
1346 cancelURL(listurls);
1347 wait();
1348 }
1349
1350 void Wakeup(void)
1351 {
1352 m_lock.lock();
1353 m_wokenup = true;
1354 m_lock.unlock();
1355 // send a wake signal
1356 m_waitcond.wakeAll();
1357 }
1358 void WaitForSignal(std::chrono::milliseconds time = std::chrono::milliseconds::max())
1359 {
1360 // must own lock
1361 m_waitcond.wait(&m_lock, time.count());
1362 }
1363 void Lock(void)
1364 {
1365 m_lock.lock();
1366 }
1367 void Unlock(void)
1368 {
1369 m_lock.unlock();
1370 }
1371
1372protected:
1373 void run(void) override // MThread
1374 {
1375 RunProlog();
1376
1378 bool live = hls ? hls->Live() : false;
1379 double wait = 0.5;
1380 double factor = live ? 1.0 : 2.0;
1381
1382 QWaitCondition mcond;
1383
1384 while (!m_interrupted)
1385 {
1386 if (m_parent->m_streamworker == nullptr)
1387 {
1388 // streamworker not running
1389 LOG(VB_PLAYBACK, LOG_ERR, LOC +
1390 "StreamWorker not running, aborting live playback");
1391 m_interrupted = true;
1392 break;
1393 }
1394
1395 Lock();
1396 if (!m_wokenup)
1397 {
1398 std::chrono::milliseconds waittime = std::max(100ms, m_wakeup);
1399 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1400 QString("PlayListWorker refreshing in %1s")
1401 .arg(duration_cast<std::chrono::seconds>(waittime).count()));
1402 WaitForSignal(waittime);
1403 }
1404 m_wokenup = false;
1405 Unlock();
1406
1407 /* reload the m3u8 */
1408 if (ReloadPlaylist() != RET_OK)
1409 {
1410 /* No change in playlist, then backoff */
1411 m_retries++;
1412 if (m_retries == 1) wait = 0.5;
1413 else if (m_retries == 2) wait = 1;
1414 else if (m_retries >= 3) wait = 2;
1415
1416 // If we haven't been able to reload the playlist after x times
1417 // it probably means the stream got deleted, so abort
1419 {
1420 LOG(VB_PLAYBACK, LOG_ERR, LOC +
1421 QString("reloading the playlist failed after %1 attempts."
1422 "aborting.").arg(PLAYLIST_FAILURE));
1423 m_parent->m_error = true;
1424 }
1425
1426 /* Can we afford to backoff? */
1428 {
1429 if (m_retries == 1)
1430 continue; // restart immediately if it's the first try
1431 m_retries = 0;
1432 wait = 0.5;
1433 }
1434 }
1435 else
1436 {
1437 // make streamworker process things
1439 m_retries = 0;
1440 wait = 0.5;
1441 }
1442
1443 if (hls == nullptr)
1444 {
1445 // an irrevocable error has occured. exit
1446 LOG(VB_PLAYBACK, LOG_ERR, LOC +
1447 "unable to retrieve current stream, aborting live playback");
1448 m_interrupted = true;
1449 break;
1450 }
1451
1452 /* determine next time to update playlist */
1453 m_wakeup = duration_cast<std::chrono::milliseconds>(
1454 hls->TargetDuration() * wait * factor);
1455 }
1456
1457 RunEpilog();
1458 }
1459
1460private:
1465 {
1466 auto *streams = new StreamsList;
1467
1468 LOG(VB_PLAYBACK, LOG_INFO, LOC + "reloading HLS live meta playlist");
1469
1470 if (GetHTTPLiveMetaPlaylist(streams) != RET_OK)
1471 {
1472 LOG(VB_PLAYBACK, LOG_ERR, LOC + "reloading playlist failed");
1473 m_parent->FreeStreamsList(streams);
1474 return RET_ERROR;
1475 }
1476
1477 /* merge playlists */
1478 int count = streams->size();
1479 for (int n = 0; n < count; n++)
1480 {
1481 HLSStream *hls_new = m_parent->GetStream(n, streams);
1482 if (hls_new == nullptr)
1483 continue;
1484
1485 HLSStream *hls_old = m_parent->FindStream(hls_new);
1486 if (hls_old == nullptr)
1487 { /* new hls stream - append */
1488 m_parent->m_streams.append(hls_new);
1489 LOG(VB_PLAYBACK, LOG_INFO, LOC +
1490 QString("new HLS stream appended (id=%1, bitrate=%2)")
1491 .arg(hls_new->Id()).arg(hls_new->Bitrate()));
1492 }
1493 else if (UpdatePlaylist(hls_new, hls_old) != RET_OK)
1494 {
1495 LOG(VB_PLAYBACK, LOG_ERR, LOC +
1496 QString("failed updating HLS stream (id=%1, bandwidth=%2)")
1497 .arg(hls_new->Id()).arg(hls_new->Bitrate()));
1498 m_parent->FreeStreamsList(streams);
1499 return RET_ERROR;
1500 }
1501 }
1502 delete streams;
1503 return RET_OK;
1504 }
1505
1506 static int UpdatePlaylist(HLSStream *hls_new, HLSStream *hls)
1507 {
1508 int count = hls_new->NumSegments();
1509
1510 LOG(VB_PLAYBACK, LOG_INFO, LOC +
1511 QString("updated hls stream (program-id=%1, bitrate=%2) has %3 segments")
1512 .arg(hls_new->Id()).arg(hls_new->Bitrate()).arg(count));
1513 QHash<HLSSegment*,bool> table;
1514
1515 for (int n = 0; n < count; n++)
1516 {
1517 HLSSegment *p = hls_new->GetSegment(n);
1518 if (p == nullptr)
1519 return RET_ERROR;
1520
1521 hls->Lock();
1522 HLSSegment *segment = hls->FindSegment(p->Id());
1523 if (segment)
1524 {
1525 segment->Lock();
1526 /* they should be the same */
1527 if ((p->Id() != segment->Id()) ||
1528 (p->Duration() != segment->Duration()) ||
1529 (p->Url() != segment->Url()))
1530 {
1531 LOG(VB_PLAYBACK, LOG_WARNING, LOC +
1532 QString("existing segment found with different content - resetting"));
1533 LOG(VB_PLAYBACK, LOG_WARNING, LOC +
1534 QString("- id: new=%1, old=%2")
1535 .arg(p->Id()).arg(segment->Id()));
1536 LOG(VB_PLAYBACK, LOG_WARNING, LOC +
1537 QString("- duration: new=%1, old=%2")
1538 .arg(p->Duration().count()).arg(segment->Duration().count()));
1539 LOG(VB_PLAYBACK, LOG_WARNING, LOC +
1540 QString("- file: new=%1 old=%2")
1541 .arg(p->Url(), segment->Url()));
1542
1543 /* Resetting content */
1544 *segment = *p;
1545 }
1546 // mark segment to be removed from new stream, and deleted
1547 table.insert(p, true);
1548 segment->Unlock();
1549 }
1550 else
1551 {
1552 int last = hls->NumSegments() - 1;
1553 HLSSegment *l = hls->GetSegment(last);
1554 if (l == nullptr)
1555 {
1556 hls->Unlock();
1557 return RET_ERROR;
1558 }
1559
1560 if ((l->Id() + 1) != p->Id())
1561 {
1562 LOG(VB_PLAYBACK, LOG_ERR, LOC +
1563 QString("gap in id numbers found: new=%1 expected %2")
1564 .arg(p->Id()).arg(l->Id()+1));
1565 }
1566 hls->AppendSegment(p);
1567 LOG(VB_PLAYBACK, LOG_INFO, LOC +
1568 QString("- segment %1 appended")
1569 .arg(p->Id()));
1570 // segment was moved to another stream, so do not delete it
1571 table.insert(p, false);
1572 }
1573 hls->Unlock();
1574 }
1575 hls_new->RemoveListSegments(table);
1576
1577 /* update meta information */
1578 hls->UpdateWith(*hls_new);
1579 return RET_OK;
1580 }
1581
1583 {
1584 int err = RET_ERROR;
1585
1586 /* Duplicate HLS stream META information */
1587 for (int i = 0; i < m_parent->m_streams.size() && !m_interrupted; i++)
1588 {
1589 auto *src = m_parent->GetStream(i);
1590 if (src == nullptr)
1591 return RET_ERROR;
1592
1593 auto *dst = new HLSStream(*src);
1594 streams->append(dst);
1595
1596 /* Download playlist file from server */
1597 QByteArray buffer;
1598 if (!downloadURL(dst->Url(), &buffer) || m_interrupted)
1599 {
1600 return RET_ERROR;
1601 }
1602 /* Parse HLS m3u8 content. */
1603 err = m_parent->ParseM3U8(&buffer, streams);
1604 }
1605 m_parent->SanitizeStreams(streams);
1606 return err;
1607 }
1608
1609 // private variable members
1611 bool m_interrupted {false};
1612 std::chrono::milliseconds m_wakeup; // next reload time
1613 int m_retries {0}; // number of consecutive failures
1614 bool m_wokenup {false};
1615 QMutex m_lock;
1616 QWaitCondition m_waitcond;
1617};
1618
1619HLSRingBuffer::HLSRingBuffer(const QString &lfilename) :
1621 m_playback(new HLSPlayback())
1622{
1623 m_startReadAhead = false;
1624 HLSRingBuffer::OpenFile(lfilename);
1625}
1626
1627HLSRingBuffer::HLSRingBuffer(const QString &lfilename, bool open) :
1629 m_playback(new HLSPlayback())
1630{
1631 m_startReadAhead = false;
1632 if (open)
1633 {
1634 HLSRingBuffer::OpenFile(lfilename);
1635 }
1636}
1637
1639{
1641
1642 QWriteLocker lock(&m_rwLock);
1643
1644 m_killed = true;
1645
1646 if (m_playlistworker)
1647 {
1649 delete m_playlistworker;
1650 }
1651 // stream worker must be deleted after playlist worker
1652 if (m_streamworker)
1653 {
1655 delete m_streamworker;
1656 }
1658 delete m_playback;
1659 if (m_fd)
1660 {
1661 fclose(m_fd);
1662 }
1663}
1664
1666{
1667 /* Free hls streams */
1668 for (int i = 0; i < streams->size(); i++)
1669 {
1670 HLSStream *hls = GetStream(i, streams);
1671 delete hls;
1672 }
1673 if (streams != &m_streams)
1674 {
1675 delete streams;
1676 }
1677}
1678
1680{
1681 int stream = m_streamworker->StreamForSegment(segnum);
1682 if (stream < 0)
1683 {
1684 return GetCurrentStream();
1685 }
1686 return GetStream(stream);
1687}
1688
1689HLSStream *HLSRingBuffer::GetStream(const int wanted, const StreamsList *streams) const
1690{
1691 if (streams == nullptr)
1692 {
1693 streams = &m_streams;
1694 }
1695 int count = streams->size();
1696 if (count <= 0)
1697 return nullptr;
1698 if ((wanted < 0) || (wanted >= count))
1699 return nullptr;
1700 return streams->at(wanted);
1701}
1702
1704{
1705 return GetStream(0, streams);
1706}
1707
1709{
1710 if (streams == nullptr)
1711 {
1712 streams = &m_streams;
1713 }
1714 int count = streams->size();
1715 if (count <= 0)
1716 return nullptr;
1717 count--;
1718 return GetStream(count, streams);
1719}
1720
1722 const StreamsList *streams) const
1723{
1724 if (streams == nullptr)
1725 {
1726 streams = &m_streams;
1727 }
1728 int count = streams->size();
1729 for (int n = 0; n < count; n++)
1730 {
1731 HLSStream *hls = GetStream(n, streams);
1732 if (hls)
1733 {
1734 /* compare */
1735 if ((hls->Id() == hls_new->Id()) &&
1736 ((hls->Bitrate() == hls_new->Bitrate()) ||
1737 (hls_new->Bitrate() == 0)))
1738 {
1739 return hls;
1740 }
1741 }
1742 }
1743 return nullptr;
1744}
1745
1750{
1751 if (!m_streamworker)
1752 {
1753 return nullptr;
1754 }
1756}
1757
1759{
1760 if (!s || s->size() < 7)
1761 return false;
1762
1763 if (!s->startsWith((const char*)"#EXTM3U"))
1764 return false;
1765
1766 QTextStream stream(s);
1767 /* Parse stream and search for
1768 * EXT-X-TARGETDURATION or EXT-X-STREAM-INF tag, see
1769 * http://tools.ietf.org/html/draft-pantos-http-live-streaming-04#page-8 */
1770 while (true)
1771 {
1772 QString line = stream.readLine();
1773 if (line.isNull())
1774 break;
1775 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1776 QString("IsHTTPLiveStreaming: %1").arg(line));
1777 if (line.startsWith(QLatin1String("#EXT-X-TARGETDURATION")) ||
1778 line.startsWith(QLatin1String("#EXT-X-MEDIA-SEQUENCE")) ||
1779 line.startsWith(QLatin1String("#EXT-X-KEY")) ||
1780 line.startsWith(QLatin1String("#EXT-X-ALLOW-CACHE")) ||
1781 line.startsWith(QLatin1String("#EXT-X-ENDLIST")) ||
1782 line.startsWith(QLatin1String("#EXT-X-STREAM-INF")) ||
1783 line.startsWith(QLatin1String("#EXT-X-DISCONTINUITY")) ||
1784 line.startsWith(QLatin1String("#EXT-X-VERSION")))
1785 {
1786 return true;
1787 }
1788 }
1789 return false;
1790}
1791
1793{
1794 bool isHLS = false;
1795
1796 // Do a peek on the URL to test the format
1798
1799 AVIOContext* context = nullptr;
1800 int ret = avio_open(&context, filename.toLatin1().constData(), AVIO_FLAG_READ);
1801 if (ret >= 0)
1802 {
1803 std::array<uint8_t,1024> buffer {};
1804 ret = avio_read(context, buffer.data(), buffer.size());
1805 if (ret > 0)
1806 {
1807 QByteArray ba((const char*)buffer.data(), ret);
1808 isHLS = IsHTTPLiveStreaming(&ba);
1809 }
1810 avio_closep(&context);
1811 }
1812 else
1813 {
1814 // couldn't peek, rely on URL analysis
1815 QUrl url { filename };
1816 isHLS =
1817 url.path().endsWith(QLatin1String("m3u8"), Qt::CaseInsensitive) ||
1818 url.query( QUrl::FullyEncoded ).contains(QLatin1String("m3u8"), Qt::CaseInsensitive);
1819 }
1820 return isHLS;
1821}
1822
1823/* Parsing */
1824QString HLSRingBuffer::ParseAttributes(const QString &line, const char *attr)
1825{
1826 int p = line.indexOf(QLatin1String(":"));
1827 if (p < 0)
1828 return {};
1829
1830 QStringList list = line.mid(p+1).split(',');
1831 for (const auto& it : std::as_const(list))
1832 {
1833 QString arg = it.trimmed();
1834 if (arg.startsWith(attr))
1835 {
1836 int pos = arg.indexOf(QLatin1String("="));
1837 if (pos < 0)
1838 continue;
1839 return arg.mid(pos+1);
1840 }
1841 }
1842 return {};
1843}
1844
1849int HLSRingBuffer::ParseDecimalValue(const QString &line, int &target)
1850{
1851 int p = line.indexOf(QLatin1String(":"));
1852 if (p < 0)
1853 return RET_ERROR;
1854 int i = p + 1;
1855 for ( ; i < line.size(); i++)
1856 if (!line[i].isNumber())
1857 break;
1858 if (i == p + 1)
1859 return RET_ERROR;
1860 target = line.mid(p+1, i - p - 1).toInt();
1861 return RET_OK;
1862}
1863
1864int HLSRingBuffer::ParseSegmentInformation(const HLSStream *hls, const QString &line,
1865 int &duration, QString &title)
1866{
1867 /*
1868 * #EXTINF:<duration>,<title>
1869 *
1870 * "duration" is an integer that specifies the duration of the media
1871 * file in seconds. Durations SHOULD be rounded to the nearest integer.
1872 * The remainder of the line following the comma is the title of the
1873 * media file, which is an optional human-readable informative title of
1874 * the media segment
1875 */
1876 int p = line.indexOf(QLatin1String(":"));
1877 if (p < 0)
1878 return RET_ERROR;
1879
1880 QStringList list = line.mid(p+1).split(',');
1881
1882 /* read duration */
1883 if (list.isEmpty())
1884 {
1885 return RET_ERROR;
1886 }
1887 const QString& val = list[0];
1888
1889 if (hls->Version() < 3)
1890 {
1891 bool ok = false;
1892 duration = val.toInt(&ok);
1893 if (!ok)
1894 {
1895 duration = -1;
1896 return RET_ERROR;
1897 }
1898 }
1899 else
1900 {
1901 bool ok = false;
1902 double d = val.toDouble(&ok);
1903 if (!ok)
1904 {
1905 duration = -1;
1906 return RET_ERROR;
1907 }
1908 if ((d) - ((int)d) >= 0.5)
1909 duration = ((int)d) + 1;
1910 else
1911 duration = ((int)d);
1912 }
1913
1914 if (list.size() >= 2)
1915 {
1916 title = list[1];
1917 }
1918
1919 /* Ignore the rest of the line */
1920 return RET_OK;
1921}
1922
1923int HLSRingBuffer::ParseTargetDuration(HLSStream *hls, const QString &line)
1924{
1925 /*
1926 * #EXT-X-TARGETDURATION:<s>
1927 *
1928 * where s is an integer indicating the target duration in seconds.
1929 */
1930 int duration = -1;
1931
1932 if (ParseDecimalValue(line, duration) != RET_OK)
1933 {
1934 LOG(VB_PLAYBACK, LOG_ERR, LOC + "expected #EXT-X-TARGETDURATION:<s>");
1935 return RET_ERROR;
1936 }
1937 hls->SetTargetDuration(std::chrono::seconds(duration));
1938 return RET_OK;
1939}
1940
1941HLSStream *HLSRingBuffer::ParseStreamInformation(const QString &line, const QString &uri) const
1942{
1943 /*
1944 * #EXT-X-STREAM-INF:[attribute=value][,attribute=value]*
1945 * <URI>
1946 */
1947 int id = 0;
1948 QString attr;
1949
1950 attr = ParseAttributes(line, "PROGRAM-ID");
1951 if (attr.isNull())
1952 {
1953 LOG(VB_PLAYBACK, LOG_INFO, LOC + "#EXT-X-STREAM-INF: expected PROGRAM-ID=<value>, using -1");
1954 id = -1;
1955 }
1956 else
1957 {
1958 id = attr.toInt();
1959 }
1960
1961 attr = ParseAttributes(line, "BANDWIDTH");
1962 if (attr.isNull())
1963 {
1964 LOG(VB_PLAYBACK, LOG_ERR, LOC + "#EXT-X-STREAM-INF: expected BANDWIDTH=<value>");
1965 return nullptr;
1966 }
1967 uint64_t bw = attr.toInt();
1968
1969 if (bw == 0)
1970 {
1971 LOG(VB_PLAYBACK, LOG_ERR, LOC + "#EXT-X-STREAM-INF: bandwidth cannot be 0");
1972 return nullptr;
1973 }
1974
1975 LOG(VB_PLAYBACK, LOG_INFO, LOC +
1976 QString("bandwidth adaptation detected (program-id=%1, bandwidth=%2")
1977 .arg(id).arg(bw));
1978
1979 QString psz_uri = relative_URI(m_m3u8, uri);
1980
1981 return new HLSStream(id, bw, psz_uri);
1982}
1983
1984int HLSRingBuffer::ParseMediaSequence(HLSStream *hls, const QString &line)
1985{
1986 /*
1987 * #EXT-X-MEDIA-SEQUENCE:<number>
1988 *
1989 * A Playlist file MUST NOT contain more than one EXT-X-MEDIA-SEQUENCE
1990 * tag. If the Playlist file does not contain an EXT-X-MEDIA-SEQUENCE
1991 * tag then the sequence number of the first URI in the playlist SHALL
1992 * be considered to be 0.
1993 */
1994 int sequence = 0;
1995
1996 if (ParseDecimalValue(line, sequence) != RET_OK)
1997 {
1998 LOG(VB_PLAYBACK, LOG_ERR, LOC + "expected #EXT-X-MEDIA-SEQUENCE:<s>");
1999 return RET_ERROR;
2000 }
2001
2002 if (hls->StartSequence() > 0 && !hls->Live())
2003 {
2004 LOG(VB_PLAYBACK, LOG_ERR, LOC +
2005 QString("EXT-X-MEDIA-SEQUENCE already present in playlist (new=%1, old=%2)")
2006 .arg(sequence).arg(hls->StartSequence()));
2007 }
2008 hls->SetStartSequence(sequence);
2009 return RET_OK;
2010}
2011
2012
2013int HLSRingBuffer::ParseKey(HLSStream *hls, const QString &line)
2014{
2015 /*
2016 * #EXT-X-KEY:METHOD=<method>[,URI="<URI>"][,IV=<IV>]
2017 *
2018 * The METHOD attribute specifies the encryption method. Two encryption
2019 * methods are defined: NONE and AES-128.
2020 */
2021 int err = 0;
2022 QString attr = ParseAttributes(line, "METHOD");
2023 if (attr.isNull())
2024 {
2025 LOG(VB_PLAYBACK, LOG_ERR, LOC + "#EXT-X-KEY: expected METHOD=<value>");
2026 return RET_ERROR;
2027 }
2028
2029 if (attr.startsWith(QLatin1String("NONE")))
2030 {
2031 QString uri = ParseAttributes(line, "URI");
2032 if (!uri.isNull())
2033 {
2034 LOG(VB_PLAYBACK, LOG_ERR, LOC + "#EXT-X-KEY: URI not expected");
2035 err = RET_ERROR;
2036 }
2037 /* IV is only supported in version 2 and above */
2038 if (hls->Version() >= 2)
2039 {
2040 QString iv = ParseAttributes(line, "IV");
2041 if (!iv.isNull())
2042 {
2043 LOG(VB_PLAYBACK, LOG_ERR, LOC + "#EXT-X-KEY: IV not expected");
2044 err = RET_ERROR;
2045 }
2046 }
2047 }
2048#if CONFIG_LIBCRYPTO
2049 else if (attr.startsWith(QLatin1String("AES-128")))
2050 {
2051 QString uri;
2052 QString iv;
2053 if (!m_aesmsg)
2054 {
2055 LOG(VB_PLAYBACK, LOG_INFO, LOC +
2056 "playback of AES-128 encrypted HTTP Live media detected.");
2057 m_aesmsg = true;
2058 }
2059 uri = ParseAttributes(line, "URI");
2060 if (uri.isNull())
2061 {
2062 LOG(VB_PLAYBACK, LOG_ERR, LOC +
2063 "#EXT-X-KEY: URI not found for encrypted HTTP Live media in AES-128");
2064 return RET_ERROR;
2065 }
2066
2067 /* Url is between quotes, remove them */
2068 hls->SetKeyPath(decoded_URI(uri.remove(QChar(QLatin1Char('"')))));
2069
2070 iv = ParseAttributes(line, "IV");
2071 if (!iv.isNull() && !hls->SetAESIV(iv))
2072 {
2073 LOG(VB_PLAYBACK, LOG_ERR, LOC + "invalid IV");
2074 err = RET_ERROR;
2075 }
2076 }
2077#endif
2078 else
2079 {
2080 LOG(VB_PLAYBACK, LOG_ERR, LOC +
2081 "invalid encryption type, only NONE "
2082#if CONFIG_LIBCRYPTO
2083 "and AES-128 are supported"
2084#else
2085 "is supported."
2086#endif
2087 );
2088 err = RET_ERROR;
2089 }
2090 return err;
2091}
2092
2093int HLSRingBuffer::ParseProgramDateTime(HLSStream */*hls*/, const QString &line)
2094{
2095 /*
2096 * #EXT-X-PROGRAM-DATE-TIME:<YYYY-MM-DDThh:mm:ssZ>
2097 */
2098 LOG(VB_PLAYBACK, LOG_WARNING, LOC +
2099 QString("tag not supported: #EXT-X-PROGRAM-DATE-TIME %1")
2100 .arg(line));
2101 return RET_OK;
2102}
2103
2104int HLSRingBuffer::ParseAllowCache(HLSStream *hls, const QString &line)
2105{
2106 /*
2107 * The EXT-X-ALLOW-CACHE tag indicates whether the client MAY or MUST
2108 * NOT cache downloaded media files for later replay. It MAY occur
2109 * anywhere in the Playlist file; it MUST NOT occur more than once. The
2110 * EXT-X-ALLOW-CACHE tag applies to all segments in the playlist. Its
2111 * format is:
2112 *
2113 * #EXT-X-ALLOW-CACHE:<YES|NO>
2114 */
2115 int pos = line.indexOf(QLatin1String(":"));
2116 if (pos < 0)
2117 return RET_ERROR;
2118 QString answer = line.mid(pos+1, 3);
2119 if (answer.size() < 2)
2120 {
2121 LOG(VB_PLAYBACK, LOG_ERR, LOC + "#EXT-X-ALLOW-CACHE, ignoring ...");
2122 return RET_ERROR;
2123 }
2124 hls->SetCache(!answer.startsWith(QLatin1String("NO")));
2125 return RET_OK;
2126}
2127
2128int HLSRingBuffer::ParseVersion(const QString &line, int &version)
2129{
2130 /*
2131 * The EXT-X-VERSION tag indicates the compatibility version of the
2132 * Playlist file. The Playlist file, its associated media, and its
2133 * server MUST comply with all provisions of the most-recent version of
2134 * this document describing the protocol version indicated by the tag
2135 * value.
2136 *
2137 * Its format is:
2138 *
2139 * #EXT-X-VERSION:<n>
2140 */
2141
2142 if (ParseDecimalValue(line, version) != RET_OK)
2143 {
2144 LOG(VB_PLAYBACK, LOG_ERR, LOC +
2145 "#EXT-X-VERSION: no protocol version found, should be version 1.");
2146 return RET_ERROR;
2147 }
2148
2149 if (version <= 0 || version > 3)
2150 {
2151 LOG(VB_PLAYBACK, LOG_ERR, LOC +
2152 QString("#EXT-X-VERSION should be version 1, 2 or 3 iso %1")
2153 .arg(version));
2154 return RET_ERROR;
2155 }
2156 return RET_OK;
2157}
2158
2160{
2161 /*
2162 * The EXT-X-ENDLIST tag indicates that no more media files will be
2163 * added to the Playlist file. It MAY occur anywhere in the Playlist
2164 * file; it MUST NOT occur more than once. Its format is:
2165 */
2166 hls->SetLive(false);
2167 LOG(VB_PLAYBACK, LOG_INFO, LOC + "video on demand (vod) mode");
2168 return RET_OK;
2169}
2170
2171int HLSRingBuffer::ParseDiscontinuity(HLSStream */*hls*/, const QString &line)
2172{
2173 /* Not handled, never seen so far */
2174 LOG(VB_PLAYBACK, LOG_DEBUG, LOC + QString("#EXT-X-DISCONTINUITY %1").arg(line));
2175 return RET_OK;
2176}
2177
2178int HLSRingBuffer::ParseM3U8(const QByteArray *buffer, StreamsList *streams)
2179{
2187 if (streams == nullptr)
2188 {
2189 streams = &m_streams;
2190 }
2191 QTextStream stream(*buffer);
2192#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
2193 stream.setCodec("UTF-8");
2194#else
2195 stream.setEncoding(QStringConverter::Utf8);
2196#endif
2197
2198 QString line = stream.readLine();
2199 if (line.isNull())
2200 return RET_ERROR;
2201
2202 if (!line.startsWith(QLatin1String("#EXTM3U")))
2203 {
2204 LOG(VB_PLAYBACK, LOG_ERR, LOC + "missing #EXTM3U tag .. aborting");
2205 return RET_ERROR;
2206 }
2207
2208 /* What is the version ? */
2209 int version = 1;
2210 int p = buffer->indexOf("#EXT-X-VERSION:");
2211 if (p >= 0)
2212 {
2213 stream.seek(p);
2214 QString psz_version = stream.readLine();
2215 if (psz_version.isNull())
2216 return RET_ERROR;
2217 int ret = ParseVersion(psz_version, version);
2218 if (ret != RET_OK)
2219 {
2220 LOG(VB_GENERAL, LOG_WARNING, LOC +
2221 "#EXT-X-VERSION: no protocol version found, assuming version 1.");
2222 version = 1;
2223 }
2224 }
2225
2226 /* Is it a meta index file ? */
2227 bool meta = buffer->indexOf("#EXT-X-STREAM-INF") >= 0;
2228
2229 int err = RET_OK;
2230
2231 if (meta)
2232 {
2233 LOG(VB_PLAYBACK, LOG_INFO, LOC + "Meta playlist");
2234
2235 /* M3U8 Meta Index file */
2236 stream.seek(0); // rewind
2237 while (!m_killed)
2238 {
2239 line = stream.readLine();
2240 if (line.isNull())
2241 break;
2242
2243 if (line.startsWith(QLatin1String("#EXT-X-STREAM-INF")))
2244 {
2245 m_meta = true;
2246 QString uri = stream.readLine();
2247 if (uri.isNull())
2248 {
2249 err = RET_ERROR;
2250 break;
2251 }
2252 if (uri.startsWith(QLatin1String("#")))
2253 {
2254 LOG(VB_GENERAL, LOG_INFO, LOC +
2255 QString("Skipping invalid stream-inf: %1")
2256 .arg(uri));
2257 }
2258 else
2259 {
2260 HLSStream *hls = ParseStreamInformation(line, decoded_URI(uri));
2261 if (hls)
2262 {
2263 /* Download playlist file from server */
2264 QByteArray buf;
2265 bool ret = downloadURL(hls->Url(), &buf);
2266 if (!ret)
2267 {
2268 LOG(VB_GENERAL, LOG_INFO, LOC +
2269 QString("Skipping invalid stream, couldn't download: %1")
2270 .arg(hls->Url()));
2271 delete hls;
2272 continue;
2273 }
2274 streams->append(hls);
2275 // One last chance to abort early
2276 if (m_killed)
2277 {
2278 err = RET_ERROR;
2279 break;
2280 }
2281 /* Parse HLS m3u8 content. */
2282 err = ParseM3U8(&buf, streams);
2283 if (err != RET_OK)
2284 break;
2285 hls->SetVersion(version);
2286 }
2287 }
2288 }
2289 }
2290 }
2291 else
2292 {
2293 HLSStream *hls = nullptr;
2294 if (m_meta)
2295 hls = GetLastStream(streams);
2296 else
2297 {
2298 /* No Meta playlist used */
2299 hls = new HLSStream(0, 0, m_m3u8);
2300 streams->append(hls);
2301 /* Get TARGET-DURATION first */
2302 p = buffer->indexOf("#EXT-X-TARGETDURATION:");
2303 if (p >= 0)
2304 {
2305 stream.seek(p);
2306 QString psz_duration = stream.readLine();
2307 if (psz_duration.isNull())
2308 return RET_ERROR;
2309 err = ParseTargetDuration(hls, psz_duration);
2310 }
2311 /* Store version */
2312 hls->SetVersion(version);
2313 }
2314 LOG(VB_PLAYBACK, LOG_INFO, LOC +
2315 QString("%1 Playlist HLS protocol version: %2")
2316 .arg(hls->Live() ? "Live": "VOD").arg(version));
2317
2318 // rewind
2319 stream.seek(0);
2320 /* */
2321 std::chrono::seconds segment_duration = -1s;
2322 QString title;
2323 do
2324 {
2325 /* Next line */
2326 line = stream.readLine();
2327 if (line.isNull())
2328 break;
2329 LOG(VB_PLAYBACK, LOG_DEBUG, LOC + QString("ParseM3U8: %1")
2330 .arg(line));
2331
2332 if (line.startsWith(QLatin1String("#EXTINF")))
2333 {
2334 int tmp = -1;
2335 err = ParseSegmentInformation(hls, line, tmp, title);
2336 segment_duration = std::chrono::seconds(tmp);
2337 }
2338 else if (line.startsWith(QLatin1String("#EXT-X-TARGETDURATION")))
2339 {
2340 err = ParseTargetDuration(hls, line);
2341 }
2342 else if (line.startsWith(QLatin1String("#EXT-X-MEDIA-SEQUENCE")))
2343 {
2344 err = ParseMediaSequence(hls, line);
2345 }
2346 else if (line.startsWith(QLatin1String("#EXT-X-KEY")))
2347 {
2348 err = ParseKey(hls, line);
2349 }
2350 else if (line.startsWith(QLatin1String("#EXT-X-PROGRAM-DATE-TIME")))
2351 {
2352 err = ParseProgramDateTime(hls, line);
2353 }
2354 else if (line.startsWith(QLatin1String("#EXT-X-ALLOW-CACHE")))
2355 {
2356 err = ParseAllowCache(hls, line);
2357 }
2358 else if (line.startsWith(QLatin1String("#EXT-X-DISCONTINUITY")))
2359 {
2360 err = ParseDiscontinuity(hls, line);
2361 }
2362 else if (line.startsWith(QLatin1String("#EXT-X-VERSION")))
2363 {
2364 int version2 = 0;
2365 err = ParseVersion(line, version2);
2366 hls->SetVersion(version2);
2367 }
2368 else if (line.startsWith(QLatin1String("#EXT-X-ENDLIST")))
2369 {
2370 err = ParseEndList(hls);
2371 }
2372 else if (!line.startsWith(QLatin1String("#")) && !line.isEmpty())
2373 {
2374 hls->AddSegment(segment_duration, title, decoded_URI(line));
2375 segment_duration = -1s; /* reset duration */
2376 title = "";
2377 }
2378 }
2379 while (err == RET_OK);
2380 }
2381 return err;
2382}
2383
2384// stream content functions
2389{
2390 int retries = 0;
2391 std::chrono::microseconds starttime = mdate();
2392 LOG(VB_PLAYBACK, LOG_INFO, LOC +
2393 QString("Starting Prefetch for %2 segments")
2394 .arg(count));
2397 while (!m_error && !m_killed && (retries < 20) &&
2398 (m_streamworker->CurrentPlaybackBuffer(false) < count) &&
2400 {
2402 retries++;
2403 }
2405 LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("Finished Prefetch (%1s)")
2406 .arg(duration_cast<std::chrono::seconds>(mdate() - starttime).count()));
2407 // we waited more than 10s abort
2408 if (retries >= 10)
2409 return RET_ERROR;
2410 return RET_OK;
2411}
2412
2414{
2415 bool live = hls->Live();
2416 /* sanity check */
2417 if ((m_streamworker->CurrentPlaybackBuffer() == 0) &&
2418 (!m_streamworker->IsAtEnd(true) || live))
2419 {
2420 LOG(VB_PLAYBACK, LOG_WARNING, LOC + "playback will stall");
2421 }
2423 (!m_streamworker->IsAtEnd(true) || live))
2424 {
2425 LOG(VB_PLAYBACK, LOG_WARNING, LOC + "playback in danger of stalling");
2426 }
2427 else if (live && m_streamworker->IsAtEnd(true) &&
2429 {
2430 LOG(VB_PLAYBACK, LOG_WARNING, LOC + "playback will exit soon, starving for data");
2431 }
2432}
2433
2439HLSSegment *HLSRingBuffer::GetSegment(int segnum, std::chrono::milliseconds timeout)
2440{
2441 HLSSegment *segment = nullptr;
2442 int stream = m_streamworker->StreamForSegment(segnum);
2443 if (stream < 0)
2444 {
2445 // we haven't downloaded that segment, request it
2446 // we should never be into this condition for normal playback
2447 m_streamworker->Seek(segnum);
2449 /* Wait for download to be finished */
2450 LOG(VB_PLAYBACK, LOG_WARNING, LOC +
2451 LOC + QString("waiting to get segment %1")
2452 .arg(segnum));
2453 int retries = 0;
2454 while (!m_error && (stream < 0) && (retries < 10))
2455 {
2457 stream = m_streamworker->StreamForSegment(segnum, false);
2458 retries++;
2459 }
2461 if (stream < 0)
2462 return nullptr;
2463 }
2464 HLSStream *hls = GetStream(stream);
2465 hls->Lock();
2466 segment = hls->GetSegment(segnum);
2467 hls->Unlock();
2468 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
2469 QString("GetSegment %1 [%2] stream[%3] (bitrate:%4)")
2470 .arg(segnum).arg(segment->Id()).arg(stream).arg(hls->Bitrate()));
2471 SanityCheck(hls);
2472 return segment;
2473}
2474
2476{
2477 return m_streams.size();
2478}
2479
2481{
2482 HLSStream *hls = GetStream(0);
2483 if (hls == nullptr)
2484 return 0;
2485 hls->Lock();
2486 int count = hls->NumSegments();
2487 hls->Unlock();
2488 return count;
2489}
2490
2491int HLSRingBuffer::ChooseSegment(int stream) const
2492{
2493 /* Choose a segment to start which is no closer than
2494 * 3 times the target duration from the end of the playlist.
2495 */
2496 int wanted = 0;
2497 int segid = 0;
2498 std::chrono::seconds wanted_duration = 0s;
2499 int count = NumSegments();
2500 int i = count - 1;
2501
2502 HLSStream *hls = GetStream(stream);
2503 while(i >= 0)
2504 {
2505 HLSSegment *segment = hls->GetSegment(i);
2506 if (segment == nullptr)
2507 continue;
2508
2509 if (segment->Duration() > hls->TargetDuration())
2510 {
2511 LOG(VB_PLAYBACK, LOG_WARNING, LOC +
2512 QString("EXTINF:%1 duration is larger than EXT-X-TARGETDURATION:%2")
2513 .arg(segment->Duration().count()).arg(hls->TargetDuration().count()));
2514 }
2515
2516 wanted_duration += segment->Duration();
2517 if (wanted_duration >= 3 * hls->TargetDuration())
2518 {
2519 /* Start point found */
2520 wanted = i;
2521 segid = segment->Id();
2522 break;
2523 }
2524 i-- ;
2525 }
2526
2527 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
2528 QString("Choose segment %1/%2 [%3]")
2529 .arg(wanted).arg(count).arg(segid));
2530 return wanted;
2531}
2532
2538{
2539 // no lock is required as, at this stage, no threads have either been started
2540 // or we are working on a stream list unique to PlaylistWorker
2541 if (streams == nullptr)
2542 {
2543 streams = &m_streams;
2544 }
2545 QMap<int,int> idstart;
2546 // Find the highest starting sequence for each stream
2547 for (int n = streams->size() - 1 ; n >= 0; n--)
2548 {
2549 HLSStream *hls = GetStream(n, streams);
2550 if (hls == nullptr)
2551 continue;
2552 if (hls->NumSegments() == 0)
2553 {
2554 streams->removeAt(n);
2555 continue; // remove it
2556 }
2557
2558 int id = hls->Id();
2559 int start = hls->StartSequence();
2560 if (!idstart.contains(id))
2561 {
2562 idstart.insert(id, start);
2563 }
2564 int start2 = idstart.value(id);
2565 if (start > start2)
2566 {
2567 idstart.insert(id, start);
2568 }
2569 }
2570 // Find the highest starting sequence for each stream
2571 for (int n = 0; n < streams->size(); n++)
2572 {
2573 HLSStream *hls = GetStream(n, streams);
2574 if (hls == nullptr)
2575 continue;
2576 int id = hls->Id();
2577 int seq = hls->StartSequence();
2578 int newstart= idstart.value(id);
2579 int todrop = newstart - seq;
2580 if (todrop == 0)
2581 {
2582 // perfect, leave it alone
2583 continue;
2584 }
2585 if (todrop >= hls->NumSegments() || todrop < 0)
2586 {
2587 LOG(VB_PLAYBACK, LOG_ERR, LOC +
2588 QString("stream %1 [id=%2] can't be properly adjusted, ignoring")
2589 .arg(n).arg(hls->Id()));
2590 continue;
2591 }
2592 for (int i = 0; i < todrop; i++)
2593 {
2594 hls->RemoveSegment(0);
2595 }
2596 hls->SetStartSequence(newstart);
2597 }
2598}
2599
2608bool HLSRingBuffer::OpenFile(const QString &lfilename, std::chrono::milliseconds /*retry_ms*/)
2609{
2610 QWriteLocker lock(&m_rwLock);
2611
2612 m_safeFilename = lfilename;
2613 m_filename = lfilename;
2614 QString finalURL;
2615
2616 QByteArray buffer;
2617 if (!downloadURL(m_filename, &buffer, finalURL))
2618 {
2619 LOG(VB_PLAYBACK, LOG_ERR, LOC +
2620 QString("Couldn't open URL %1").arg(m_filename));
2621 return false; // can't download file
2622 }
2623 if (m_filename != finalURL)
2624 {
2625 LOG(VB_PLAYBACK, LOG_INFO, LOC +
2626 QString("Redirected %1 -> %2 ").arg(m_filename, finalURL));
2627 m_filename = finalURL;
2628 }
2629 if (!IsHTTPLiveStreaming(&buffer))
2630 {
2631 LOG(VB_PLAYBACK, LOG_ERR, LOC +
2632 QString("%1 isn't a HTTP Live Streaming URL").arg(m_filename));
2633 return false;
2634 }
2635 // let's go
2637 LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("HTTP Live Streaming (%1)")
2638 .arg(m_m3u8));
2639
2640 /* Parse HLS m3u8 content. */
2641 if (ParseM3U8(&buffer, &m_streams) != RET_OK || m_streams.isEmpty())
2642 {
2643 LOG(VB_PLAYBACK, LOG_ERR, LOC +
2644 QString("An error occurred reading M3U8 playlist (%1)").arg(m_filename));
2645 m_error = true;
2646 return false;
2647 }
2648
2650
2651 /* HLS standard doesn't provide any guaranty about streams
2652 being sorted by bitrate, so we sort them, higher bitrate being first */
2653 std::sort(m_streams.begin(), m_streams.end(), HLSStream::IsGreater);
2654
2655 // if we want as close to live. We should be selecting a further segment
2656 // m_live ? ChooseSegment(0) : 0;
2657// if (m_live && m_startup < 0)
2658// {
2659// LOG(VB_PLAYBACK, LOG_WARNING, LOC +
2660// "less data than 3 times 'target duration' available for "
2661// "live playback, playback may stall");
2662// m_startup = 0;
2663// }
2664 m_startup = 0;
2666
2669
2670 if (Prefetch(std::min(NumSegments(), PLAYBACK_MINBUFFER)) != RET_OK)
2671 {
2672 LOG(VB_PLAYBACK, LOG_ERR, LOC +
2673 "fetching first segment failed or didn't complete within 10s.");
2674 m_error = true;
2675 return false;
2676 }
2677
2678 // set bitrate value used to calculate the size of the stream
2679 HLSStream *hls = GetCurrentStream();
2680 m_bitrate = hls->Bitrate();
2681
2682 // Set initial seek position (relative to m_startup)
2684
2685 /* Initialize HLS live stream thread */
2686 //if (m_live) // commented out as some streams are marked as VOD, yet
2687 // aren't, they are updated over time
2688 {
2689 m_playlistworker = new PlaylistWorker(this, 0ms);
2691 }
2692
2693 return true;
2694}
2695
2696bool HLSRingBuffer::SaveToDisk(const QString &filename, int segstart, int segend)
2697{
2698 // download it all
2699 FILE *fp = fopen(filename.toLatin1().constData(), "w");
2700 if (fp == nullptr)
2701 return false;
2702 int count = NumSegments();
2703 if (segend < 0)
2704 {
2705 segend = count;
2706 }
2707 for (int i = segstart; i < segend; i++)
2708 {
2709 HLSSegment *segment = GetSegment(i);
2710 if (segment == nullptr)
2711 {
2712 LOG(VB_PLAYBACK, LOG_ERR, LOC +
2713 QString("downloading %1 failed").arg(i));
2714 }
2715 else
2716 {
2717 LOG(VB_PLAYBACK, LOG_INFO, LOC +
2718 QString("download of %1 succeeded")
2719 .arg(i));
2720 fwrite(segment->Data(), segment->Size(), 1, fp);
2721 fflush(fp);
2722 }
2723 }
2724 fclose(fp);
2725 return true;
2726}
2727
2728int64_t HLSRingBuffer::SizeMedia(void) const
2729{
2730 if (m_error)
2731 return -1;
2732
2733 HLSStream *hls = GetCurrentStream();
2734 if (nullptr == hls)
2735 return -1;
2736 int64_t size = hls->Duration().count() * m_bitrate / 8;
2737
2738 return size;
2739}
2740
2746{
2747 HLSStream *hls = GetCurrentStream();
2748 bool live = hls ? hls->Live() : false;
2749
2750 // last seek was to end of media, we are just in seek mode so do not wait
2751 if (m_seektoend)
2752 return;
2753
2755 (!live && m_streamworker->IsAtEnd()))
2756 {
2757 return;
2758 }
2759
2760 // danger of getting to the end... pause until we have some more
2761 LOG(VB_PLAYBACK, LOG_INFO, LOC +
2762 QString("pausing until we get sufficient data buffered"));
2765 while (!m_error && !m_interrupted &&
2766 (m_streamworker->CurrentPlaybackBuffer(false) < 2) &&
2767 (live || !m_streamworker->IsAtEnd()))
2768 {
2770 }
2772}
2773
2775{
2776 if (m_error)
2777 return -1;
2778
2779 int used = 0;
2780 int i_read = sz;
2781
2783 if (m_interrupted)
2784 {
2785 LOG(VB_PLAYBACK, LOG_DEBUG, LOC + QString("interrupted"));
2786 return 0;
2787 }
2788
2789 do
2790 {
2791 int segnum = m_playback->Segment();
2792 if (segnum >= NumSegments())
2793 {
2794 m_playback->AddOffset(used);
2795 return used;
2796 }
2797 int stream = m_streamworker->StreamForSegment(segnum);
2798 if (stream < 0)
2799 {
2800 // we haven't downloaded this segment yet, likely that it was
2801 // dropped (livetv?)
2803 continue;
2804 }
2805 HLSStream *hls = GetStream(stream);
2806 if (hls == nullptr)
2807 break;
2808 HLSSegment *segment = hls->GetSegment(segnum);
2809 if (segment == nullptr)
2810 break;
2811
2812 segment->Lock();
2813 if (segment->SizePlayed() == segment->Size())
2814 {
2815 if (!hls->Cache() || hls->Live())
2816 {
2817 segment->Clear();
2819 }
2820 else
2821 {
2822 segment->Reset();
2823 }
2824
2826 segment->Unlock();
2827
2828 /* signal download thread we're about to use a new segment */
2830 continue;
2831 }
2832
2833 if (segment->SizePlayed() == 0)
2834 {
2835 LOG(VB_PLAYBACK, LOG_INFO, LOC +
2836 QString("started reading segment %1 [id:%2] from stream %3 (%4 buffered)")
2837 .arg(segnum).arg(segment->Id()).arg(stream)
2839 }
2840
2841 int32_t len = segment->Read((uint8_t*)data + used, i_read, m_fd);
2842 used += len;
2843 i_read -= len;
2844 segment->Unlock();
2845 }
2846 while (i_read > 0 && !m_interrupted); // cppcheck-suppress knownConditionTrueFalse
2847
2848 if (m_interrupted)
2849 LOG(VB_PLAYBACK, LOG_DEBUG, LOC + QString("interrupted"));
2850
2851 m_playback->AddOffset(used);
2852 return used;
2853}
2854
2860{
2861 int segnum = m_playback->Segment();
2862 int stream = m_streamworker->StreamForSegment(segnum);
2863 if (stream < 0)
2864 {
2865 return 0;
2866 }
2867 HLSStream *hls = GetStream(stream);
2868 if (hls == nullptr)
2869 {
2870 return 0;
2871 }
2872 HLSSegment *segment = hls->GetSegment(segnum);
2873 if (segment == nullptr)
2874 {
2875 return 0;
2876 }
2877 auto byterate = (uint64_t)(((double)segment->Size()) /
2878 ((double)segment->Duration().count()));
2879
2880 return (int)((size * 1000.0) / byterate);
2881}
2882
2884{
2885 QReadLocker lock(&m_rwLock);
2886 return SizeMedia();
2887}
2888
2889long long HLSRingBuffer::SeekInternal(long long pos, int whence)
2890{
2891 if (m_error)
2892 return -1;
2893
2894 if (!IsSeekingAllowed())
2895 {
2896 return m_playback->Offset();
2897 }
2898
2899 std::chrono::microseconds starting = mdate();
2900
2901 QWriteLocker lock(&m_posLock);
2902
2903 int totalsize = SizeMedia();
2904 int64_t where = 0;
2905 switch (whence)
2906 {
2907 case SEEK_CUR:
2908 // return current location, nothing to do
2909 if (pos == 0)
2910 {
2911 return m_playback->Offset();
2912 }
2913 where = m_playback->Offset() + pos;
2914 break;
2915 case SEEK_END:
2916 where = SizeMedia() - pos;
2917 break;
2918 case SEEK_SET:
2919 default:
2920 where = pos;
2921 break;
2922 }
2923
2924 // We determine the duration at which it was really attempting to seek to
2925 auto postime = secondsFromFloat((where * 8.0) / m_bitrate);
2926 int count = NumSegments();
2927 int segnum = m_playback->Segment();
2928 HLSStream *hls = GetStreamForSegment(segnum);
2929
2930 /* restore current segment's file position indicator to 0 */
2931 HLSSegment *segment = hls->GetSegment(segnum);
2932 if (segment != nullptr)
2933 {
2934 segment->Lock();
2935 segment->Reset();
2936 segment->Unlock();
2937 }
2938
2939 if (where > totalsize)
2940 {
2941 // we're at the end, never let seek after last 3 segments
2942 postime -= hls->TargetDuration() * 3;
2943 if (postime < 0s)
2944 {
2945 postime = 0s;
2946 }
2947 }
2948
2949 // Find segment containing position
2950 std::chrono::seconds starttime = 0s;
2951 std::chrono::seconds endtime = 0s;
2952 for (int n = m_startup; n < count; n++)
2953 {
2954 hls = GetStreamForSegment(n);
2955 if (hls == nullptr)
2956 {
2957 // error, should never happen, irrecoverable error
2958 return -1;
2959 }
2960 segment = hls->GetSegment(n);
2961 if (segment == nullptr)
2962 {
2963 // stream doesn't contain segment error can't continue,
2964 // unknown error
2965 return -1;
2966 }
2967 endtime += segment->Duration();
2968 if (postime < endtime)
2969 {
2970 segnum = n;
2971 break;
2972 }
2973 starttime = endtime;
2974 }
2975
2976 /*
2977 * Live Mode exception:
2978 * FFmpeg seek to the last segment in order to determine the size of the video
2979 * so do not allow seeking to the last segment if in live mode as we don't care
2980 * about the size
2981 * Also do not allow to seek before the current playback segment as segment
2982 * has been cleared from memory
2983 * We only let determine the size if the bandwidth would allow fetching the
2984 * the segments in less than 5s
2985 */
2986 if (hls->Live() && (segnum >= count - 1 || segnum < m_playback->Segment()) &&
2987 ((hls->TargetDuration() * hls->Bitrate() / m_streamworker->Bandwidth()) > 5s))
2988 {
2989 return m_playback->Offset();
2990 }
2991 m_seektoend = segnum >= count - 1;
2992
2993 m_playback->SetSegment(segnum);
2994
2995 m_streamworker->Seek(segnum);
2996 m_playback->SetOffset(postime.count() * m_bitrate / 8);
2997
2999
3000 /* Wait for download to be finished and to buffer 3 segment */
3001 LOG(VB_PLAYBACK, LOG_INFO, LOC +
3002 QString("seek to segment %1").arg(segnum));
3003
3004 // see if we've already got the segment, and at least 2 buffered after
3005 // then no need to wait for streamworker
3006 while (!m_error && !m_interrupted &&
3007 (!m_streamworker->GotBufferedSegments(segnum, 2) &&
3008 (m_streamworker->CurrentPlaybackBuffer(false) < 2) &&
3010 {
3012 }
3013 if (m_interrupted)
3014 LOG(VB_PLAYBACK, LOG_DEBUG, LOC + QString("interrupted"));
3015
3017
3018 // now seek within found segment
3019 int stream = m_streamworker->StreamForSegment(segnum);
3020 if (stream < 0)
3021 {
3022 // segment didn't get downloaded (timeout?)
3023 LOG(VB_PLAYBACK, LOG_ERR, LOC +
3024 QString("seek error: segment %1 should have been downloaded, but didn't."
3025 " Playback will stall")
3026 .arg(segnum));
3027 }
3028 else
3029 {
3030 if (segment == nullptr) // can never happen, make coverity happy
3031 {
3032 // stream doesn't contain segment error can't continue,
3033 // unknown error
3034 return -1;
3035 }
3036 int32_t skip = ((postime - starttime) * segment->Size()) / segment->Duration();
3037 segment->Read(nullptr, skip);
3038 }
3039 LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("seek completed in %1s")
3040 .arg(duration_cast<std::chrono::seconds>(mdate() - starting).count()));
3041
3042 return m_playback->Offset();
3043}
3044
3046{
3047 if (m_error)
3048 return 0;
3049 return m_playback->Offset();
3050}
3051
3053{
3054 return !m_error && !m_streams.isEmpty() && NumSegments() > 0;
3055}
3056
3058{
3059 QMutexLocker lock(&m_lock);
3060
3061 // segment didn't get downloaded (timeout?)
3062 LOG(VB_PLAYBACK, LOG_DEBUG, LOC + QString("requesting interrupt"));
3063 m_interrupted = true;
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 restart"));
3072 m_interrupted = false;
3073}
static constexpr uint8_t AES128_KEY_SIZE
Definition: HLSStream.h:19
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)
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:194
void start(QThread::Priority p=QThread::InheritPriority)
Tell MThread to start running the thread in the near future.
Definition: mthread.cpp:281
void RunEpilog(void)
Cleans up a thread's resources, call this if you reimplement run().
Definition: mthread.cpp:207
bool wait(std::chrono::milliseconds time=std::chrono::milliseconds::max())
Wait for the MThread to exit, with a maximum timeout.
Definition: mthread.cpp:298
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
QMap< int, int > m_segmap
int64_t Bandwidth(void) const
void RemoveSegmentFromStream(int segnum)
unsigned int uint
Definition: compat.h:60
static uint32_t * tmp
Definition: goom_core.cpp:28
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::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.
Definition: mythchrono.h:80
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
STL namespace.
std::array< uint8_t, AES128_KEY_SIZE > key
Definition: HLSStream.h:21