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