MythTV master
HLSReader.cpp
Go to the documentation of this file.
1#include "HLSReader.h"
2
3#include <cstring>
4#include <thread>
5
6#include <QtGlobal>
7#include <QRegularExpression>
8#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
9#include <QStringConverter>
10#endif
11#include <QUrl>
12
13#include "libmythbase/mythconfig.h"
16
17#include "m3u.h"
18
19#define LOC QString("HLSReader[%1]: ").arg(m_inputId)
20
26static QUrl RelativeURI (const QString& baseString, const QString& uriString)
27{
28 QUrl base(baseString);
29 QUrl uri(QUrl::fromEncoded(uriString.toLatin1()));
30
31 return base.resolved(uri);
32}
33
35{
36 LOG(VB_RECORD, LOG_INFO, LOC + "dtor -- start");
37 Close();
38 LOG(VB_RECORD, LOG_INFO, LOC + "dtor -- end");
39}
40
41bool HLSReader::Open(const QString & m3u, int bitrate_index)
42{
43 LOG(VB_RECORD, LOG_INFO, LOC + QString("Opening '%1'").arg(m3u));
44
45 m_bitrateIndex = bitrate_index;
46
47 if (IsOpen(m3u))
48 {
49 LOG(VB_RECORD, LOG_ERR, LOC + "Already open");
50 return true;
51 }
52 Close(true);
53 m_cancel = false;
54
55 QByteArray buffer;
56
57#ifdef HLS_USE_MYTHDOWNLOADMANAGER // MythDownloadManager leaks memory
58 if (!DownloadURL(m3u, &buffer))
59 {
60 LOG(VB_RECORD, LOG_ERR, LOC + "Open failed.");
61 return false; // can't download file
62 }
63#else
64 MythSingleDownload downloader;
65 QString redir;
66 QUrl m3uUrl { m3u };
67 if (!m3uUrl.isValid())
68 {
69 LOG(VB_GENERAL, LOG_ERR, LOC + "Invalid url: " + m3u);
70 return false;
71 }
72 if (!downloader.DownloadURL(m3uUrl, &buffer, 30s, 0, 0, &redir))
73 {
74 LOG(VB_GENERAL, LOG_ERR,
75 LOC + "Open failed: " + downloader.ErrorString());
76 return false; // can't download file
77 }
78#endif
79
80 QTextStream text(&buffer);
81#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
82 text.setCodec("UTF-8");
83#else
84 text.setEncoding(QStringConverter::Utf8);
85#endif
86
87 if (!IsValidPlaylist(text))
88 {
89 LOG(VB_RECORD, LOG_ERR, LOC +
90 QString("Open '%1': not a valid playlist").arg(m3u));
91 return false;
92 }
93
94 m_m3u8 = m3u;
95 m_segmentBase = redir.isEmpty() ? m3u : redir;
96
97 QMutexLocker lock(&m_streamLock);
98 m_streams.clear();
99 m_curstream = nullptr;
100
101 if (!ParseM3U8(buffer))
102 return false;
103
104 if (m_bitrateIndex == 0)
105 {
106 // Select the highest bitrate stream
107 StreamContainer::iterator Istream;
108 for (Istream = m_streams.begin(); Istream != m_streams.end(); ++Istream)
109 {
110 if (m_curstream == nullptr ||
111 (*Istream)->Bitrate() > m_curstream->Bitrate())
112 m_curstream = *Istream;
113 }
114 }
115 else
116 {
117 if (m_streams.size() < m_bitrateIndex)
118 {
119 LOG(VB_RECORD, LOG_ERR, LOC +
120 QString("Open '%1': Only %2 bitrates, %3 is not a valid index")
121 .arg(m3u)
122 .arg(m_streams.size())
123 .arg(m_bitrateIndex));
124 m_fatal = true;
125 return false;
126 }
127
128 auto it = m_streams.begin();
129 for (auto i = 0; i < m_bitrateIndex - 1; i++)
130 it++;
131 m_curstream = *it;
132 }
133
134 if (!m_curstream)
135 {
136 LOG(VB_RECORD, LOG_ERR, LOC + QString("No stream selected"));
137 m_fatal = true;
138 return false;
139 }
140 LOG(VB_RECORD, LOG_INFO, LOC +
141 QString("Selected stream with %3 bitrate")
142 .arg(m_curstream->Bitrate()));
143
144 QMutexLocker worker_lock(&m_workerLock);
145
148
151
152 LOG(VB_RECORD, LOG_INFO, LOC + "Open -- end");
153 return true;
154}
155
156void HLSReader::Close(bool quiet)
157{
158 LOG(VB_RECORD, (quiet ? LOG_DEBUG : LOG_INFO), LOC + "Close -- start");
159
160 Cancel(quiet);
161
162 QMutexLocker stream_lock(&m_streamLock);
163 m_curstream = nullptr;
164
165 StreamContainer::iterator Istream;
166 for (Istream = m_streams.begin(); Istream != m_streams.end(); ++Istream)
167 delete *Istream;
168 m_streams.clear();
169
170 QMutexLocker lock(&m_workerLock);
171
172 delete m_streamWorker;
173 m_streamWorker = nullptr;
174 delete m_playlistWorker;
175 m_playlistWorker = nullptr;
176
177 LOG(VB_RECORD, (quiet ? LOG_DEBUG : LOG_INFO), LOC + "Close -- end");
178}
179
180void HLSReader::Cancel(bool quiet)
181{
182 LOG(VB_RECORD, (quiet ? LOG_DEBUG : LOG_INFO), LOC + "Cancel -- start");
183
184 m_cancel = true;
185
186 m_throttleLock.lock();
187 m_throttleCond.wakeAll();
188 m_throttleLock.unlock();
189
190 QMutexLocker lock(&m_workerLock);
191
192 if (m_curstream)
193 LOG(VB_RECORD, LOG_INFO, LOC + "Cancel");
194
197
198 if (m_streamWorker)
200
201#ifdef HLS_USE_MYTHDOWNLOADMANAGER // MythDownloadManager leaks memory
202 if (!m_sements.empty())
203 CancelURL(m_segments.front().Url());
204#endif
205
206 LOG(VB_RECORD, (quiet ? LOG_DEBUG : LOG_INFO), LOC + "Cancel -- done");
207}
208
210{
211 LOG(VB_RECORD, LOG_INFO, LOC + QString("Throttle(%1)")
212 .arg(val ? "true" : "false"));
213
214 m_throttleLock.lock();
215 m_throttle = val;
216 if (val)
217 m_prebufferCnt += 4;
218 else
219 m_throttleCond.wakeAll();
220 m_throttleLock.unlock();
221}
222
223qint64 HLSReader::Read(uint8_t* buffer, qint64 maxlen)
224{
225 if (!m_curstream)
226 {
227 LOG(VB_RECORD, LOG_ERR, LOC + "Read: no stream selected");
228 return 0;
229 }
230 if (m_cancel)
231 {
232 LOG(VB_RECORD, LOG_DEBUG, LOC + QString("Read: canceled"));
233 return 0;
234 }
235
236 QMutexLocker lock(&m_bufLock);
237
238 qint64 len = m_buffer.size() < maxlen ? m_buffer.size() : maxlen;
239 LOG(VB_RECORD, LOG_DEBUG, LOC + QString("Reading %1 of %2 bytes")
240 .arg(len).arg(m_buffer.size()));
241
242 memcpy(buffer, m_buffer.constData(), len);
243 if (len < m_buffer.size())
244 {
245 m_buffer.remove(0, len);
246 }
247 else
248 {
249 m_buffer.clear();
250 }
251
252 return len;
253}
254
255#ifdef HLS_USE_MYTHDOWNLOADMANAGER // MythDownloadManager leaks memory
256bool HLSReader::DownloadURL(const QString &url, QByteArray *buffer)
257{
259 return mdm->download(url, buffer);
260}
261
262void HLSReader::CancelURL(const QString &url)
263{
265 mdm->cancelDownload(url);
266 delete mdm;
267}
268
269void HLSReader::CancelURL(const QStringList &urls)
270{
272 mdm->cancelDownload(urls);
273}
274#endif
275
276bool HLSReader::IsValidPlaylist(QTextStream & text)
277{
278 /* Parse stream and search for
279 * EXT-X-TARGETDURATION or EXT-X-STREAM-INF tag, see
280 * http://tools.ietf.org/html/draft-pantos-http-live-streaming-04#page-8
281 *
282 * Updated with latest available version from 2017, see
283 * https://datatracker.ietf.org/doc/html/rfc8216
284 */
285 QString line = text.readLine();
286 if (!line.startsWith((const char*)"#EXTM3U"))
287 return false;
288
289 for (;;)
290 {
291 line = text.readLine();
292 if (line.isNull())
293 break;
294 LOG(VB_RECORD, LOG_DEBUG,
295 QString("IsValidPlaylist: |'%1'").arg(line));
296 if (line.startsWith(QLatin1String("#EXT-X-TARGETDURATION")) ||
297 line.startsWith(QLatin1String("#EXT-X-STREAM-INF")) ||
298 line.startsWith(QLatin1String("#EXT-X-MEDIA")) ||
299 line.startsWith(QLatin1String("#EXT-X-KEY")) ||
300 line.startsWith(QLatin1String("#EXT-X-ALLOW-CACHE")) ||
301 line.startsWith(QLatin1String("#EXT-X-ENDLIST")) ||
302 line.startsWith(QLatin1String("#EXT-X-DISCONTINUITY")) ||
303 line.startsWith(QLatin1String("#EXT-X-VERSION")))
304 {
305 return true;
306 }
307 }
308
309 LOG(VB_RECORD, LOG_ERR, QString("IsValidPlaylist: false"));
310 return false;
311}
312
313bool HLSReader::ParseM3U8(const QByteArray& buffer, HLSRecStream* stream)
314{
323 LOG(VB_RECORD, LOG_DEBUG, LOC + "ParseM3U8 -- begin");
324
325 QTextStream text(buffer);
326
327 QString line = text.readLine();
328 if (line.isNull())
329 {
330 LOG(VB_RECORD, LOG_ERR, LOC + "ParseM3U8: empty line");
331 return false;
332 }
333
334 if (!line.startsWith(QLatin1String("#EXTM3U")))
335 {
336 LOG(VB_RECORD, LOG_ERR, LOC +
337 "ParseM3U8: missing #EXTM3U tag .. aborting");
338 return false;
339 }
340
341 // Version is 1 if not specified, otherwise must be in range 1 to 7.
342 int version = 1;
343 int p = buffer.indexOf("#EXT-X-VERSION:");
344 if (p >= 0)
345 {
346 text.seek(p);
347 if (!M3U::ParseVersion(text.readLine(), StreamURL(), version))
348 return false;
349 }
350
351 if (buffer.indexOf("#EXT-X-STREAM-INF") >= 0)
352 {
353 // Meta index file
354 LOG(VB_RECORD, LOG_DEBUG, LOC + "Master Playlist");
355
356 /* M3U8 Meta Index file */
357 text.seek(0); // rewind
358 while (!m_cancel)
359 {
360 line = text.readLine();
361 if (line.isNull())
362 break;
363
364 LOG(VB_RECORD, LOG_INFO, LOC + QString("|%1").arg(line));
365
366 // EXT-X-STREAM-INF
367 if (line.startsWith(QLatin1String("#EXT-X-STREAM-INF")))
368 {
369 QString uri = text.readLine();
370 if (uri.isNull() || uri.startsWith(QLatin1String("#")))
371 {
372 LOG(VB_RECORD, LOG_INFO, LOC +
373 QString("ParseM3U8: Invalid EXT-X-STREAM-INF data '%1'")
374 .arg(uri));
375 }
376 else
377 {
378 QString url = RelativeURI(m_segmentBase, uri).toString();
379
380 StreamContainer::iterator Istream = m_streams.find(url);
381 if (Istream == m_streams.end())
382 {
383 int id = 0;
384 uint64_t bandwidth = 0;
385 QString audio;
386 QString video;
387 if (!M3U::ParseStreamInformation(line, url, StreamURL(),
388 id, bandwidth, audio, video))
389 break;
390 auto *hls = new HLSRecStream(m_inputId, id, bandwidth, url,
392 if (hls)
393 {
394 LOG(VB_RECORD, LOG_INFO, LOC +
395 QString("Adding stream %1")
396 .arg(hls->toString()));
397 Istream = m_streams.insert(url, hls);
398 }
399 }
400 else
401 {
402 LOG(VB_RECORD, LOG_INFO, LOC +
403 QString("Already have stream '%1'").arg(url));
404 }
405 }
406 }
407 }
408 }
409 else
410 {
411 LOG(VB_RECORD, LOG_DEBUG, LOC + "Media Playlist");
412
413 HLSRecStream *hls = stream;
414 if (stream == nullptr)
415 {
416 /* No Meta playlist used */
417 StreamContainer::iterator Istream =
419 if (Istream == m_streams.end())
420 {
421 hls = new HLSRecStream(m_inputId, 0, 0, m_m3u8, m_segmentBase);
422 if (hls)
423 {
424 LOG(VB_RECORD, LOG_INFO, LOC +
425 QString("Adding new stream '%1'").arg(m_m3u8));
426 Istream = m_streams.insert(m_m3u8, hls);
427 }
428 }
429 else
430 {
431 hls = *Istream;
432 LOG(VB_RECORD, LOG_INFO, LOC +
433 QString("Updating stream '%1'").arg(hls->toString()));
434 }
435
436 /* Get TARGET-DURATION first */
437 p = buffer.indexOf("#EXT-X-TARGETDURATION:");
438 if (p >= 0)
439 {
440 int duration = 0;
441
442 text.seek(p);
443 if (!M3U::ParseTargetDuration(text.readLine(), StreamURL(),
444 duration))
445 return false;
446 hls->SetTargetDuration(std::chrono::seconds(duration));
447 }
448 /* Store version */
449 hls->SetVersion(version);
450 }
451 LOG(VB_RECORD, LOG_INFO, LOC +
452 QString("%1 Media Playlist HLS protocol version: %2")
453 .arg(hls->Live() ? "Live": "VOD").arg(version));
454
455 // rewind
456 text.seek(0);
457
458 QString title; // From playlist, #EXTINF:<duration>,<title>
459 std::chrono::milliseconds segment_duration = 0s;// From playlist, e.g. #EXTINF:10.24,
460 int64_t first_sequence = -1; // Sequence number of first segment to be recorded
461 int64_t sequence_num = 0; // Sequence number of next segment to be read
462 int skipped = 0; // Segments skipped, sequence number at or below current
463#if CONFIG_LIBCRYPTO
464 QString aes_keypath; // AES key path
465 QString aes_iv; // AES IV value
466#endif
467 SegmentContainer new_segments; // All segments read from Media Playlist
468
469 QMutexLocker lock(&m_seqLock);
470 while (!m_cancel)
471 {
472 /* Next line */
473 line = text.readLine();
474 if (line.isNull())
475 break;
476 LOG(VB_RECORD, (m_debug ? LOG_INFO : LOG_DEBUG),
477 LOC + QString("|%1").arg(line));
478
479 if (line.startsWith(QLatin1String("#EXTINF")))
480 {
481 int tmp_duration = 0;
482 if (!M3U::ParseSegmentInformation(hls->Version(), line,
483 tmp_duration,
484 title, StreamURL()))
485 return false;
486 segment_duration = std::chrono::milliseconds(tmp_duration);
487 }
488 else if (line.startsWith(QLatin1String("#EXT-X-TARGETDURATION")))
489 {
490 int duration = 0;
491 if (!M3U::ParseTargetDuration(line, StreamURL(), duration))
492 return false;
493 hls->SetTargetDuration(std::chrono::seconds(duration));
494 }
495 else if (line.startsWith(QLatin1String("#EXT-X-MEDIA-SEQUENCE")))
496 {
497 if (!M3U::ParseMediaSequence(sequence_num, line, StreamURL()))
498 return false;
499 if (first_sequence < 0)
500 first_sequence = sequence_num;
501 }
502 else if (line.startsWith(QLatin1String("#EXT-X-MEDIA")))
503 {
504 // Not handled yet
505 }
506 else if (line.startsWith(QLatin1String("#EXT-X-KEY")))
507 {
508#if CONFIG_LIBCRYPTO
509 QString path;
510 QString iv;
511 if (!M3U::ParseKey(hls->Version(), line, m_aesMsg, LOC,
512 path, iv))
513 return false;
514
515 aes_keypath = path;
516 aes_iv = iv;
517#else // CONFIG_LIBCRYPTO
518 LOG(VB_RECORD, LOG_ERR, LOC + "#EXT-X-KEY needs libcrypto");
519 return false;
520#endif // CONFIG_LIBCRYPTO
521 }
522 else if (line.startsWith(QLatin1String("#EXT-X-MAP")))
523 {
524 QString uri;
525 if (!M3U::ParseMap(line, StreamURL(), uri))
526 return false;
527 hls->SetMapUri(uri);
528 }
529 else if (line.startsWith(QLatin1String("#EXT-X-PROGRAM-DATE-TIME")))
530 {
531 QDateTime dt;
532 if (!M3U::ParseProgramDateTime(line, StreamURL(), dt))
533 return false;
534 hls->SetDateTime(dt);
535 }
536 else if (line.startsWith(QLatin1String("#EXT-X-ALLOW-CACHE")))
537 {
538 bool do_cache = false;
539 if (!M3U::ParseAllowCache(line, StreamURL(), do_cache))
540 return false;
541 hls->SetCache(do_cache);
542 }
543 else if (line.startsWith(QLatin1String("#EXT-X-DISCONTINUITY-SEQUENCE")))
544 {
545 int sequence = 0;
546 if (!M3U::ParseDiscontinuitySequence(line, StreamURL(), sequence))
547 return false;
548 hls->SetDiscontinuitySequence(sequence);
549 }
550 else if (line.startsWith(QLatin1String("#EXT-X-DISCONTINUITY")))
551 {
553 return false;
554 // Not handled yet
555 }
556 else if (line.startsWith(QLatin1String("#EXT-X-INDEPENDENT-SEGMENTS")))
557 {
559 return false;
560 // Not handled yet
561 }
562 else if (line.startsWith(QLatin1String("#EXT-X-VERSION")))
563 {
564 int version2 = 0;
565 if (!M3U::ParseVersion(line, StreamURL(), version2))
566 return false;
567 hls->SetVersion(version2);
568 }
569 else if (line.startsWith(QLatin1String("#EXT-X-ENDLIST")))
570 {
571 bool is_vod = false;
572 if (!M3U::ParseEndList(StreamURL(), is_vod))
573 return false;
574 hls->SetLive(!is_vod);
575 }
576 else if (!line.startsWith(QLatin1String("#")) && !line.isEmpty())
577 {
578 if (m_curSeq < 0 || sequence_num > m_curSeq)
579 {
580 HLSRecSegment segment =
581 HLSRecSegment(m_inputId, sequence_num, segment_duration, title,
582 RelativeURI(hls->SegmentBaseUrl(), line));
583#if CONFIG_LIBCRYPTO
584 if (!aes_iv.isEmpty() || !aes_keypath.isEmpty())
585 {
586 LOG(VB_RECORD, LOG_DEBUG, LOC + " aes_iv:" + aes_iv + " aes_keypath:" + aes_keypath);
587 }
588
589 segment.SetKeyPath(aes_keypath);
590 if (!aes_iv.isEmpty() && !segment.SetAESIV(aes_iv))
591 {
592 LOG(VB_RECORD, LOG_ERR, LOC + "invalid AES IV:" + aes_iv);
593 }
594
595 aes_keypath.clear();
596 aes_iv.clear();
597#endif // CONFIG_LIBCRYPTO
598 new_segments.push_back(segment);
599 }
600 else
601 {
602 ++skipped;
603 }
604
605 ++sequence_num;
606 segment_duration = -1s; /* reset duration */
607 title.clear();
608 }
609 }
610
611 LOG(VB_RECORD, LOG_DEBUG, LOC +
612 QString("first_sequence:%1").arg(first_sequence) +
613 QString(" sequence_num:%1").arg(sequence_num) +
614 QString(" m_curSeq:%1").arg(m_curSeq) +
615 QString(" skipped:%1").arg(skipped));
616
617 if (sequence_num < m_curSeq)
618 {
619 // Sequence has been reset
620 LOG(VB_RECORD, LOG_WARNING, LOC +
621 QString("Sequence number has been reset from %1 to %2")
622 .arg(m_curSeq).arg(first_sequence));
624 return false;
625 }
626
627 // For near-live skip all segments that are too far in the past.
628 if (m_curSeq < 0)
629 {
630 // Compute number of segments for 30 seconds buffer from live.
631 // If the duration is not know keep 3 segments.
632 int numseg = new_segments.size();
633 numseg = std::min(numseg, 3);
634 if (hls->TargetDuration() > 0s)
635 {
636 numseg = 30s / hls->TargetDuration();
637 numseg = std::clamp(numseg, 2, 10);
638 }
639
640 // Trim new_segments to leave only the last part
641 if (new_segments.size() > numseg)
642 {
643 int size_before = new_segments.size();
644 SegmentContainer::iterator it = new_segments.begin() + (new_segments.size() - numseg);
645 new_segments.erase(new_segments.begin(), it);
646 LOG(VB_RECORD, LOG_INFO, LOC +
647 QString("Read last %1 segments instead of %2 for near-live")
648 .arg(new_segments.size()).arg(size_before));
649
650 // Adjust first_sequence to first segment to be read
651 first_sequence += size_before - new_segments.size();
652 }
653 }
654
655 SegmentContainer::iterator Inew = new_segments.begin();
656 SegmentContainer::iterator Iseg = m_segments.end() - 1;
657
658 // Does this playlist overlap?
659 if (!m_segments.empty() && !new_segments.empty())
660 {
661 if ((*Iseg).Sequence() >= first_sequence &&
662 (*Iseg).Sequence() < sequence_num)
663 {
664 // Find the beginning of the overlap
665 while (Iseg != m_segments.begin() &&
666 (*Iseg).Sequence() > first_sequence &&
667 (*Iseg).Sequence() < sequence_num)
668 --Iseg;
669
670 int64_t diff = (*Iseg).Sequence() - (*Inew).Sequence();
671 if (diff >= 0 && new_segments.size() > diff)
672 {
673 Inew += diff;
674
675 // Update overlapping segment info
676 for ( ; Iseg != m_segments.end(); ++Iseg, ++Inew)
677 {
678 if (Inew == new_segments.end())
679 {
680 LOG(VB_RECORD, LOG_ERR, LOC +
681 QString("Went off the end with %1 left")
682 .arg(m_segments.end() - Iseg));
683 break;
684 }
685 if ((*Iseg).Sequence() != (*Inew).Sequence())
686 {
687 LOG(VB_RECORD, LOG_ERR, LOC +
688 QString("Sequence non-sequential? %1 != %2")
689 .arg((*Iseg).Sequence())
690 .arg((*Inew).Sequence()));
691 break;
692 }
693
694 (*Iseg).m_duration = (*Inew).Duration();
695 (*Iseg).m_title = (*Inew).Title();
696 (*Iseg).m_url = (*Inew).Url();
697 }
698 }
699 }
700 }
701
702 for ( ; Inew != new_segments.end(); ++Inew)
703 m_segments.push_back(*Inew);
704
705 m_playlistSize = new_segments.size() + skipped;
706 int behind = m_segments.size() - m_playlistSize;
707 int max_behind = m_playlistSize / 2;
708
709 LOG(VB_RECORD, LOG_INFO, LOC +
710 QString("new_segments.size():%1 ").arg(new_segments.size()) +
711 QString("m_playlistSize:%1 ").arg(m_playlistSize) +
712 QString("behind:%1 ").arg(behind) +
713 QString("max_behind:%1").arg(max_behind));
714
715 if (behind > max_behind)
716 {
717 LOG(VB_RECORD, LOG_WARNING, LOC +
718 QString("Not downloading fast enough! "
719 "%1 segments behind, skipping %2 segments. "
720 "playlist size: %3, queued: %4")
721 .arg(behind).arg(behind - max_behind)
722 .arg(m_playlistSize).arg(m_segments.size()));
723 m_workerLock.lock();
724 if (m_streamWorker)
726 m_workerLock.unlock();
727
729 Iseg = m_segments.begin() + (behind - max_behind);
730 m_segments.erase(m_segments.begin(), Iseg);
732 }
733 else if (m_debugCnt > 0)
734 {
735 --m_debugCnt;
736 }
737 else
738 {
739 m_debug = false;
740 }
741 }
742
743 LOG(VB_RECORD, LOG_DEBUG, LOC + "ParseM3U8 -- end");
744 return true;
745}
746
748{
749 if (!m_curstream || m_cancel)
750 return false;
751
752 LOG(VB_RECORD, LOG_DEBUG, LOC +
753 QString("LoadMetaPlaylists stream %1")
754 .arg(m_curstream->toString()));
755
756 StreamContainer::iterator Istream;
757
758 m_streamLock.lock();
759 if (m_bandwidthCheck /* && !m_segments.empty() */)
760 {
761 int buffered = PercentBuffered();
762
763 if (buffered < 15)
764 {
765 // It is taking too long to download the segments
766 LOG(VB_RECORD, LOG_WARNING, LOC +
767 QString("Falling behind: only %1% buffered").arg(buffered));
768 LOG(VB_RECORD, LOG_DEBUG, LOC +
769 QString("playlist size %1, queued %2")
770 .arg(m_playlistSize).arg(m_segments.size()));
773 m_bandwidthCheck = false;
774 }
775 else if (buffered > 85)
776 {
777 // Keeping up easily, raise the bitrate.
778 LOG(VB_RECORD, LOG_DEBUG, LOC +
779 QString("Plenty of bandwidth, downloading %1 of %2")
780 .arg(m_playlistSize - m_segments.size())
781 .arg(m_playlistSize));
782 LOG(VB_RECORD, LOG_DEBUG, LOC +
783 QString("playlist size %1, queued %2")
784 .arg(m_playlistSize).arg(m_segments.size()));
786 m_bandwidthCheck = false;
787 }
788 }
789 m_streamLock.unlock();
790
791 QByteArray buffer;
792
793#ifdef HLS_USE_MYTHDOWNLOADMANAGER // MythDownloadManager leaks memory
794 if (!DownloadURL(m_curstream->Url(), &buffer))
795 return false;
796#else
797 QString redir;
798 QUrl m3uUrl { m_curstream->M3U8Url() };
799 if (!m3uUrl.isValid())
800 {
801 LOG(VB_GENERAL, LOG_WARNING,
802 LOC + "Download failed: invalid url " + m_curstream->M3U8Url());
803 return false;
804 }
805 if (!downloader.DownloadURL(m3uUrl, &buffer, 30s, 0, 0, &redir))
806 {
807 LOG(VB_GENERAL, LOG_WARNING,
808 LOC + "Download failed: " + downloader.ErrorString());
809 return false;
810 }
811#endif
812
813 if (m_segmentBase != redir)
814 {
815 m_segmentBase = redir;
817 }
818
819 if (m_cancel || !ParseM3U8(buffer, m_curstream))
820 return false;
821
822 // Signal SegmentWorker that there is work to do...
824
825 return true;
826}
827
829{
830 HLSRecStream *hls = nullptr;
831 uint64_t bitrate = m_curstream->Bitrate();
832 uint64_t candidate = 0;
833
834 for (auto Istream = m_streams.cbegin(); Istream != m_streams.cend(); ++Istream)
835 {
836 if ((*Istream)->Id() != progid)
837 continue;
838 if (bitrate > (*Istream)->Bitrate() &&
839 candidate < (*Istream)->Bitrate())
840 {
841 LOG(VB_RECORD, LOG_DEBUG, LOC +
842 QString("candidate stream '%1' bitrate %2 >= %3")
843 .arg(Istream.key()).arg(bitrate).arg((*Istream)->Bitrate()));
844 hls = *Istream;
845 candidate = hls->Bitrate();
846 }
847 }
848
849 if (hls)
850 {
851 LOG(VB_RECORD, LOG_INFO, LOC +
852 QString("Switching to a lower bitrate stream %1 -> %2")
853 .arg(bitrate).arg(candidate));
854 m_curstream = hls;
855 }
856 else
857 {
858 LOG(VB_RECORD, LOG_DEBUG, LOC +
859 QString("Already at lowest bitrate %1").arg(bitrate));
860 }
861}
862
864{
865 HLSRecStream *hls = nullptr;
866 uint64_t bitrate = m_curstream->Bitrate();
867 uint64_t candidate = INT_MAX;
868
869 for (auto Istream = m_streams.cbegin(); Istream != m_streams.cend(); ++Istream)
870 {
871 if ((*Istream)->Id() != progid)
872 continue;
873 if (bitrate < (*Istream)->Bitrate() &&
874 candidate > (*Istream)->Bitrate())
875 {
876 LOG(VB_RECORD, LOG_DEBUG, LOC +
877 QString("candidate stream '%1' bitrate %2 >= %3")
878 .arg(Istream.key()).arg(bitrate).arg((*Istream)->Bitrate()));
879 hls = *Istream;
880 candidate = hls->Bitrate();
881 }
882 }
883
884 if (hls)
885 {
886 LOG(VB_RECORD, LOG_INFO, LOC +
887 QString("Switching to a higher bitrate stream %1 -> %2")
888 .arg(bitrate).arg(candidate));
889 m_curstream = hls;
890 }
891 else
892 {
893 LOG(VB_RECORD, LOG_DEBUG, LOC +
894 QString("Already at highest bitrate %1").arg(bitrate));
895 }
896}
897
899{
900 LOG(VB_RECORD, LOG_DEBUG, LOC + "LoadSegment -- start");
901
902 if (!m_curstream)
903 {
904 LOG(VB_RECORD, LOG_ERR, LOC + "LoadSegment: current stream not set.");
906 return false;
907 }
908
910 for (;;)
911 {
912 m_seqLock.lock();
913 if (m_cancel || m_segments.empty())
914 {
915 m_seqLock.unlock();
916 break;
917 }
918
919 seg = m_segments.front();
920 if (m_segments.size() > m_playlistSize)
921 {
922 LOG(VB_RECORD, (m_debug ? LOG_INFO : LOG_DEBUG), LOC +
923 QString("Downloading segment %1 (1 of %2) with %3 behind")
924 .arg(seg.Sequence())
925 .arg(m_segments.size() + m_playlistSize)
926 .arg(m_segments.size() - m_playlistSize));
927 }
928 else
929 {
930 LOG(VB_RECORD, (m_debug ? LOG_INFO : LOG_DEBUG), LOC +
931 QString("Downloading segment %1 (%2 of %3)")
932 .arg(seg.Sequence())
933 .arg(m_playlistSize - m_segments.size() + 1)
934 .arg(m_playlistSize));
935 }
936 m_seqLock.unlock();
937
938 m_streamLock.lock();
940 m_streamLock.unlock();
941 if (!hls)
942 {
943 LOG(VB_RECORD, LOG_DEBUG, LOC + "LoadSegment -- no current stream");
944 return false;
945 }
946
947 long throttle = DownloadSegmentData(downloader, hls, seg, m_playlistSize);
948
949 m_seqLock.lock();
950 if (throttle < 0)
951 {
952 if (m_segments.size() > m_playlistSize)
953 {
954 SegmentContainer::iterator Iseg = m_segments.begin() +
955 (m_segments.size() - m_playlistSize);
956 m_segments.erase(m_segments.begin(), Iseg);
957 }
958 m_seqLock.unlock();
959 return false;
960 }
961
962 m_curSeq = m_segments.front().Sequence();
963 m_segments.pop_front();
964
965 m_seqLock.unlock();
966
967 if (m_throttle && throttle == 0)
968 throttle = 2;
969 else if (throttle > 8)
970 throttle = 8;
971 if (throttle > 0)
972 {
973 LOG(VB_RECORD, LOG_INFO, LOC +
974 QString("Throttling -- sleeping %1 secs.")
975 .arg(throttle));
976 throttle *= 1000;
977 m_throttleLock.lock();
978 if (m_throttleCond.wait(&m_throttleLock, throttle))
979 LOG(VB_RECORD, LOG_INFO, LOC + "Throttle aborted");
980 m_throttleLock.unlock();
981 LOG(VB_RECORD, LOG_INFO, LOC + "Throttle done");
982 }
983 else
984 {
985 std::this_thread::sleep_for(5ms);
986 }
987
988 if (m_prebufferCnt == 0)
989 {
991 m_prebufferCnt = 2;
992 }
993 else
994 {
996 }
997 }
998
999 LOG(VB_RECORD, LOG_DEBUG, LOC + "LoadSegment -- end");
1000 return true;
1001}
1002
1004{
1005 if (m_playlistSize == 0 || m_segments.size() > m_playlistSize)
1006 return 0;
1007 return (static_cast<float>(m_playlistSize - m_segments.size()) /
1008 static_cast<float>(m_playlistSize)) * 100.0F;
1009}
1010
1012 HLSRecStream* hls,
1013 HLSRecSegment& segment, int playlist_size)
1014{
1015 uint64_t bandwidth = hls->AverageBandwidth();
1016
1017 LOG(VB_RECORD, LOG_DEBUG, LOC +
1018 QString("Downloading seq#%1 av.bandwidth:%2 bitrate:%3")
1019 .arg(segment.Sequence()).arg(bandwidth).arg(hls->Bitrate()));
1020
1021 /* sanity check - can we download this segment on time? */
1022 if ((bandwidth > 0) && (hls->Bitrate() > 0) && (segment.Duration().count() > 0))
1023 {
1024 uint64_t size = (segment.Duration().count() * hls->Bitrate()); /* bits */
1025 auto estimated_time = std::chrono::milliseconds(size / bandwidth);
1026 if (estimated_time > segment.Duration())
1027 {
1028 LOG(VB_RECORD, LOG_WARNING, LOC +
1029 QString("downloading of %1 will take %2ms, "
1030 "which is longer than its playback (%3ms) at %4kB/s")
1031 .arg(segment.Sequence())
1032 .arg(estimated_time.count())
1033 .arg(segment.Duration().count())
1034 .arg(bandwidth / 8000));
1035 }
1036 LOG(VB_RECORD, LOG_DEBUG, LOC +
1037 QString(" sequence:%1").arg(segment.Sequence()) +
1038 QString(" bandwidth:%1").arg(bandwidth) +
1039 QString(" hls->Bitrate:%1").arg(hls->Bitrate()) +
1040 QString(" seg.Dur.cnt:%1").arg(segment.Duration().count()) +
1041 QString(" est_time:%1").arg(estimated_time.count()));
1042 }
1043
1044 QByteArray buffer;
1045 auto start = nowAsDuration<std::chrono::milliseconds>();
1046
1047#ifdef HLS_USE_MYTHDOWNLOADMANAGER // MythDownloadManager leaks memory
1048 // and can only handle six download at a time
1049 if (!HLSReader::DownloadURL(segment.Url(), &buffer))
1050 {
1051 LOG(VB_RECORD, LOG_ERR, LOC +
1052 QString("%1 failed").arg(segment.Sequence()));
1053 if (estimated_time * 2 < segment.Duration())
1054 {
1055 if (!HLSReader::DownloadURL(segment.Url(), &buffer))
1056 {
1057 LOG(VB_RECORD, LOG_ERR, LOC + QString("%1 failed")
1058 .arg(segment.Sequence()));
1059 return -1;
1060 }
1061 }
1062 else
1063 return 0;
1064 }
1065#else
1066 if (!downloader.DownloadURL(segment.Url(), &buffer))
1067 {
1068 LOG(VB_RECORD, LOG_ERR, LOC + QString("%1 failed: %2")
1069 .arg(segment.Sequence()).arg(downloader.ErrorString()));
1070 return -1;
1071 }
1072#endif
1073
1074 auto downloadduration = nowAsDuration<std::chrono::milliseconds>() - start;
1075
1076 LOG(VB_RECORD, LOG_DEBUG, LOC +
1077 QString("Downloaded segment %1 %2").arg(segment.Sequence()).arg(segment.Url().toString()));
1078
1079#if CONFIG_LIBCRYPTO
1080 /* If the segment is encrypted, decode it */
1081 if (segment.HasKeyPath())
1082 {
1083 if (!hls->DecodeData(downloader,
1084 segment.IVLoaded() ? segment.AESIV() : QByteArray(),
1085 segment.KeyPath(),
1086 buffer,
1087 segment.Sequence()))
1088 return 0;
1089
1090 LOG(VB_RECORD, LOG_DEBUG, LOC +
1091 QString("Decoded segment sequence %1").arg(segment.Sequence()));
1092 }
1093#endif // CONFIG_LIBCRYPTO
1094 int64_t segment_len = buffer.size();
1095
1096 m_bufLock.lock();
1097 if (m_buffer.size() > segment_len * playlist_size)
1098 {
1099 LOG(VB_RECORD, LOG_WARNING, LOC +
1100 QString("streambuffer is not reading fast enough. "
1101 "buffer size %1").arg(m_buffer.size()));
1103 if (++m_slowCnt > 15)
1104 {
1105 m_slowCnt = 15;
1106 m_fatal = true;
1107 return -1;
1108 }
1109 }
1110 else if (m_slowCnt > 0)
1111 {
1112 --m_slowCnt;
1113 }
1114
1115 if (m_buffer.size() >= segment_len * playlist_size * 2)
1116 {
1117 LOG(VB_RECORD, LOG_WARNING, LOC +
1118 QString("streambuffer is not reading fast enough. "
1119 "buffer size %1. Dropping %2 bytes")
1120 .arg(m_buffer.size()).arg(segment_len));
1121 m_buffer.remove(0, segment_len);
1122 }
1123
1124 m_buffer += buffer;
1125 m_bufLock.unlock();
1126
1127 if (hls->Bitrate() == 0 && segment.Duration() > 0s)
1128 {
1129 /* Try to estimate the bandwidth for this stream */
1130 hls->SetBitrate((uint64_t)(((double)segment_len * 8 * 1000) /
1131 ((double)segment.Duration().count())));
1132 }
1133
1134 if (downloadduration < 1ms)
1135 downloadduration = 1ms;
1136
1137 /* bits/sec */
1138 bandwidth = 8ULL * 1000 * segment_len / downloadduration.count();
1139 hls->AverageBandwidth(bandwidth);
1140 if (segment.Duration() > 0s)
1141 {
1142 hls->SetCurrentByteRate(segment_len * 1000 / segment.Duration().count());
1143 }
1144
1145 LOG(VB_RECORD, (m_debug ? LOG_INFO : LOG_DEBUG), LOC +
1146 QString("Sequence %1 took %2ms for %3 bytes, bandwidth:%4kB/s byterate:%5kB/s")
1147 .arg(segment.Sequence())
1148 .arg(downloadduration.count())
1149 .arg(segment_len)
1150 .arg(bandwidth / 8000)
1151 .arg(hls->CurrentByteRate() / 1000));
1152
1153 return m_slowCnt;
1154}
1155
1157{
1158 QMutexLocker lock(&m_streamLock);
1159 if (m_curstream)
1160 m_curstream->Good();
1161}
1162
1164{
1165 QMutexLocker lock(&m_streamLock);
1166 if (m_curstream)
1168}
1169
1171{
1172 QMutexLocker lock(&m_streamLock);
1173 if (m_curstream)
1174 return m_curstream->RetryCount();
1175 return 0;
1176}
1177
1179{
1180 m_debug = true;
1181 m_debugCnt = 5;
1182 LOG(VB_RECORD, LOG_INFO, LOC + "Debugging enabled");
1183}
#define LOC
Definition: HLSReader.cpp:19
static QUrl RelativeURI(const QString &baseString, const QString &uriString)
Handles relative URLs without breaking URI encoded parameters by avoiding storing the decoded URL in ...
Definition: HLSReader.cpp:26
void DecreaseBitrate(int progid)
Definition: HLSReader.cpp:828
static bool IsValidPlaylist(QTextStream &text)
Definition: HLSReader.cpp:276
~HLSReader(void)
Definition: HLSReader.cpp:34
QWaitCondition m_throttleCond
Definition: HLSReader.h:133
QMutex m_bufLock
Definition: HLSReader.h:141
StreamContainer m_streams
Definition: HLSReader.h:107
QMutex m_seqLock
Definition: HLSReader.h:126
bool m_cancel
Definition: HLSReader.h:114
QMutex m_throttleLock
Definition: HLSReader.h:132
void PlaylistRetrying(void)
Definition: HLSReader.cpp:1163
int m_playlistSize
Definition: HLSReader.h:122
bool m_fatal
Definition: HLSReader.h:113
friend class HLSStreamWorker
Definition: HLSReader.h:44
HLSStreamWorker * m_streamWorker
Definition: HLSReader.h:120
QVector< HLSRecSegment > SegmentContainer
Definition: HLSReader.h:49
bool IsOpen(const QString &url) const
Definition: HLSReader.h:59
bool ParseM3U8(const QByteArray &buffer, HLSRecStream *stream=nullptr)
Definition: HLSReader.cpp:313
int m_debugCnt
Definition: HLSReader.h:136
QMutex m_workerLock
Definition: HLSReader.h:130
HLSRecStream * m_curstream
Definition: HLSReader.h:109
uint m_prebufferCnt
Definition: HLSReader.h:124
QByteArray m_buffer
Definition: HLSReader.h:140
static void CancelURL(const QString &url)
SegmentContainer m_segments
Definition: HLSReader.h:108
bool m_bandwidthCheck
Definition: HLSReader.h:123
void EnableDebugging(void)
Definition: HLSReader.cpp:1178
int m_bitrateIndex
Definition: HLSReader.h:111
int DownloadSegmentData(MythSingleDownload &downloader, HLSRecStream *hls, HLSRecSegment &segment, int playlist_size)
Definition: HLSReader.cpp:1011
bool m_throttle
Definition: HLSReader.h:115
bool m_debug
Definition: HLSReader.h:135
QString StreamURL(void) const
Definition: HLSReader.h:68
void PlaylistGood(void)
Definition: HLSReader.cpp:1156
uint PercentBuffered(void) const
Definition: HLSReader.cpp:1003
void IncreaseBitrate(int progid)
Definition: HLSReader.cpp:863
QMutex m_streamLock
Definition: HLSReader.h:128
void ResetSequence(void)
Definition: HLSReader.h:66
bool LoadSegments(MythSingleDownload &downloader)
Definition: HLSReader.cpp:898
QString m_segmentBase
Definition: HLSReader.h:106
int64_t m_curSeq
Definition: HLSReader.h:110
void Throttle(bool val)
Definition: HLSReader.cpp:209
QString m_m3u8
Definition: HLSReader.h:105
qint64 Read(uint8_t *buffer, qint64 len)
Definition: HLSReader.cpp:223
bool Open(const QString &m3u, int bitrate_index=0)
Definition: HLSReader.cpp:41
bool LoadMetaPlaylists(MythSingleDownload &downloader)
Definition: HLSReader.cpp:747
int m_inputId
Definition: HLSReader.h:144
int PlaylistRetryCount(void) const
Definition: HLSReader.cpp:1170
int m_slowCnt
Definition: HLSReader.h:139
void Cancel(bool quiet=false)
Definition: HLSReader.cpp:180
friend class HLSPlaylistWorker
Definition: HLSReader.h:45
HLSPlaylistWorker * m_playlistWorker
Definition: HLSReader.h:119
bool m_aesMsg
Definition: HLSReader.h:117
void Close(bool quiet=false)
Definition: HLSReader.cpp:156
std::chrono::milliseconds Duration(void) const
Definition: HLSSegment.h:36
int64_t Sequence(void) const
Definition: HLSSegment.h:33
QUrl Url(void) const
Definition: HLSSegment.h:35
void Good(void)
Definition: HLSStream.h:68
void SetDiscontinuitySequence(int s)
Definition: HLSStream.h:46
void SetCache(bool x)
Definition: HLSStream.h:53
QString M3U8Url(void) const
Definition: HLSStream.h:57
bool Live(void) const
Definition: HLSStream.h:55
void SetSegmentBaseUrl(const QString &n)
Definition: HLSStream.h:59
void SetCurrentByteRate(uint64_t byterate)
Definition: HLSStream.h:51
std::chrono::seconds TargetDuration(void) const
Definition: HLSStream.h:43
QString toString(void) const
Definition: HLSStream.cpp:47
void SetLive(bool x)
Definition: HLSStream.h:56
void SetDateTime(QDateTime &dt)
Definition: HLSStream.h:54
void SetTargetDuration(std::chrono::seconds x)
Definition: HLSStream.h:44
QString SegmentBaseUrl(void) const
Definition: HLSStream.h:58
uint64_t Bitrate(void) const
Definition: HLSStream.h:48
int RetryCount(void) const
Definition: HLSStream.h:70
uint64_t CurrentByteRate(void) const
Definition: HLSStream.h:50
int Id(void) const
Definition: HLSStream.h:40
void SetMapUri(const QString &x)
Definition: HLSStream.h:61
void Retrying(void)
Definition: HLSStream.h:69
int Version(void) const
Definition: HLSStream.h:41
void SetBitrate(uint64_t bitrate)
Definition: HLSStream.h:49
void SetVersion(int x)
Definition: HLSStream.h:42
uint64_t AverageBandwidth(void) const
Definition: HLSStream.h:47
void CancelCurrentDownload(void)
void Wakeup(void)
void start(QThread::Priority p=QThread::InheritPriority)
Tell MThread to start running the thread in the near future.
Definition: mthread.cpp:281
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.
QString ErrorString(void) const
bool DownloadURL(const QUrl &url, QByteArray *buffer, std::chrono::seconds timeout=30s, uint redirs=0, qint64 maxsize=0, QString *final_url=nullptr)
unsigned int uint
Definition: compat.h:60
MythDownloadManager * GetMythDownloadManager(void)
Gets the pointer to the MythDownloadManager singleton.
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
bool ParseDiscontinuitySequence(const QString &line, const QString &loc, int &discontinuity_sequence)
Definition: m3u.cpp:520
bool ParseVersion(const QString &line, const QString &loc, int &version)
Definition: m3u.cpp:92
bool ParseAllowCache(const QString &line, const QString &loc, bool &do_cache)
Definition: m3u.cpp:487
bool ParseEndList(const QString &loc, bool &is_vod)
Definition: m3u.cpp:552
bool ParseMap(const QString &line, const QString &loc, QString &uri)
Definition: m3u.cpp:436
bool ParseTargetDuration(const QString &line, const QString &loc, int &duration)
Definition: m3u.cpp:234
bool ParseIndependentSegments(const QString &line, const QString &loc)
Definition: m3u.cpp:564
bool ParseMediaSequence(int64_t &sequence_num, const QString &line, const QString &loc)
Definition: m3u.cpp:329
bool ParseDiscontinuity(const QString &line, const QString &loc)
Definition: m3u.cpp:544
bool ParseProgramDateTime(const QString &line, const QString &loc, QDateTime &dt)
Definition: m3u.cpp:467
bool ParseSegmentInformation(int version, const QString &line, int &duration, QString &title, const QString &loc)
Definition: m3u.cpp:251
QString DecodedURI(const QString &uri)
Definition: m3u.cpp:15
bool ParseKey(int version, const QString &line, bool &aesmsg, const QString &loc, QString &path, QString &iv)
Definition: m3u.cpp:350
bool ParseStreamInformation(const QString &line, const QString &url, const QString &loc, int &id, uint64_t &bandwidth, QString &audio, QString &video)
Definition: m3u.cpp:127
string version
Definition: giantbomb.py:185
static eu8 clamp(eu8 value, eu8 low, eu8 high)
Definition: pxsup2dast.c:201