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