1 /*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 //#define LOG_NDEBUG 0
18 #define LOG_TAG "M3UParser"
19 #include <utils/Log.h>
20
21 #include "M3UParser.h"
22 #include <binder/Parcel.h>
23 #include <cutils/properties.h>
24 #include <media/stagefright/foundation/ADebug.h>
25 #include <media/stagefright/foundation/AMessage.h>
26 #include <media/stagefright/foundation/ByteUtils.h>
27 #include <media/stagefright/MediaDefs.h>
28 #include <media/stagefright/MediaErrors.h>
29 #include <media/stagefright/Utils.h>
30 #include <media/mediaplayer.h>
31
32 namespace android {
33
34 struct M3UParser::MediaGroup : public RefBase {
35 enum Type {
36 TYPE_AUDIO,
37 TYPE_VIDEO,
38 TYPE_SUBS,
39 TYPE_CC,
40 };
41
42 enum FlagBits {
43 FLAG_AUTOSELECT = 1,
44 FLAG_DEFAULT = 2,
45 FLAG_FORCED = 4,
46 FLAG_HAS_LANGUAGE = 8,
47 FLAG_HAS_URI = 16,
48 };
49
50 explicit MediaGroup(Type type);
51
52 Type type() const;
53
54 status_t addMedia(
55 const char *name,
56 const char *uri,
57 const char *language,
58 uint32_t flags);
59
60 bool getActiveURI(AString *uri) const;
61
62 void pickRandomMediaItems();
63 status_t selectTrack(size_t index, bool select);
64 size_t countTracks() const;
65 sp<AMessage> getTrackInfo(size_t index) const;
66
67 protected:
68 virtual ~MediaGroup();
69
70 private:
71
72 friend struct M3UParser;
73
74 struct Media {
75 AString mName;
76 AString mURI;
77 AString mLanguage;
78 uint32_t mFlags;
79 };
80
81 Type mType;
82 Vector<Media> mMediaItems;
83
84 ssize_t mSelectedIndex;
85
86 DISALLOW_EVIL_CONSTRUCTORS(MediaGroup);
87 };
88
MediaGroup(Type type)89 M3UParser::MediaGroup::MediaGroup(Type type)
90 : mType(type),
91 mSelectedIndex(-1) {
92 }
93
~MediaGroup()94 M3UParser::MediaGroup::~MediaGroup() {
95 }
96
type() const97 M3UParser::MediaGroup::Type M3UParser::MediaGroup::type() const {
98 return mType;
99 }
100
addMedia(const char * name,const char * uri,const char * language,uint32_t flags)101 status_t M3UParser::MediaGroup::addMedia(
102 const char *name,
103 const char *uri,
104 const char *language,
105 uint32_t flags) {
106 mMediaItems.push();
107 Media &item = mMediaItems.editItemAt(mMediaItems.size() - 1);
108
109 item.mName = name;
110
111 if (uri) {
112 item.mURI = uri;
113 }
114
115 if (language) {
116 item.mLanguage = language;
117 }
118
119 item.mFlags = flags;
120
121 return OK;
122 }
123
pickRandomMediaItems()124 void M3UParser::MediaGroup::pickRandomMediaItems() {
125 #if 1
126 switch (mType) {
127 case TYPE_AUDIO:
128 {
129 char value[PROPERTY_VALUE_MAX];
130 if (property_get("media.httplive.audio-index", value, NULL)) {
131 char *end;
132 mSelectedIndex = strtoul(value, &end, 10);
133 CHECK(end > value && *end == '\0');
134
135 if (mSelectedIndex >= (ssize_t)mMediaItems.size()) {
136 mSelectedIndex = mMediaItems.size() - 1;
137 }
138 } else {
139 mSelectedIndex = 0;
140 }
141 break;
142 }
143
144 case TYPE_VIDEO:
145 {
146 mSelectedIndex = 0;
147 break;
148 }
149
150 case TYPE_SUBS:
151 {
152 mSelectedIndex = -1;
153 break;
154 }
155
156 default:
157 TRESPASS();
158 }
159 #else
160 mSelectedIndex = (rand() * mMediaItems.size()) / RAND_MAX;
161 #endif
162 }
163
selectTrack(size_t index,bool select)164 status_t M3UParser::MediaGroup::selectTrack(size_t index, bool select) {
165 if (mType != TYPE_SUBS && mType != TYPE_AUDIO) {
166 ALOGE("only select subtitile/audio tracks for now!");
167 return INVALID_OPERATION;
168 }
169
170 if (select) {
171 if (index >= mMediaItems.size()) {
172 ALOGE("track %zu does not exist", index);
173 return INVALID_OPERATION;
174 }
175 if (mSelectedIndex == (ssize_t)index) {
176 ALOGE("track %zu already selected", index);
177 return BAD_VALUE;
178 }
179 ALOGV("selected track %zu", index);
180 mSelectedIndex = index;
181 } else {
182 if (mSelectedIndex != (ssize_t)index) {
183 ALOGE("track %zu is not selected", index);
184 return BAD_VALUE;
185 }
186 ALOGV("unselected track %zu", index);
187 mSelectedIndex = -1;
188 }
189
190 return OK;
191 }
192
countTracks() const193 size_t M3UParser::MediaGroup::countTracks() const {
194 return mMediaItems.size();
195 }
196
getTrackInfo(size_t index) const197 sp<AMessage> M3UParser::MediaGroup::getTrackInfo(size_t index) const {
198 if (index >= mMediaItems.size()) {
199 return NULL;
200 }
201
202 sp<AMessage> format = new AMessage();
203
204 int32_t trackType;
205 if (mType == TYPE_AUDIO) {
206 trackType = MEDIA_TRACK_TYPE_AUDIO;
207 } else if (mType == TYPE_VIDEO) {
208 trackType = MEDIA_TRACK_TYPE_VIDEO;
209 } else if (mType == TYPE_SUBS) {
210 trackType = MEDIA_TRACK_TYPE_SUBTITLE;
211 } else {
212 trackType = MEDIA_TRACK_TYPE_UNKNOWN;
213 }
214 format->setInt32("type", trackType);
215
216 const Media &item = mMediaItems.itemAt(index);
217 const char *lang = item.mLanguage.empty() ? "und" : item.mLanguage.c_str();
218 format->setString("language", lang);
219
220 if (mType == TYPE_SUBS) {
221 // TO-DO: pass in a MediaFormat instead
222 format->setString("mime", MEDIA_MIMETYPE_TEXT_VTT);
223 format->setInt32("auto", !!(item.mFlags & MediaGroup::FLAG_AUTOSELECT));
224 format->setInt32("default", !!(item.mFlags & MediaGroup::FLAG_DEFAULT));
225 format->setInt32("forced", !!(item.mFlags & MediaGroup::FLAG_FORCED));
226 }
227
228 return format;
229 }
230
getActiveURI(AString * uri) const231 bool M3UParser::MediaGroup::getActiveURI(AString *uri) const {
232 for (size_t i = 0; i < mMediaItems.size(); ++i) {
233 if (mSelectedIndex >= 0 && i == (size_t)mSelectedIndex) {
234 const Media &item = mMediaItems.itemAt(i);
235
236 *uri = item.mURI;
237 return true;
238 }
239 }
240
241 return false;
242 }
243
244 ////////////////////////////////////////////////////////////////////////////////
245
M3UParser(const char * baseURI,const void * data,size_t size)246 M3UParser::M3UParser(
247 const char *baseURI, const void *data, size_t size)
248 : mInitCheck(NO_INIT),
249 mBaseURI(baseURI),
250 mIsExtM3U(false),
251 mIsVariantPlaylist(false),
252 mIsComplete(false),
253 mIsEvent(false),
254 mFirstSeqNumber(-1),
255 mLastSeqNumber(-1),
256 mTargetDurationUs(-1ll),
257 mDiscontinuitySeq(0),
258 mDiscontinuityCount(0),
259 mSelectedIndex(-1) {
260 mInitCheck = parse(data, size);
261 }
262
~M3UParser()263 M3UParser::~M3UParser() {
264 }
265
initCheck() const266 status_t M3UParser::initCheck() const {
267 return mInitCheck;
268 }
269
isExtM3U() const270 bool M3UParser::isExtM3U() const {
271 return mIsExtM3U;
272 }
273
isVariantPlaylist() const274 bool M3UParser::isVariantPlaylist() const {
275 return mIsVariantPlaylist;
276 }
277
isComplete() const278 bool M3UParser::isComplete() const {
279 return mIsComplete;
280 }
281
isEvent() const282 bool M3UParser::isEvent() const {
283 return mIsEvent;
284 }
285
getDiscontinuitySeq() const286 size_t M3UParser::getDiscontinuitySeq() const {
287 return mDiscontinuitySeq;
288 }
289
getTargetDuration() const290 int64_t M3UParser::getTargetDuration() const {
291 return mTargetDurationUs;
292 }
293
getFirstSeqNumber() const294 int32_t M3UParser::getFirstSeqNumber() const {
295 return mFirstSeqNumber;
296 }
297
getSeqNumberRange(int32_t * firstSeq,int32_t * lastSeq) const298 void M3UParser::getSeqNumberRange(int32_t *firstSeq, int32_t *lastSeq) const {
299 *firstSeq = mFirstSeqNumber;
300 *lastSeq = mLastSeqNumber;
301 }
302
meta()303 sp<AMessage> M3UParser::meta() {
304 return mMeta;
305 }
306
size()307 size_t M3UParser::size() {
308 return mItems.size();
309 }
310
itemAt(size_t index,AString * uri,sp<AMessage> * meta)311 bool M3UParser::itemAt(size_t index, AString *uri, sp<AMessage> *meta) {
312 if (uri) {
313 uri->clear();
314 }
315
316 if (meta) {
317 *meta = NULL;
318 }
319
320 if (index >= mItems.size()) {
321 return false;
322 }
323
324 if (uri) {
325 *uri = mItems.itemAt(index).mURI;
326 }
327
328 if (meta) {
329 *meta = mItems.itemAt(index).mMeta;
330 }
331
332 return true;
333 }
334
pickRandomMediaItems()335 void M3UParser::pickRandomMediaItems() {
336 for (size_t i = 0; i < mMediaGroups.size(); ++i) {
337 mMediaGroups.valueAt(i)->pickRandomMediaItems();
338 }
339 }
340
selectTrack(size_t index,bool select)341 status_t M3UParser::selectTrack(size_t index, bool select) {
342 for (size_t i = 0, ii = index; i < mMediaGroups.size(); ++i) {
343 sp<MediaGroup> group = mMediaGroups.valueAt(i);
344 size_t tracks = group->countTracks();
345 if (ii < tracks) {
346 status_t err = group->selectTrack(ii, select);
347 if (err == OK) {
348 mSelectedIndex = select ? index : -1;
349 }
350 return err;
351 }
352 ii -= tracks;
353 }
354 return INVALID_OPERATION;
355 }
356
getTrackCount() const357 size_t M3UParser::getTrackCount() const {
358 size_t trackCount = 0;
359 for (size_t i = 0; i < mMediaGroups.size(); ++i) {
360 trackCount += mMediaGroups.valueAt(i)->countTracks();
361 }
362 return trackCount;
363 }
364
getTrackInfo(size_t index) const365 sp<AMessage> M3UParser::getTrackInfo(size_t index) const {
366 for (size_t i = 0, ii = index; i < mMediaGroups.size(); ++i) {
367 sp<MediaGroup> group = mMediaGroups.valueAt(i);
368 size_t tracks = group->countTracks();
369 if (ii < tracks) {
370 return group->getTrackInfo(ii);
371 }
372 ii -= tracks;
373 }
374 return NULL;
375 }
376
getSelectedIndex() const377 ssize_t M3UParser::getSelectedIndex() const {
378 return mSelectedIndex;
379 }
380
getSelectedTrack(media_track_type type) const381 ssize_t M3UParser::getSelectedTrack(media_track_type type) const {
382 MediaGroup::Type groupType;
383 switch (type) {
384 case MEDIA_TRACK_TYPE_VIDEO:
385 groupType = MediaGroup::TYPE_VIDEO;
386 break;
387
388 case MEDIA_TRACK_TYPE_AUDIO:
389 groupType = MediaGroup::TYPE_AUDIO;
390 break;
391
392 case MEDIA_TRACK_TYPE_SUBTITLE:
393 groupType = MediaGroup::TYPE_SUBS;
394 break;
395
396 default:
397 return -1;
398 }
399
400 for (size_t i = 0, ii = 0; i < mMediaGroups.size(); ++i) {
401 sp<MediaGroup> group = mMediaGroups.valueAt(i);
402 size_t tracks = group->countTracks();
403 if (groupType != group->mType) {
404 ii += tracks;
405 } else if (group->mSelectedIndex >= 0) {
406 return ii + group->mSelectedIndex;
407 }
408 }
409
410 return -1;
411 }
412
getTypeURI(size_t index,const char * key,AString * uri) const413 bool M3UParser::getTypeURI(size_t index, const char *key, AString *uri) const {
414 if (!mIsVariantPlaylist) {
415 if (uri != NULL) {
416 *uri = mBaseURI;
417 }
418
419 // Assume media without any more specific attribute contains
420 // audio and video, but no subtitles.
421 return !strcmp("audio", key) || !strcmp("video", key);
422 }
423
424 CHECK_LT(index, mItems.size());
425
426 sp<AMessage> meta = mItems.itemAt(index).mMeta;
427
428 AString groupID;
429 if (!meta->findString(key, &groupID)) {
430 if (uri != NULL) {
431 *uri = mItems.itemAt(index).mURI;
432 }
433
434 AString codecs;
435 if (!meta->findString("codecs", &codecs)) {
436 // Assume media without any more specific attribute contains
437 // audio and video, but no subtitles.
438 return !strcmp("audio", key) || !strcmp("video", key);
439 } else {
440 // Split the comma separated list of codecs.
441 size_t offset = 0;
442 ssize_t commaPos = -1;
443 codecs.append(',');
444 while ((commaPos = codecs.find(",", offset)) >= 0) {
445 AString codec(codecs, offset, commaPos - offset);
446 codec.trim();
447 // return true only if a codec of type `key` ("audio"/"video")
448 // is found.
449 if (codecIsType(codec, key)) {
450 return true;
451 }
452 offset = commaPos + 1;
453 }
454 return false;
455 }
456 }
457
458 // if uri == NULL, we're only checking if the type is present,
459 // don't care about the active URI (or if there is an active one)
460 if (uri != NULL) {
461 sp<MediaGroup> group = mMediaGroups.valueFor(groupID);
462 if (!group->getActiveURI(uri)) {
463 return false;
464 }
465
466 if ((*uri).empty()) {
467 *uri = mItems.itemAt(index).mURI;
468 }
469 }
470
471 return true;
472 }
473
hasType(size_t index,const char * key) const474 bool M3UParser::hasType(size_t index, const char *key) const {
475 return getTypeURI(index, key, NULL /* uri */);
476 }
477
MakeURL(const char * baseURL,const char * url,AString * out)478 static bool MakeURL(const char *baseURL, const char *url, AString *out) {
479 out->clear();
480
481 if (strncasecmp("http://", baseURL, 7)
482 && strncasecmp("https://", baseURL, 8)
483 && strncasecmp("file://", baseURL, 7)) {
484 // Base URL must be absolute
485 return false;
486 }
487 if (!strncasecmp("data:", url, 5)) {
488 return false;
489 }
490 const size_t schemeEnd = (strstr(baseURL, "//") - baseURL) + 2;
491 CHECK(schemeEnd == 7 || schemeEnd == 8);
492
493 if (!strncasecmp("http://", url, 7) || !strncasecmp("https://", url, 8)) {
494 // "url" is already an absolute URL, ignore base URL.
495 out->setTo(url);
496
497 ALOGV("base:'%s', url:'%s' => '%s'", baseURL, url, out->c_str());
498
499 return true;
500 }
501
502 if (url[0] == '/') {
503 // URL is an absolute path.
504
505 const char *protocolEnd = strstr(baseURL, "//") + 2;
506 const char *pathStart = strchr(protocolEnd, '/');
507
508 if (pathStart != NULL) {
509 out->setTo(baseURL, pathStart - baseURL);
510 } else {
511 out->setTo(baseURL);
512 }
513
514 out->append(url);
515 } else {
516 // URL is a relative path
517
518 // Check for a possible query string
519 const char *qsPos = strchr(baseURL, '?');
520 size_t end;
521 if (qsPos != NULL) {
522 end = qsPos - baseURL;
523 } else {
524 end = strlen(baseURL);
525 }
526 // Check for the last slash before a potential query string
527 for (ssize_t pos = end - 1; pos >= 0; pos--) {
528 if (baseURL[pos] == '/') {
529 end = pos;
530 break;
531 }
532 }
533
534 // Check whether the found slash actually is part of the path
535 // and not part of the "http://".
536 if (end >= schemeEnd) {
537 out->setTo(baseURL, end);
538 } else {
539 out->setTo(baseURL);
540 }
541
542 out->append("/");
543 out->append(url);
544 }
545
546 ALOGV("base:'%s', url:'%s' => '%s'", baseURL, url, out->c_str());
547
548 return true;
549 }
550
parse(const void * _data,size_t size)551 status_t M3UParser::parse(const void *_data, size_t size) {
552 int32_t lineNo = 0;
553
554 sp<AMessage> itemMeta;
555
556 const char *data = (const char *)_data;
557 size_t offset = 0;
558 uint64_t segmentRangeOffset = 0;
559 while (offset < size) {
560 size_t offsetLF = offset;
561 while (offsetLF < size && data[offsetLF] != '\n') {
562 ++offsetLF;
563 }
564
565 AString line;
566 if (offsetLF > offset && data[offsetLF - 1] == '\r') {
567 line.setTo(&data[offset], offsetLF - offset - 1);
568 } else {
569 line.setTo(&data[offset], offsetLF - offset);
570 }
571
572 // ALOGI("#%s#", line.c_str());
573
574 if (line.empty()) {
575 offset = offsetLF + 1;
576 continue;
577 }
578
579 if (lineNo == 0 && line == "#EXTM3U") {
580 mIsExtM3U = true;
581 }
582
583 if (mIsExtM3U) {
584 status_t err = OK;
585
586 if (line.startsWith("#EXT-X-TARGETDURATION")) {
587 if (mIsVariantPlaylist) {
588 return ERROR_MALFORMED;
589 }
590 err = parseMetaData(line, &mMeta, "target-duration");
591 } else if (line.startsWith("#EXT-X-MEDIA-SEQUENCE")) {
592 if (mIsVariantPlaylist) {
593 return ERROR_MALFORMED;
594 }
595 err = parseMetaData(line, &mMeta, "media-sequence");
596 } else if (line.startsWith("#EXT-X-KEY")) {
597 if (mIsVariantPlaylist) {
598 return ERROR_MALFORMED;
599 }
600 err = parseCipherInfo(line, &itemMeta, mBaseURI);
601 } else if (line.startsWith("#EXT-X-ENDLIST")) {
602 mIsComplete = true;
603 } else if (line.startsWith("#EXT-X-PLAYLIST-TYPE:EVENT")) {
604 mIsEvent = true;
605 } else if (line.startsWith("#EXTINF")) {
606 if (mIsVariantPlaylist) {
607 return ERROR_MALFORMED;
608 }
609 err = parseMetaDataDuration(line, &itemMeta, "durationUs");
610 } else if (line.startsWith("#EXT-X-DISCONTINUITY-SEQUENCE")) {
611 if (mIsVariantPlaylist) {
612 return ERROR_MALFORMED;
613 }
614 size_t seq;
615 err = parseDiscontinuitySequence(line, &seq);
616 if (err == OK) {
617 mDiscontinuitySeq = seq;
618 ALOGI("mDiscontinuitySeq %zu", mDiscontinuitySeq);
619 } else {
620 ALOGI("Failed to parseDiscontinuitySequence %d", err);
621 }
622 } else if (line.startsWith("#EXT-X-DISCONTINUITY")) {
623 if (mIsVariantPlaylist) {
624 return ERROR_MALFORMED;
625 }
626 if (itemMeta == NULL) {
627 itemMeta = new AMessage;
628 }
629 itemMeta->setInt32("discontinuity", true);
630 ++mDiscontinuityCount;
631 } else if (line.startsWith("#EXT-X-STREAM-INF")) {
632 if (mMeta != NULL) {
633 return ERROR_MALFORMED;
634 }
635 mIsVariantPlaylist = true;
636 err = parseStreamInf(line, &itemMeta);
637 } else if (line.startsWith("#EXT-X-BYTERANGE")) {
638 if (mIsVariantPlaylist) {
639 return ERROR_MALFORMED;
640 }
641
642 uint64_t length, offset;
643 err = parseByteRange(line, segmentRangeOffset, &length, &offset);
644
645 if (err == OK) {
646 if (itemMeta == NULL) {
647 itemMeta = new AMessage;
648 }
649
650 itemMeta->setInt64("range-offset", offset);
651 itemMeta->setInt64("range-length", length);
652
653 segmentRangeOffset = offset + length;
654 }
655 } else if (line.startsWith("#EXT-X-MEDIA")) {
656 err = parseMedia(line);
657 }
658
659 if (err != OK) {
660 return err;
661 }
662 }
663
664 if (!line.startsWith("#")) {
665 if (itemMeta == NULL) {
666 ALOGV("itemMeta == NULL");
667 return ERROR_MALFORMED;
668 }
669 if (!mIsVariantPlaylist) {
670 int64_t durationUs;
671 if (!itemMeta->findInt64("durationUs", &durationUs)) {
672 return ERROR_MALFORMED;
673 }
674 itemMeta->setInt32("discontinuity-sequence",
675 mDiscontinuitySeq + mDiscontinuityCount);
676 }
677
678 mItems.push();
679 Item *item = &mItems.editItemAt(mItems.size() - 1);
680
681 CHECK(MakeURL(mBaseURI.c_str(), line.c_str(), &item->mURI));
682
683 item->mMeta = itemMeta;
684
685 itemMeta.clear();
686 }
687
688 offset = offsetLF + 1;
689 ++lineNo;
690 }
691
692 // error checking of all fields that's required to appear once
693 // (currently only checking "target-duration"), and
694 // initialization of playlist properties (eg. mTargetDurationUs)
695 if (!mIsVariantPlaylist) {
696 int32_t targetDurationSecs;
697 if (mMeta == NULL || !mMeta->findInt32(
698 "target-duration", &targetDurationSecs)) {
699 ALOGE("Media playlist missing #EXT-X-TARGETDURATION");
700 return ERROR_MALFORMED;
701 }
702 mTargetDurationUs = targetDurationSecs * 1000000ll;
703
704 mFirstSeqNumber = 0;
705 if (mMeta != NULL) {
706 mMeta->findInt32("media-sequence", &mFirstSeqNumber);
707 }
708 mLastSeqNumber = mFirstSeqNumber + mItems.size() - 1;
709 }
710
711 for (size_t i = 0; i < mItems.size(); ++i) {
712 sp<AMessage> meta = mItems.itemAt(i).mMeta;
713 const char *keys[] = {"audio", "video", "subtitles"};
714 for (size_t j = 0; j < sizeof(keys) / sizeof(const char *); ++j) {
715 AString groupID;
716 if (meta->findString(keys[j], &groupID)) {
717 ssize_t groupIndex = mMediaGroups.indexOfKey(groupID);
718 if (groupIndex < 0) {
719 ALOGE("Undefined media group '%s' referenced in stream info.",
720 groupID.c_str());
721 return ERROR_MALFORMED;
722 }
723 }
724 }
725 }
726
727 return OK;
728 }
729
730 // static
parseMetaData(const AString & line,sp<AMessage> * meta,const char * key)731 status_t M3UParser::parseMetaData(
732 const AString &line, sp<AMessage> *meta, const char *key) {
733 ssize_t colonPos = line.find(":");
734
735 if (colonPos < 0) {
736 return ERROR_MALFORMED;
737 }
738
739 int32_t x;
740 status_t err = ParseInt32(line.c_str() + colonPos + 1, &x);
741
742 if (err != OK) {
743 return err;
744 }
745
746 if (meta->get() == NULL) {
747 *meta = new AMessage;
748 }
749 (*meta)->setInt32(key, x);
750
751 return OK;
752 }
753
754 // static
parseMetaDataDuration(const AString & line,sp<AMessage> * meta,const char * key)755 status_t M3UParser::parseMetaDataDuration(
756 const AString &line, sp<AMessage> *meta, const char *key) {
757 ssize_t colonPos = line.find(":");
758
759 if (colonPos < 0) {
760 return ERROR_MALFORMED;
761 }
762
763 double x;
764 status_t err = ParseDouble(line.c_str() + colonPos + 1, &x);
765
766 if (err != OK) {
767 return err;
768 }
769
770 if (meta->get() == NULL) {
771 *meta = new AMessage;
772 }
773 (*meta)->setInt64(key, (int64_t)(x * 1E6));
774
775 return OK;
776 }
777
778 // Find the next occurence of the character "what" at or after "offset",
779 // but ignore occurences between quotation marks.
780 // Return the index of the occurrence or -1 if not found.
FindNextUnquoted(const AString & line,char what,size_t offset)781 static ssize_t FindNextUnquoted(
782 const AString &line, char what, size_t offset) {
783 CHECK_NE((int)what, (int)'"');
784
785 bool quoted = false;
786 while (offset < line.size()) {
787 char c = line.c_str()[offset];
788
789 if (c == '"') {
790 quoted = !quoted;
791 } else if (c == what && !quoted) {
792 return offset;
793 }
794
795 ++offset;
796 }
797
798 return -1;
799 }
800
parseStreamInf(const AString & line,sp<AMessage> * meta) const801 status_t M3UParser::parseStreamInf(
802 const AString &line, sp<AMessage> *meta) const {
803 ssize_t colonPos = line.find(":");
804
805 if (colonPos < 0) {
806 return ERROR_MALFORMED;
807 }
808
809 size_t offset = colonPos + 1;
810
811 while (offset < line.size()) {
812 ssize_t end = FindNextUnquoted(line, ',', offset);
813 if (end < 0) {
814 end = line.size();
815 }
816
817 AString attr(line, offset, end - offset);
818 attr.trim();
819
820 offset = end + 1;
821
822 ssize_t equalPos = attr.find("=");
823 if (equalPos < 0) {
824 continue;
825 }
826
827 AString key(attr, 0, equalPos);
828 key.trim();
829
830 AString val(attr, equalPos + 1, attr.size() - equalPos - 1);
831 val.trim();
832
833 ALOGV("key=%s value=%s", key.c_str(), val.c_str());
834
835 if (!strcasecmp("bandwidth", key.c_str())) {
836 const char *s = val.c_str();
837 char *end;
838 unsigned long x = strtoul(s, &end, 10);
839
840 if (end == s || *end != '\0') {
841 // malformed
842 continue;
843 }
844
845 if (meta->get() == NULL) {
846 *meta = new AMessage;
847 }
848 (*meta)->setInt32("bandwidth", x);
849 } else if (!strcasecmp("codecs", key.c_str())) {
850 if (!isQuotedString(val)) {
851 ALOGE("Expected quoted string for %s attribute, "
852 "got '%s' instead.",
853 key.c_str(), val.c_str());;
854
855 return ERROR_MALFORMED;
856 }
857
858 key.tolower();
859 const AString &codecs = unquoteString(val);
860 if (meta->get() == NULL) {
861 *meta = new AMessage;
862 }
863 (*meta)->setString(key.c_str(), codecs.c_str());
864 } else if (!strcasecmp("resolution", key.c_str())) {
865 const char *s = val.c_str();
866 char *end;
867 unsigned long width = strtoul(s, &end, 10);
868
869 if (end == s || *end != 'x') {
870 // malformed
871 continue;
872 }
873
874 s = end + 1;
875 unsigned long height = strtoul(s, &end, 10);
876
877 if (end == s || *end != '\0') {
878 // malformed
879 continue;
880 }
881
882 if (meta->get() == NULL) {
883 *meta = new AMessage;
884 }
885 (*meta)->setInt32("width", width);
886 (*meta)->setInt32("height", height);
887 } else if (!strcasecmp("audio", key.c_str())
888 || !strcasecmp("video", key.c_str())
889 || !strcasecmp("subtitles", key.c_str())) {
890 if (!isQuotedString(val)) {
891 ALOGE("Expected quoted string for %s attribute, "
892 "got '%s' instead.",
893 key.c_str(), val.c_str());
894
895 return ERROR_MALFORMED;
896 }
897
898 const AString &groupID = unquoteString(val);
899 key.tolower();
900 if (meta->get() == NULL) {
901 *meta = new AMessage;
902 }
903 (*meta)->setString(key.c_str(), groupID.c_str());
904 }
905 }
906
907 if (meta->get() == NULL) {
908 return ERROR_MALFORMED;
909 }
910 return OK;
911 }
912
913 // static
parseCipherInfo(const AString & line,sp<AMessage> * meta,const AString & baseURI)914 status_t M3UParser::parseCipherInfo(
915 const AString &line, sp<AMessage> *meta, const AString &baseURI) {
916 ssize_t colonPos = line.find(":");
917
918 if (colonPos < 0) {
919 return ERROR_MALFORMED;
920 }
921
922 size_t offset = colonPos + 1;
923
924 while (offset < line.size()) {
925 ssize_t end = FindNextUnquoted(line, ',', offset);
926 if (end < 0) {
927 end = line.size();
928 }
929
930 AString attr(line, offset, end - offset);
931 attr.trim();
932
933 offset = end + 1;
934
935 ssize_t equalPos = attr.find("=");
936 if (equalPos < 0) {
937 continue;
938 }
939
940 AString key(attr, 0, equalPos);
941 key.trim();
942
943 AString val(attr, equalPos + 1, attr.size() - equalPos - 1);
944 val.trim();
945
946 ALOGV("key=%s value=%s", key.c_str(), val.c_str());
947
948 key.tolower();
949
950 if (key == "method" || key == "uri" || key == "iv") {
951 if (meta->get() == NULL) {
952 *meta = new AMessage;
953 }
954
955 if (key == "uri") {
956 if (val.size() >= 2
957 && val.c_str()[0] == '"'
958 && val.c_str()[val.size() - 1] == '"') {
959 // Remove surrounding quotes.
960 AString tmp(val, 1, val.size() - 2);
961 val = tmp;
962 }
963
964 AString absURI;
965 if (MakeURL(baseURI.c_str(), val.c_str(), &absURI)) {
966 val = absURI;
967 } else {
968 ALOGE("failed to make absolute url for %s.",
969 uriDebugString(baseURI).c_str());
970 }
971 }
972
973 key.insert(AString("cipher-"), 0);
974
975 (*meta)->setString(key.c_str(), val.c_str(), val.size());
976 }
977 }
978
979 return OK;
980 }
981
982 // static
parseByteRange(const AString & line,uint64_t curOffset,uint64_t * length,uint64_t * offset)983 status_t M3UParser::parseByteRange(
984 const AString &line, uint64_t curOffset,
985 uint64_t *length, uint64_t *offset) {
986 ssize_t colonPos = line.find(":");
987
988 if (colonPos < 0) {
989 return ERROR_MALFORMED;
990 }
991
992 ssize_t atPos = line.find("@", colonPos + 1);
993
994 AString lenStr;
995 if (atPos < 0) {
996 lenStr = AString(line, colonPos + 1, line.size() - colonPos - 1);
997 } else {
998 lenStr = AString(line, colonPos + 1, atPos - colonPos - 1);
999 }
1000
1001 lenStr.trim();
1002
1003 const char *s = lenStr.c_str();
1004 char *end;
1005 *length = strtoull(s, &end, 10);
1006
1007 if (s == end || *end != '\0') {
1008 return ERROR_MALFORMED;
1009 }
1010
1011 if (atPos >= 0) {
1012 AString offStr = AString(line, atPos + 1, line.size() - atPos - 1);
1013 offStr.trim();
1014
1015 const char *s = offStr.c_str();
1016 *offset = strtoull(s, &end, 10);
1017
1018 if (s == end || *end != '\0') {
1019 return ERROR_MALFORMED;
1020 }
1021 } else {
1022 *offset = curOffset;
1023 }
1024
1025 return OK;
1026 }
1027
parseMedia(const AString & line)1028 status_t M3UParser::parseMedia(const AString &line) {
1029 ssize_t colonPos = line.find(":");
1030
1031 if (colonPos < 0) {
1032 return ERROR_MALFORMED;
1033 }
1034
1035 bool haveGroupType = false;
1036 MediaGroup::Type groupType = MediaGroup::TYPE_AUDIO;
1037
1038 bool haveGroupID = false;
1039 AString groupID;
1040
1041 bool haveGroupLanguage = false;
1042 AString groupLanguage;
1043
1044 bool haveGroupName = false;
1045 AString groupName;
1046
1047 bool haveGroupAutoselect = false;
1048 bool groupAutoselect = false;
1049
1050 bool haveGroupDefault = false;
1051 bool groupDefault = false;
1052
1053 bool haveGroupForced = false;
1054 bool groupForced = false;
1055
1056 bool haveGroupURI = false;
1057 AString groupURI;
1058
1059 size_t offset = colonPos + 1;
1060
1061 while (offset < line.size()) {
1062 ssize_t end = FindNextUnquoted(line, ',', offset);
1063 if (end < 0) {
1064 end = line.size();
1065 }
1066
1067 AString attr(line, offset, end - offset);
1068 attr.trim();
1069
1070 offset = end + 1;
1071
1072 ssize_t equalPos = attr.find("=");
1073 if (equalPos < 0) {
1074 continue;
1075 }
1076
1077 AString key(attr, 0, equalPos);
1078 key.trim();
1079
1080 AString val(attr, equalPos + 1, attr.size() - equalPos - 1);
1081 val.trim();
1082
1083 ALOGV("key=%s value=%s", key.c_str(), val.c_str());
1084
1085 if (!strcasecmp("type", key.c_str())) {
1086 if (!strcasecmp("subtitles", val.c_str())) {
1087 groupType = MediaGroup::TYPE_SUBS;
1088 } else if (!strcasecmp("audio", val.c_str())) {
1089 groupType = MediaGroup::TYPE_AUDIO;
1090 } else if (!strcasecmp("video", val.c_str())) {
1091 groupType = MediaGroup::TYPE_VIDEO;
1092 } else if (!strcasecmp("closed-captions", val.c_str())){
1093 groupType = MediaGroup::TYPE_CC;
1094 } else {
1095 ALOGE("Invalid media group type '%s'", val.c_str());
1096 return ERROR_MALFORMED;
1097 }
1098
1099 haveGroupType = true;
1100 } else if (!strcasecmp("group-id", key.c_str())) {
1101 if (val.size() < 2
1102 || val.c_str()[0] != '"'
1103 || val.c_str()[val.size() - 1] != '"') {
1104 ALOGE("Expected quoted string for GROUP-ID, got '%s' instead.",
1105 val.c_str());
1106
1107 return ERROR_MALFORMED;
1108 }
1109
1110 groupID.setTo(val, 1, val.size() - 2);
1111 haveGroupID = true;
1112 } else if (!strcasecmp("language", key.c_str())) {
1113 if (val.size() < 2
1114 || val.c_str()[0] != '"'
1115 || val.c_str()[val.size() - 1] != '"') {
1116 ALOGE("Expected quoted string for LANGUAGE, got '%s' instead.",
1117 val.c_str());
1118
1119 return ERROR_MALFORMED;
1120 }
1121
1122 groupLanguage.setTo(val, 1, val.size() - 2);
1123 haveGroupLanguage = true;
1124 } else if (!strcasecmp("name", key.c_str())) {
1125 if (val.size() < 2
1126 || val.c_str()[0] != '"'
1127 || val.c_str()[val.size() - 1] != '"') {
1128 ALOGE("Expected quoted string for NAME, got '%s' instead.",
1129 val.c_str());
1130
1131 return ERROR_MALFORMED;
1132 }
1133
1134 groupName.setTo(val, 1, val.size() - 2);
1135 haveGroupName = true;
1136 } else if (!strcasecmp("autoselect", key.c_str())) {
1137 groupAutoselect = false;
1138 if (!strcasecmp("YES", val.c_str())) {
1139 groupAutoselect = true;
1140 } else if (!strcasecmp("NO", val.c_str())) {
1141 groupAutoselect = false;
1142 } else {
1143 ALOGE("Expected YES or NO for AUTOSELECT attribute, "
1144 "got '%s' instead.",
1145 val.c_str());
1146
1147 return ERROR_MALFORMED;
1148 }
1149
1150 haveGroupAutoselect = true;
1151 } else if (!strcasecmp("default", key.c_str())) {
1152 groupDefault = false;
1153 if (!strcasecmp("YES", val.c_str())) {
1154 groupDefault = true;
1155 } else if (!strcasecmp("NO", val.c_str())) {
1156 groupDefault = false;
1157 } else {
1158 ALOGE("Expected YES or NO for DEFAULT attribute, "
1159 "got '%s' instead.",
1160 val.c_str());
1161
1162 return ERROR_MALFORMED;
1163 }
1164
1165 haveGroupDefault = true;
1166 } else if (!strcasecmp("forced", key.c_str())) {
1167 groupForced = false;
1168 if (!strcasecmp("YES", val.c_str())) {
1169 groupForced = true;
1170 } else if (!strcasecmp("NO", val.c_str())) {
1171 groupForced = false;
1172 } else {
1173 ALOGE("Expected YES or NO for FORCED attribute, "
1174 "got '%s' instead.",
1175 val.c_str());
1176
1177 return ERROR_MALFORMED;
1178 }
1179
1180 haveGroupForced = true;
1181 } else if (!strcasecmp("uri", key.c_str())) {
1182 if (val.size() < 2
1183 || val.c_str()[0] != '"'
1184 || val.c_str()[val.size() - 1] != '"') {
1185 ALOGE("Expected quoted string for URI, got '%s' instead.",
1186 val.c_str());
1187
1188 return ERROR_MALFORMED;
1189 }
1190
1191 AString tmp(val, 1, val.size() - 2);
1192
1193 if (!MakeURL(mBaseURI.c_str(), tmp.c_str(), &groupURI)) {
1194 ALOGI("Failed to make absolute URI from '%s'.", tmp.c_str());
1195 }
1196
1197 haveGroupURI = true;
1198 }
1199 }
1200
1201 if (!haveGroupType || !haveGroupID || !haveGroupName) {
1202 ALOGE("Incomplete EXT-X-MEDIA element.");
1203 return ERROR_MALFORMED;
1204 }
1205
1206 if (groupType == MediaGroup::TYPE_CC) {
1207 // TODO: ignore this for now.
1208 // CC track will be detected by CCDecoder. But we still need to
1209 // pass the CC track flags (lang, auto) to the app in the future.
1210 return OK;
1211 }
1212
1213 uint32_t flags = 0;
1214 if (haveGroupAutoselect && groupAutoselect) {
1215 flags |= MediaGroup::FLAG_AUTOSELECT;
1216 }
1217 if (haveGroupDefault && groupDefault) {
1218 flags |= MediaGroup::FLAG_DEFAULT;
1219 }
1220 if (haveGroupForced) {
1221 if (groupType != MediaGroup::TYPE_SUBS) {
1222 ALOGE("The FORCED attribute MUST not be present on anything "
1223 "but SUBS media.");
1224
1225 return ERROR_MALFORMED;
1226 }
1227
1228 if (groupForced) {
1229 flags |= MediaGroup::FLAG_FORCED;
1230 }
1231 }
1232 if (haveGroupLanguage) {
1233 flags |= MediaGroup::FLAG_HAS_LANGUAGE;
1234 }
1235 if (haveGroupURI) {
1236 flags |= MediaGroup::FLAG_HAS_URI;
1237 }
1238
1239 ssize_t groupIndex = mMediaGroups.indexOfKey(groupID);
1240 sp<MediaGroup> group;
1241
1242 if (groupIndex < 0) {
1243 group = new MediaGroup(groupType);
1244 mMediaGroups.add(groupID, group);
1245 } else {
1246 group = mMediaGroups.valueAt(groupIndex);
1247
1248 if (group->type() != groupType) {
1249 ALOGE("Attempt to put media item under group of different type "
1250 "(groupType = %d, item type = %d",
1251 group->type(),
1252 groupType);
1253
1254 return ERROR_MALFORMED;
1255 }
1256 }
1257
1258 return group->addMedia(
1259 groupName.c_str(),
1260 haveGroupURI ? groupURI.c_str() : NULL,
1261 haveGroupLanguage ? groupLanguage.c_str() : NULL,
1262 flags);
1263 }
1264
1265 // static
parseDiscontinuitySequence(const AString & line,size_t * seq)1266 status_t M3UParser::parseDiscontinuitySequence(const AString &line, size_t *seq) {
1267 ssize_t colonPos = line.find(":");
1268
1269 if (colonPos < 0) {
1270 return ERROR_MALFORMED;
1271 }
1272
1273 int32_t x;
1274 status_t err = ParseInt32(line.c_str() + colonPos + 1, &x);
1275 if (err != OK) {
1276 return err;
1277 }
1278
1279 if (x < 0) {
1280 return ERROR_MALFORMED;
1281 }
1282
1283 if (seq) {
1284 *seq = x;
1285 }
1286 return OK;
1287 }
1288
1289 // static
ParseInt32(const char * s,int32_t * x)1290 status_t M3UParser::ParseInt32(const char *s, int32_t *x) {
1291 char *end;
1292 long lval = strtol(s, &end, 10);
1293
1294 if (end == s || (*end != '\0' && *end != ',')) {
1295 return ERROR_MALFORMED;
1296 }
1297
1298 *x = (int32_t)lval;
1299
1300 return OK;
1301 }
1302
1303 // static
ParseDouble(const char * s,double * x)1304 status_t M3UParser::ParseDouble(const char *s, double *x) {
1305 char *end;
1306 double dval = strtod(s, &end);
1307
1308 if (end == s || (*end != '\0' && *end != ',')) {
1309 return ERROR_MALFORMED;
1310 }
1311
1312 *x = dval;
1313
1314 return OK;
1315 }
1316
1317 // static
isQuotedString(const AString & str)1318 bool M3UParser::isQuotedString(const AString &str) {
1319 if (str.size() < 2
1320 || str.c_str()[0] != '"'
1321 || str.c_str()[str.size() - 1] != '"') {
1322 return false;
1323 }
1324 return true;
1325 }
1326
1327 // static
unquoteString(const AString & str)1328 AString M3UParser::unquoteString(const AString &str) {
1329 if (!isQuotedString(str)) {
1330 return str;
1331 }
1332 return AString(str, 1, str.size() - 2);
1333 }
1334
1335 // static
codecIsType(const AString & codec,const char * type)1336 bool M3UParser::codecIsType(const AString &codec, const char *type) {
1337 if (codec.size() < 4) {
1338 return false;
1339 }
1340 const char *c = codec.c_str();
1341 switch (FOURCC(c[0], c[1], c[2], c[3])) {
1342 // List extracted from http://www.mp4ra.org/codecs.html
1343 case 'ac-3':
1344 case 'alac':
1345 case 'dra1':
1346 case 'dtsc':
1347 case 'dtse':
1348 case 'dtsh':
1349 case 'dtsl':
1350 case 'ec-3':
1351 case 'enca':
1352 case 'g719':
1353 case 'g726':
1354 case 'm4ae':
1355 case 'mlpa':
1356 case 'mp4a':
1357 case 'raw ':
1358 case 'samr':
1359 case 'sawb':
1360 case 'sawp':
1361 case 'sevc':
1362 case 'sqcp':
1363 case 'ssmv':
1364 case 'twos':
1365 case 'agsm':
1366 case 'alaw':
1367 case 'dvi ':
1368 case 'fl32':
1369 case 'fl64':
1370 case 'ima4':
1371 case 'in24':
1372 case 'in32':
1373 case 'lpcm':
1374 case 'Qclp':
1375 case 'QDM2':
1376 case 'QDMC':
1377 case 'ulaw':
1378 case 'vdva':
1379 return !strcmp("audio", type);
1380
1381 case 'avc1':
1382 case 'avc2':
1383 case 'avcp':
1384 case 'drac':
1385 case 'encv':
1386 case 'mjp2':
1387 case 'mp4v':
1388 case 'mvc1':
1389 case 'mvc2':
1390 case 'resv':
1391 case 's263':
1392 case 'svc1':
1393 case 'vc-1':
1394 case 'CFHD':
1395 case 'civd':
1396 case 'DV10':
1397 case 'dvh5':
1398 case 'dvh6':
1399 case 'dvhp':
1400 case 'DVOO':
1401 case 'DVOR':
1402 case 'DVTV':
1403 case 'DVVT':
1404 case 'flic':
1405 case 'gif ':
1406 case 'h261':
1407 case 'h263':
1408 case 'HD10':
1409 case 'jpeg':
1410 case 'M105':
1411 case 'mjpa':
1412 case 'mjpb':
1413 case 'png ':
1414 case 'PNTG':
1415 case 'rle ':
1416 case 'rpza':
1417 case 'Shr0':
1418 case 'Shr1':
1419 case 'Shr2':
1420 case 'Shr3':
1421 case 'Shr4':
1422 case 'SVQ1':
1423 case 'SVQ3':
1424 case 'tga ':
1425 case 'tiff':
1426 case 'WRLE':
1427 return !strcmp("video", type);
1428
1429 default:
1430 return false;
1431 }
1432 }
1433
1434 } // namespace android
1435