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