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