MythTV  master
m3u.cpp
Go to the documentation of this file.
1 #include <QRegularExpression>
2 #include <QStringList>
3 #include <QUrl>
4 
5 #include "libmythbase/mythdate.h"
7 #include "HLS/m3u.h"
8 
9 namespace M3U
10 {
11  static const QRegularExpression kQuotes{"^\"|\"$"};
12 
13  QString DecodedURI(const QString& uri)
14  {
15  QByteArray ba = uri.toLatin1();
16  QUrl url = QUrl::fromEncoded(ba);
17  return url.toString();
18  }
19 
20  QString RelativeURI(const QString& surl, const QString& spath)
21  {
22  QUrl url = QUrl(surl);
23  QUrl path = QUrl(spath);
24 
25  if (!path.isRelative())
26  return spath;
27 
28  return url.resolved(path).toString();
29  }
30 
31  QString ParseAttributes(const QString& line, const char *attr)
32  {
33  int p = line.indexOf(QLatin1String(":"));
34  if (p < 0)
35  return {};
36 
37  QStringList list = line.mid(p + 1).split(',');
38  for (const auto & it : std::as_const(list))
39  {
40  QString arg = it.trimmed();
41  if (arg.startsWith(attr))
42  {
43  int pos = arg.indexOf(QLatin1String("="));
44  if (pos < 0)
45  continue;
46  return arg.mid(pos+1);
47  }
48  }
49  return {};
50  }
51 
56  bool ParseDecimalValue(const QString& line, int &target)
57  {
58  int p = line.indexOf(QLatin1String(":"));
59  if (p < 0)
60  return false;
61  int i = p + 1;
62  for ( ; i < line.size(); i++)
63  if (!line[i].isNumber())
64  break;
65  if (i == p + 1)
66  return false;
67  target = line.mid(p + 1, i - p - 1).toInt();
68  return true;
69  }
70 
75  bool ParseDecimalValue(const QString& line, int64_t &target)
76  {
77  int p = line.indexOf(QLatin1String(":"));
78  if (p < 0)
79  return false;
80  int i = p + 1;
81  for ( ; i < line.size(); i++)
82  if (!line[i].isNumber())
83  break;
84  if (i == p + 1)
85  return false;
86  target = line.mid(p + 1, i - p - 1).toInt();
87  return true;
88  }
89 
90  bool ParseVersion(const QString& line, const QString& loc, int& version)
91  {
92  /*
93  * The EXT-X-VERSION tag indicates the compatibility version of the
94  * Playlist file. The Playlist file, its associated media, and its
95  * server MUST comply with all provisions of the most-recent version of
96  * this document describing the protocol version indicated by the tag
97  * value.
98  *
99  * Its format is:
100  *
101  * #EXT-X-VERSION:<n>
102  */
103 
104  if (line.isNull() || !ParseDecimalValue(line, version))
105  {
106  LOG(VB_RECORD, LOG_ERR, loc +
107  "#EXT-X-VERSION: no protocol version found, should be version 1.");
108  version = 1;
109  return false;
110  }
111 
112  if (version < 1 || version > 7)
113  {
114  LOG(VB_RECORD, LOG_ERR, loc +
115  QString("#EXT-X-VERSION is %1, but we only understand 1 to 7")
116  .arg(version));
117  return false;
118  }
119 
120  return true;
121  }
122 
123  // EXT-X-STREAM-INF
124  //
125  bool ParseStreamInformation(const QString& line,
126  const QString& url,
127  const QString& loc,
128  int& id,
129  uint64_t& bandwidth,
130  QString& audio,
131  QString& video)
132  {
133  LOG(VB_RECORD, LOG_INFO, loc +
134  QString("Parsing stream from %1").arg(url));
135 
136  /*
137  * #EXT-X-STREAM-INF:[attribute=value][,attribute=value]*
138  * <URI>
139  */
140  QString attr;
141 
142  /* The PROGRAM-ID attribute of the EXT-X-STREAM-INF and the EXT-X-I-
143  * FRAME-STREAM-INF tags was removed in protocol version 6.
144  */
145  attr = ParseAttributes(line, "PROGRAM-ID");
146  if (attr.isNull())
147  {
148  LOG(VB_RECORD, LOG_INFO, loc +
149  "#EXT-X-STREAM-INF: No PROGRAM-ID=<value>, using 1");
150  id = 1;
151  }
152  else
153  {
154  id = attr.toInt();
155  }
156 
157  attr = ParseAttributes(line, "BANDWIDTH");
158  if (attr.isNull())
159  {
160  LOG(VB_RECORD, LOG_ERR, loc +
161  "#EXT-X-STREAM-INF: expected BANDWIDTH=<value>");
162  return false;
163  }
164  bandwidth = attr.toInt();
165 
166  if (bandwidth == 0)
167  {
168  LOG(VB_RECORD, LOG_ERR, loc +
169  "#EXT-X-STREAM-INF: bandwidth cannot be 0");
170  return false;
171  }
172 
173  // AUDIO
174  //
175  // The value is a quoted-string. It MUST match the value of the
176  // GROUP-ID attribute of an EXT-X-MEDIA tag elsewhere in the Master
177  // Playlist whose TYPE attribute is AUDIO. It indicates the set of
178  // audio Renditions that SHOULD be used when playing the
179  // presentation. See Section 4.3.4.2.1.
180  //
181  // The AUDIO attribute is OPTIONAL.
182  audio = ParseAttributes(line, "AUDIO");
183  if (!audio.isEmpty())
184  {
185  audio.replace(M3U::kQuotes, "");
186  LOG(VB_RECORD, LOG_INFO, loc +
187  QString("#EXT-X-STREAM-INF: attribute AUDIO=%1").arg(audio));
188  }
189 
190  // The VIDEO attribute is OPTIONAL.
191  video = ParseAttributes(line, "VIDEO");
192  if (!video.isEmpty())
193  {
194  video.replace(M3U::kQuotes, "");
195  LOG(VB_RECORD, LOG_INFO, loc +
196  QString("#EXT-X-STREAM-INF: attribute VIDEO=%1").arg(video));
197  }
198 
199 
200  LOG(VB_RECORD, LOG_INFO, loc +
201  QString("bandwidth adaptation detected (program-id=%1, bandwidth=%2)")
202  .arg(id).arg(bandwidth));
203 
204  return true;
205  }
206 
207  // EXT-X-MEDIA
208  //
209  bool ParseMedia(const QString& line,
210  const QString& loc,
211  QString& media_type,
212  QString& group_id,
213  QString& uri,
214  QString& name)
215  {
216  LOG(VB_RECORD, LOG_INFO, loc + QString("Parsing EXT-X-MEDIA line"));
217 
218  media_type = ParseAttributes(line, "TYPE");
219  group_id = ParseAttributes(line, "GROUP-ID");
220  uri = ParseAttributes(line, "URI");
221  name = ParseAttributes(line, "NAME");
222 
223  // Remove string quotes
224  group_id.replace(M3U::kQuotes, "");
225  uri.replace(M3U::kQuotes, "");
226  name.replace(M3U::kQuotes, "");
227 
228  return true;
229  }
230 
231 
232  bool ParseTargetDuration(const QString& line, const QString& loc,
233  int& duration)
234  {
235  /*
236  * #EXT-X-TARGETDURATION:<s>
237  *
238  * where s is an integer indicating the target duration in seconds.
239  */
240  if (!ParseDecimalValue(line, duration))
241  {
242  LOG(VB_RECORD, LOG_ERR, loc + "expected #EXT-X-TARGETDURATION:<s>");
243  return false;
244  }
245 
246  return true;
247  }
248 
249  bool ParseSegmentInformation(int version, const QString& line,
250  int& duration, QString& title,
251  const QString& loc)
252  {
253  /*
254  * #EXTINF:<duration>,<title>
255  *
256  * where duration is a decimal-floating-point or decimal-integer number
257  * (as described in Section 4.2) that specifies the duration of the
258  * Media Segment in seconds. Durations SHOULD be decimal-floating-
259  * point, with enough accuracy to avoid perceptible error when segment
260  * durations are accumulated. However, if the compatibility version
261  * number is less than 3, durations MUST be integers. Durations that
262  * are reported as integers SHOULD be rounded to the nearest integer.
263  * The remainder of the line following the comma is an optional human-
264  * readable informative title of the Media Segment expressed as UTF-8
265  * text.
266  */
267  int p = line.indexOf(QLatin1String(":"));
268  if (p < 0)
269  {
270  LOG(VB_RECORD, LOG_ERR, loc +
271  QString("ParseSegmentInformation: Missing ':' in '%1'")
272  .arg(line));
273  return false;
274  }
275 
276  QStringList list = line.mid(p + 1).split(',');
277 
278  /* read duration */
279  if (list.isEmpty())
280  {
281  LOG(VB_RECORD, LOG_ERR, loc +
282  QString("ParseSegmentInformation: Missing arguments in '%1'")
283  .arg(line));
284  return false;
285  }
286 
287  // Duration in ms
288  bool ok = false;
289  const QString& val = list[0];
290  if (version < 3)
291  {
292  int duration_seconds = val.toInt(&ok);
293  if (ok)
294  {
295  duration = duration_seconds * 1000;
296  }
297  else
298  {
299  LOG(VB_RECORD, LOG_ERR, loc +
300  QString("ParseSegmentInformation: invalid duration in '%1'")
301  .arg(line));
302  return false;
303  }
304  }
305  else
306  {
307  double d = val.toDouble(&ok);
308  if (!ok)
309  {
310  LOG(VB_RECORD, LOG_ERR, loc +
311  QString("ParseSegmentInformation: invalid duration in '%1'")
312  .arg(line));
313  return false;
314  }
315  duration = static_cast<int>(d * 1000);
316  }
317 
318  if (list.size() >= 2)
319  {
320  title = list[1];
321  }
322 
323  /* Ignore the rest of the line */
324  return true;
325  }
326 
327  bool ParseMediaSequence(int64_t & sequence_num,
328  const QString& line, const QString& loc)
329  {
330  /*
331  * #EXT-X-MEDIA-SEQUENCE:<number>
332  *
333  * A Playlist file MUST NOT contain more than one EXT-X-MEDIA-SEQUENCE
334  * tag. If the Playlist file does not contain an EXT-X-MEDIA-SEQUENCE
335  * tag then the sequence number of the first URI in the playlist SHALL
336  * be considered to be 0.
337  */
338 
339  if (!ParseDecimalValue(line, sequence_num))
340  {
341  LOG(VB_RECORD, LOG_ERR, loc + "expected #EXT-X-MEDIA-SEQUENCE:<s>");
342  return false;
343  }
344 
345  return true;
346  }
347 
348  bool ParseKey(int version, const QString& line,
349  [[maybe_unused]] bool& aesmsg,
350  const QString& loc, QString &path, QString &iv)
351  {
352  /*
353  * #EXT-X-KEY:METHOD=<method>[,URI="<URI>"][,IV=<IV>]
354  *
355  * The METHOD attribute specifies the encryption method. Two encryption
356  * methods are defined: NONE and AES-128.
357  */
358 
359  path.clear();
360  iv.clear();
361 
362  QString attr = ParseAttributes(line, "METHOD");
363  if (attr.isNull())
364  {
365  LOG(VB_RECORD, LOG_ERR, loc + "#EXT-X-KEY: expected METHOD=<value>");
366  return false;
367  }
368 
369  if (attr.startsWith(QLatin1String("NONE")))
370  {
371  QString uri = ParseAttributes(line, "URI");
372  if (!uri.isEmpty())
373  {
374  LOG(VB_RECORD, LOG_ERR, loc + "#EXT-X-KEY: URI not expected");
375  return false;
376  }
377  /* IV is only supported in version 2 and above */
378  if (version >= 2)
379  {
380  QString parsed_iv = ParseAttributes(line, "IV");
381  if (!parsed_iv.isEmpty())
382  {
383  LOG(VB_RECORD, LOG_ERR, loc + "#EXT-X-KEY: IV not expected");
384  return false;
385  }
386  }
387  }
388 #ifdef USING_LIBCRYPTO
389  else if (attr.startsWith(QLatin1String("AES-128")))
390  {
391  QString uri;
392  if (!aesmsg)
393  {
394  LOG(VB_RECORD, LOG_INFO, loc +
395  "playback of AES-128 encrypted HTTP Live media detected.");
396  aesmsg = true;
397  }
398  uri = ParseAttributes(line, "URI");
399  if (uri.isNull())
400  {
401  LOG(VB_RECORD, LOG_ERR, loc + "#EXT-X-KEY: URI not found for "
402  "encrypted HTTP Live media in AES-128");
403  return false;
404  }
405 
406  /* Url is between quotes, remove them */
407  path = DecodedURI(uri.remove(QChar(QLatin1Char('"'))));
408  iv = ParseAttributes(line, "IV");
409 
410  LOG(VB_RECORD, LOG_DEBUG, QString("M3U::ParseKey #EXT-X-KEY: %1").arg(line));
411  LOG(VB_RECORD, LOG_DEBUG, QString("M3U::ParseKey path:%1 IV:%2").arg(path, iv));
412  }
413  else if (attr.startsWith(QLatin1String("SAMPLE-AES")))
414  {
415  LOG(VB_RECORD, LOG_ERR, loc + "encryption SAMPLE-AES not supported.");
416  return false;
417  }
418 #endif
419  else
420  {
421 #ifndef _MSC_VER
422  LOG(VB_RECORD, LOG_ERR, loc +
423  "invalid encryption type, only NONE "
424 #ifdef USING_LIBCRYPTO
425  "and AES-128 are supported"
426 #else
427  "is supported."
428 #endif
429  );
430 #else
431 // msvc doesn't like #ifdef in the middle of the LOG macro.
432 // Errors with '#':invalid character: possibly the result of a macro expansion
433 #endif
434  return false;
435  }
436  return true;
437  }
438 
439  bool ParseMap(const QString &line,
440  const QString &loc,
441  QString &uri)
442  {
443  /*
444  * #EXT-X-MAP:<attribute-list>
445  *
446  * The EXT-X-MAP tag specifies how to obtain the Media Initialization
447  * Section (Section 3) required to parse the applicable Media Segments.
448  * It applies to every Media Segment that appears after it in the
449  * Playlist until the next EXT-X-MAP tag or until the end of the
450  * Playlist.
451  *
452  * The following attributes are defined:
453  *
454  * URI
455  * The value is a quoted-string containing a URI that identifies a
456  * resource that contains the Media Initialization Section. This
457  * attribute is REQUIRED.
458  */
459  uri = ParseAttributes(line, "URI");
460  if (uri.isEmpty())
461  {
462  LOG(VB_RECORD, LOG_ERR, loc +
463  QString("Attribute URI not present in: #EXT-X-MAP %1")
464  .arg(line));
465  return false;
466  }
467  return true;
468  }
469 
470  bool ParseProgramDateTime(const QString& line, const QString& loc,
471  QDateTime &dt)
472  {
473  /*
474  * #EXT-X-PROGRAM-DATE-TIME:<YYYY-MM-DDThh:mm:ssZ>
475  */
476  int p = line.indexOf(QLatin1String(":"));
477  if (p < 0)
478  {
479  LOG(VB_RECORD, LOG_ERR, loc +
480  QString("ParseProgramDateTime: Missing ':' in '%1'")
481  .arg(line));
482  return false;
483  }
484 
485  QString dt_string = line.mid(p+1);
486  dt = MythDate::fromString(dt_string);
487  return true;
488  }
489 
490  bool ParseAllowCache(const QString& line, const QString& loc, bool &do_cache)
491  {
492  /*
493  * The EXT-X-ALLOW-CACHE tag indicates whether the client MAY or MUST
494  * NOT cache downloaded media files for later replay. It MAY occur
495  * anywhere in the Playlist file; it MUST NOT occur more than once. The
496  * EXT-X-ALLOW-CACHE tag applies to all segments in the playlist. Its
497  * format is:
498  *
499  * #EXT-X-ALLOW-CACHE:<YES|NO>
500  */
501 
502  /* The EXT-X-ALLOW-CACHE tag was removed in protocol version 7.
503  */
504  int pos = line.indexOf(QLatin1String(":"));
505  if (pos < 0)
506  {
507  LOG(VB_RECORD, LOG_ERR, loc +
508  QString("ParseAllowCache: missing ':' in '%1'")
509  .arg(line));
510  return false;
511  }
512  QString answer = line.mid(pos+1, 3);
513  if (answer.size() < 2)
514  {
515  LOG(VB_RECORD, LOG_ERR, loc + "#EXT-X-ALLOW-CACHE, ignoring ...");
516  return true;
517  }
518  do_cache = (!answer.startsWith(QLatin1String("NO")));
519 
520  return true;
521  }
522 
523  bool ParseDiscontinuitySequence(const QString& line, const QString& loc, int &discontinuity_sequence)
524  {
525  /*
526  * The EXT-X-DISCONTINUITY-SEQUENCE tag allows synchronization between
527  * different Renditions of the same Variant Stream or different Variant
528  * Streams that have EXT-X-DISCONTINUITY tags in their Media Playlists.
529  *
530  * Its format is:
531  *
532  * #EXT-X-DISCONTINUITY-SEQUENCE:<number>
533  *
534  * where number is a decimal-integer
535  */
536  if (!ParseDecimalValue(line, discontinuity_sequence))
537  {
538  LOG(VB_RECORD, LOG_ERR, loc + "expected #EXT-X-DISCONTINUITY-SEQUENCE:<s>");
539  return false;
540  }
541 
542  LOG(VB_RECORD, LOG_DEBUG, loc + QString("#EXT-X-DISCONTINUITY-SEQUENCE %1")
543  .arg(line));
544  return true;
545  }
546 
547  bool ParseDiscontinuity(const QString& line, const QString& loc)
548  {
549  /* Not handled, never seen so far */
550  LOG(VB_RECORD, LOG_DEBUG, loc + QString("#EXT-X-DISCONTINUITY %1")
551  .arg(line));
552  return true;
553  }
554 
555  bool ParseEndList(const QString& loc, bool& is_vod)
556  {
557  /*
558  * The EXT-X-ENDLIST tag indicates that no more media files will be
559  * added to the Playlist file. It MAY occur anywhere in the Playlist
560  * file; it MUST NOT occur more than once. Its format is:
561  */
562  is_vod = true;
563  LOG(VB_RECORD, LOG_INFO, loc + " video on demand (vod) mode");
564  return true;
565  }
566 
567  bool ParseIndependentSegments(const QString& line, const QString& loc)
568  {
569  /* #EXT-X-INDEPENDENT-SEGMENTS
570  *
571  * The EXT-X-INDEPENDENT-SEGMENTS tag indicates that all media samples
572  * in a Media Segment can be decoded without information from other
573  * segments. It applies to every Media Segment in the Playlist.
574  *
575  * Its format is:
576  *
577  * #EXT-X-INDEPENDENT-SEGMENTS
578  */
579 
580  // Not handled yet
581  LOG(VB_RECORD, LOG_DEBUG, loc + QString("#EXT-X-INDEPENDENT-SEGMENTS %1")
582  .arg(line));
583  return true;
584  }
585 
586 } // namespace M3U
M3U::ParseStreamInformation
bool ParseStreamInformation(const QString &line, const QString &url, const QString &loc, int &id, uint64_t &bandwidth, QString &audio, QString &video)
Definition: m3u.cpp:125
M3U::RelativeURI
QString RelativeURI(const QString &surl, const QString &spath)
Definition: m3u.cpp:20
M3U::ParseDecimalValue
bool ParseDecimalValue(const QString &line, int &target)
Return the decimal argument in a line of type: blah:<decimal> presence of value <decimal> is compulso...
Definition: m3u.cpp:56
M3U
Definition: m3u.cpp:9
LOG
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
M3U::ParseKey
bool ParseKey(int version, const QString &line, [[maybe_unused]] bool &aesmsg, const QString &loc, QString &path, QString &iv)
Definition: m3u.cpp:348
M3U::ParseMediaSequence
bool ParseMediaSequence(int64_t &sequence_num, const QString &line, const QString &loc)
Definition: m3u.cpp:327
mythdate.h
mythlogging.h
hardwareprofile.config.p
p
Definition: config.py:33
M3U::ParseAllowCache
bool ParseAllowCache(const QString &line, const QString &loc, bool &do_cache)
Definition: m3u.cpp:490
M3U::DecodedURI
QString DecodedURI(const QString &uri)
Definition: m3u.cpp:13
M3U::ParseDiscontinuity
bool ParseDiscontinuity(const QString &line, const QString &loc)
Definition: m3u.cpp:547
M3U::ParseMedia
bool ParseMedia(const QString &line, const QString &loc, QString &media_type, QString &group_id, QString &uri, QString &name)
Definition: m3u.cpp:209
MythDate::fromString
QDateTime fromString(const QString &dtstr)
Converts kFilename && kISODate formats to QDateTime.
Definition: mythdate.cpp:39
M3U::ParseMap
bool ParseMap(const QString &line, const QString &loc, QString &uri)
Definition: m3u.cpp:439
M3U::kQuotes
static const QRegularExpression kQuotes
Definition: m3u.cpp:11
M3U::ParseAttributes
QString ParseAttributes(const QString &line, const char *attr)
Definition: m3u.cpp:31
M3U::ParseDiscontinuitySequence
bool ParseDiscontinuitySequence(const QString &line, const QString &loc, int &discontinuity_sequence)
Definition: m3u.cpp:523
M3U::ParseSegmentInformation
bool ParseSegmentInformation(int version, const QString &line, int &duration, QString &title, const QString &loc)
Definition: m3u.cpp:249
M3U::ParseVersion
bool ParseVersion(const QString &line, const QString &loc, int &version)
Definition: m3u.cpp:90
M3U::ParseProgramDateTime
bool ParseProgramDateTime(const QString &line, const QString &loc, QDateTime &dt)
Definition: m3u.cpp:470
d
static const iso6937table * d
Definition: iso6937tables.cpp:1025
M3U::ParseEndList
bool ParseEndList(const QString &loc, bool &is_vod)
Definition: m3u.cpp:555
M3U::ParseTargetDuration
bool ParseTargetDuration(const QString &line, const QString &loc, int &duration)
Definition: m3u.cpp:232
m3u.h
nv_python_libs.bbciplayer.bbciplayer_api.version
string version
Definition: bbciplayer_api.py:77
M3U::ParseIndependentSegments
bool ParseIndependentSegments(const QString &line, const QString &loc)
Definition: m3u.cpp:567