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