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