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