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 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 char *protocolEnd = strstr(baseURL, "//") + 2;
502 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")) {
607 if (mIsVariantPlaylist) {
608 return ERROR_MALFORMED;
609 }
610 if (itemMeta == NULL) {
611 itemMeta = new AMessage;
612 }
613 itemMeta->setInt32("discontinuity", true);
614 ++mDiscontinuityCount;
615 } else if (line.startsWith("#EXT-X-STREAM-INF")) {
616 if (mMeta != NULL) {
617 return ERROR_MALFORMED;
618 }
619 mIsVariantPlaylist = true;
620 err = parseStreamInf(line, &itemMeta);
621 } else if (line.startsWith("#EXT-X-BYTERANGE")) {
622 if (mIsVariantPlaylist) {
623 return ERROR_MALFORMED;
624 }
625
626 uint64_t length, offset;
627 err = parseByteRange(line, segmentRangeOffset, &length, &offset);
628
629 if (err == OK) {
630 if (itemMeta == NULL) {
631 itemMeta = new AMessage;
632 }
633
634 itemMeta->setInt64("range-offset", offset);
635 itemMeta->setInt64("range-length", length);
636
637 segmentRangeOffset = offset + length;
638 }
639 } else if (line.startsWith("#EXT-X-MEDIA")) {
640 err = parseMedia(line);
641 } else if (line.startsWith("#EXT-X-DISCONTINUITY-SEQUENCE")) {
642 if (mIsVariantPlaylist) {
643 return ERROR_MALFORMED;
644 }
645 size_t seq;
646 err = parseDiscontinuitySequence(line, &seq);
647 if (err == OK) {
648 mDiscontinuitySeq = seq;
649 }
650 }
651
652 if (err != OK) {
653 return err;
654 }
655 }
656
657 if (!line.startsWith("#")) {
658 if (!mIsVariantPlaylist) {
659 int64_t durationUs;
660 if (itemMeta == NULL
661 || !itemMeta->findInt64("durationUs", &durationUs)) {
662 return ERROR_MALFORMED;
663 }
664 itemMeta->setInt32("discontinuity-sequence",
665 mDiscontinuitySeq + mDiscontinuityCount);
666 }
667
668 mItems.push();
669 Item *item = &mItems.editItemAt(mItems.size() - 1);
670
671 CHECK(MakeURL(mBaseURI.c_str(), line.c_str(), &item->mURI));
672
673 item->mMeta = itemMeta;
674
675 itemMeta.clear();
676 }
677
678 offset = offsetLF + 1;
679 ++lineNo;
680 }
681
682 // error checking of all fields that's required to appear once
683 // (currently only checking "target-duration"), and
684 // initialization of playlist properties (eg. mTargetDurationUs)
685 if (!mIsVariantPlaylist) {
686 int32_t targetDurationSecs;
687 if (mMeta == NULL || !mMeta->findInt32(
688 "target-duration", &targetDurationSecs)) {
689 ALOGE("Media playlist missing #EXT-X-TARGETDURATION");
690 return ERROR_MALFORMED;
691 }
692 mTargetDurationUs = targetDurationSecs * 1000000ll;
693
694 mFirstSeqNumber = 0;
695 if (mMeta != NULL) {
696 mMeta->findInt32("media-sequence", &mFirstSeqNumber);
697 }
698 mLastSeqNumber = mFirstSeqNumber + mItems.size() - 1;
699 }
700
701 return OK;
702 }
703
704 // static
parseMetaData(const AString & line,sp<AMessage> * meta,const char * key)705 status_t M3UParser::parseMetaData(
706 const AString &line, sp<AMessage> *meta, const char *key) {
707 ssize_t colonPos = line.find(":");
708
709 if (colonPos < 0) {
710 return ERROR_MALFORMED;
711 }
712
713 int32_t x;
714 status_t err = ParseInt32(line.c_str() + colonPos + 1, &x);
715
716 if (err != OK) {
717 return err;
718 }
719
720 if (meta->get() == NULL) {
721 *meta = new AMessage;
722 }
723 (*meta)->setInt32(key, x);
724
725 return OK;
726 }
727
728 // static
parseMetaDataDuration(const AString & line,sp<AMessage> * meta,const char * key)729 status_t M3UParser::parseMetaDataDuration(
730 const AString &line, sp<AMessage> *meta, const char *key) {
731 ssize_t colonPos = line.find(":");
732
733 if (colonPos < 0) {
734 return ERROR_MALFORMED;
735 }
736
737 double x;
738 status_t err = ParseDouble(line.c_str() + colonPos + 1, &x);
739
740 if (err != OK) {
741 return err;
742 }
743
744 if (meta->get() == NULL) {
745 *meta = new AMessage;
746 }
747 (*meta)->setInt64(key, (int64_t)(x * 1E6));
748
749 return OK;
750 }
751
752 // Find the next occurence of the character "what" at or after "offset",
753 // but ignore occurences between quotation marks.
754 // Return the index of the occurrence or -1 if not found.
FindNextUnquoted(const AString & line,char what,size_t offset)755 static ssize_t FindNextUnquoted(
756 const AString &line, char what, size_t offset) {
757 CHECK_NE((int)what, (int)'"');
758
759 bool quoted = false;
760 while (offset < line.size()) {
761 char c = line.c_str()[offset];
762
763 if (c == '"') {
764 quoted = !quoted;
765 } else if (c == what && !quoted) {
766 return offset;
767 }
768
769 ++offset;
770 }
771
772 return -1;
773 }
774
parseStreamInf(const AString & line,sp<AMessage> * meta) const775 status_t M3UParser::parseStreamInf(
776 const AString &line, sp<AMessage> *meta) const {
777 ssize_t colonPos = line.find(":");
778
779 if (colonPos < 0) {
780 return ERROR_MALFORMED;
781 }
782
783 size_t offset = colonPos + 1;
784
785 while (offset < line.size()) {
786 ssize_t end = FindNextUnquoted(line, ',', offset);
787 if (end < 0) {
788 end = line.size();
789 }
790
791 AString attr(line, offset, end - offset);
792 attr.trim();
793
794 offset = end + 1;
795
796 ssize_t equalPos = attr.find("=");
797 if (equalPos < 0) {
798 continue;
799 }
800
801 AString key(attr, 0, equalPos);
802 key.trim();
803
804 AString val(attr, equalPos + 1, attr.size() - equalPos - 1);
805 val.trim();
806
807 ALOGV("key=%s value=%s", key.c_str(), val.c_str());
808
809 if (!strcasecmp("bandwidth", key.c_str())) {
810 const char *s = val.c_str();
811 char *end;
812 unsigned long x = strtoul(s, &end, 10);
813
814 if (end == s || *end != '\0') {
815 // malformed
816 continue;
817 }
818
819 if (meta->get() == NULL) {
820 *meta = new AMessage;
821 }
822 (*meta)->setInt32("bandwidth", x);
823 } else if (!strcasecmp("codecs", key.c_str())) {
824 if (!isQuotedString(val)) {
825 ALOGE("Expected quoted string for %s attribute, "
826 "got '%s' instead.",
827 key.c_str(), val.c_str());;
828
829 return ERROR_MALFORMED;
830 }
831
832 key.tolower();
833 const AString &codecs = unquoteString(val);
834 if (meta->get() == NULL) {
835 *meta = new AMessage;
836 }
837 (*meta)->setString(key.c_str(), codecs.c_str());
838 } else if (!strcasecmp("resolution", key.c_str())) {
839 const char *s = val.c_str();
840 char *end;
841 unsigned long width = strtoul(s, &end, 10);
842
843 if (end == s || *end != 'x') {
844 // malformed
845 continue;
846 }
847
848 s = end + 1;
849 unsigned long height = strtoul(s, &end, 10);
850
851 if (end == s || *end != '\0') {
852 // malformed
853 continue;
854 }
855
856 if (meta->get() == NULL) {
857 *meta = new AMessage;
858 }
859 (*meta)->setInt32("width", width);
860 (*meta)->setInt32("height", height);
861 } else if (!strcasecmp("audio", key.c_str())
862 || !strcasecmp("video", key.c_str())
863 || !strcasecmp("subtitles", key.c_str())) {
864 if (!isQuotedString(val)) {
865 ALOGE("Expected quoted string for %s attribute, "
866 "got '%s' instead.",
867 key.c_str(), val.c_str());
868
869 return ERROR_MALFORMED;
870 }
871
872 const AString &groupID = unquoteString(val);
873 ssize_t groupIndex = mMediaGroups.indexOfKey(groupID);
874
875 if (groupIndex < 0) {
876 ALOGE("Undefined media group '%s' referenced in stream info.",
877 groupID.c_str());
878
879 return ERROR_MALFORMED;
880 }
881
882 key.tolower();
883 if (meta->get() == NULL) {
884 *meta = new AMessage;
885 }
886 (*meta)->setString(key.c_str(), groupID.c_str());
887 }
888 }
889
890 return OK;
891 }
892
893 // static
parseCipherInfo(const AString & line,sp<AMessage> * meta,const AString & baseURI)894 status_t M3UParser::parseCipherInfo(
895 const AString &line, sp<AMessage> *meta, const AString &baseURI) {
896 ssize_t colonPos = line.find(":");
897
898 if (colonPos < 0) {
899 return ERROR_MALFORMED;
900 }
901
902 size_t offset = colonPos + 1;
903
904 while (offset < line.size()) {
905 ssize_t end = FindNextUnquoted(line, ',', offset);
906 if (end < 0) {
907 end = line.size();
908 }
909
910 AString attr(line, offset, end - offset);
911 attr.trim();
912
913 offset = end + 1;
914
915 ssize_t equalPos = attr.find("=");
916 if (equalPos < 0) {
917 continue;
918 }
919
920 AString key(attr, 0, equalPos);
921 key.trim();
922
923 AString val(attr, equalPos + 1, attr.size() - equalPos - 1);
924 val.trim();
925
926 ALOGV("key=%s value=%s", key.c_str(), val.c_str());
927
928 key.tolower();
929
930 if (key == "method" || key == "uri" || key == "iv") {
931 if (meta->get() == NULL) {
932 *meta = new AMessage;
933 }
934
935 if (key == "uri") {
936 if (val.size() >= 2
937 && val.c_str()[0] == '"'
938 && val.c_str()[val.size() - 1] == '"') {
939 // Remove surrounding quotes.
940 AString tmp(val, 1, val.size() - 2);
941 val = tmp;
942 }
943
944 AString absURI;
945 if (MakeURL(baseURI.c_str(), val.c_str(), &absURI)) {
946 val = absURI;
947 } else {
948 ALOGE("failed to make absolute url for %s.",
949 uriDebugString(baseURI).c_str());
950 }
951 }
952
953 key.insert(AString("cipher-"), 0);
954
955 (*meta)->setString(key.c_str(), val.c_str(), val.size());
956 }
957 }
958
959 return OK;
960 }
961
962 // static
parseByteRange(const AString & line,uint64_t curOffset,uint64_t * length,uint64_t * offset)963 status_t M3UParser::parseByteRange(
964 const AString &line, uint64_t curOffset,
965 uint64_t *length, uint64_t *offset) {
966 ssize_t colonPos = line.find(":");
967
968 if (colonPos < 0) {
969 return ERROR_MALFORMED;
970 }
971
972 ssize_t atPos = line.find("@", colonPos + 1);
973
974 AString lenStr;
975 if (atPos < 0) {
976 lenStr = AString(line, colonPos + 1, line.size() - colonPos - 1);
977 } else {
978 lenStr = AString(line, colonPos + 1, atPos - colonPos - 1);
979 }
980
981 lenStr.trim();
982
983 const char *s = lenStr.c_str();
984 char *end;
985 *length = strtoull(s, &end, 10);
986
987 if (s == end || *end != '\0') {
988 return ERROR_MALFORMED;
989 }
990
991 if (atPos >= 0) {
992 AString offStr = AString(line, atPos + 1, line.size() - atPos - 1);
993 offStr.trim();
994
995 const char *s = offStr.c_str();
996 *offset = strtoull(s, &end, 10);
997
998 if (s == end || *end != '\0') {
999 return ERROR_MALFORMED;
1000 }
1001 } else {
1002 *offset = curOffset;
1003 }
1004
1005 return OK;
1006 }
1007
parseMedia(const AString & line)1008 status_t M3UParser::parseMedia(const AString &line) {
1009 ssize_t colonPos = line.find(":");
1010
1011 if (colonPos < 0) {
1012 return ERROR_MALFORMED;
1013 }
1014
1015 bool haveGroupType = false;
1016 MediaGroup::Type groupType = MediaGroup::TYPE_AUDIO;
1017
1018 bool haveGroupID = false;
1019 AString groupID;
1020
1021 bool haveGroupLanguage = false;
1022 AString groupLanguage;
1023
1024 bool haveGroupName = false;
1025 AString groupName;
1026
1027 bool haveGroupAutoselect = false;
1028 bool groupAutoselect = false;
1029
1030 bool haveGroupDefault = false;
1031 bool groupDefault = false;
1032
1033 bool haveGroupForced = false;
1034 bool groupForced = false;
1035
1036 bool haveGroupURI = false;
1037 AString groupURI;
1038
1039 size_t offset = colonPos + 1;
1040
1041 while (offset < line.size()) {
1042 ssize_t end = FindNextUnquoted(line, ',', offset);
1043 if (end < 0) {
1044 end = line.size();
1045 }
1046
1047 AString attr(line, offset, end - offset);
1048 attr.trim();
1049
1050 offset = end + 1;
1051
1052 ssize_t equalPos = attr.find("=");
1053 if (equalPos < 0) {
1054 continue;
1055 }
1056
1057 AString key(attr, 0, equalPos);
1058 key.trim();
1059
1060 AString val(attr, equalPos + 1, attr.size() - equalPos - 1);
1061 val.trim();
1062
1063 ALOGV("key=%s value=%s", key.c_str(), val.c_str());
1064
1065 if (!strcasecmp("type", key.c_str())) {
1066 if (!strcasecmp("subtitles", val.c_str())) {
1067 groupType = MediaGroup::TYPE_SUBS;
1068 } else if (!strcasecmp("audio", val.c_str())) {
1069 groupType = MediaGroup::TYPE_AUDIO;
1070 } else if (!strcasecmp("video", val.c_str())) {
1071 groupType = MediaGroup::TYPE_VIDEO;
1072 } else if (!strcasecmp("closed-captions", val.c_str())){
1073 groupType = MediaGroup::TYPE_CC;
1074 } else {
1075 ALOGE("Invalid media group type '%s'", val.c_str());
1076 return ERROR_MALFORMED;
1077 }
1078
1079 haveGroupType = true;
1080 } else if (!strcasecmp("group-id", key.c_str())) {
1081 if (val.size() < 2
1082 || val.c_str()[0] != '"'
1083 || val.c_str()[val.size() - 1] != '"') {
1084 ALOGE("Expected quoted string for GROUP-ID, got '%s' instead.",
1085 val.c_str());
1086
1087 return ERROR_MALFORMED;
1088 }
1089
1090 groupID.setTo(val, 1, val.size() - 2);
1091 haveGroupID = true;
1092 } else if (!strcasecmp("language", key.c_str())) {
1093 if (val.size() < 2
1094 || val.c_str()[0] != '"'
1095 || val.c_str()[val.size() - 1] != '"') {
1096 ALOGE("Expected quoted string for LANGUAGE, got '%s' instead.",
1097 val.c_str());
1098
1099 return ERROR_MALFORMED;
1100 }
1101
1102 groupLanguage.setTo(val, 1, val.size() - 2);
1103 haveGroupLanguage = true;
1104 } else if (!strcasecmp("name", key.c_str())) {
1105 if (val.size() < 2
1106 || val.c_str()[0] != '"'
1107 || val.c_str()[val.size() - 1] != '"') {
1108 ALOGE("Expected quoted string for NAME, got '%s' instead.",
1109 val.c_str());
1110
1111 return ERROR_MALFORMED;
1112 }
1113
1114 groupName.setTo(val, 1, val.size() - 2);
1115 haveGroupName = true;
1116 } else if (!strcasecmp("autoselect", key.c_str())) {
1117 groupAutoselect = false;
1118 if (!strcasecmp("YES", val.c_str())) {
1119 groupAutoselect = true;
1120 } else if (!strcasecmp("NO", val.c_str())) {
1121 groupAutoselect = false;
1122 } else {
1123 ALOGE("Expected YES or NO for AUTOSELECT attribute, "
1124 "got '%s' instead.",
1125 val.c_str());
1126
1127 return ERROR_MALFORMED;
1128 }
1129
1130 haveGroupAutoselect = true;
1131 } else if (!strcasecmp("default", key.c_str())) {
1132 groupDefault = false;
1133 if (!strcasecmp("YES", val.c_str())) {
1134 groupDefault = true;
1135 } else if (!strcasecmp("NO", val.c_str())) {
1136 groupDefault = false;
1137 } else {
1138 ALOGE("Expected YES or NO for DEFAULT attribute, "
1139 "got '%s' instead.",
1140 val.c_str());
1141
1142 return ERROR_MALFORMED;
1143 }
1144
1145 haveGroupDefault = true;
1146 } else if (!strcasecmp("forced", key.c_str())) {
1147 groupForced = false;
1148 if (!strcasecmp("YES", val.c_str())) {
1149 groupForced = true;
1150 } else if (!strcasecmp("NO", val.c_str())) {
1151 groupForced = false;
1152 } else {
1153 ALOGE("Expected YES or NO for FORCED attribute, "
1154 "got '%s' instead.",
1155 val.c_str());
1156
1157 return ERROR_MALFORMED;
1158 }
1159
1160 haveGroupForced = true;
1161 } else if (!strcasecmp("uri", key.c_str())) {
1162 if (val.size() < 2
1163 || val.c_str()[0] != '"'
1164 || val.c_str()[val.size() - 1] != '"') {
1165 ALOGE("Expected quoted string for URI, got '%s' instead.",
1166 val.c_str());
1167
1168 return ERROR_MALFORMED;
1169 }
1170
1171 AString tmp(val, 1, val.size() - 2);
1172
1173 if (!MakeURL(mBaseURI.c_str(), tmp.c_str(), &groupURI)) {
1174 ALOGI("Failed to make absolute URI from '%s'.", tmp.c_str());
1175 }
1176
1177 haveGroupURI = true;
1178 }
1179 }
1180
1181 if (!haveGroupType || !haveGroupID || !haveGroupName) {
1182 ALOGE("Incomplete EXT-X-MEDIA element.");
1183 return ERROR_MALFORMED;
1184 }
1185
1186 if (groupType == MediaGroup::TYPE_CC) {
1187 // TODO: ignore this for now.
1188 // CC track will be detected by CCDecoder. But we still need to
1189 // pass the CC track flags (lang, auto) to the app in the future.
1190 return OK;
1191 }
1192
1193 uint32_t flags = 0;
1194 if (haveGroupAutoselect && groupAutoselect) {
1195 flags |= MediaGroup::FLAG_AUTOSELECT;
1196 }
1197 if (haveGroupDefault && groupDefault) {
1198 flags |= MediaGroup::FLAG_DEFAULT;
1199 }
1200 if (haveGroupForced) {
1201 if (groupType != MediaGroup::TYPE_SUBS) {
1202 ALOGE("The FORCED attribute MUST not be present on anything "
1203 "but SUBS media.");
1204
1205 return ERROR_MALFORMED;
1206 }
1207
1208 if (groupForced) {
1209 flags |= MediaGroup::FLAG_FORCED;
1210 }
1211 }
1212 if (haveGroupLanguage) {
1213 flags |= MediaGroup::FLAG_HAS_LANGUAGE;
1214 }
1215 if (haveGroupURI) {
1216 flags |= MediaGroup::FLAG_HAS_URI;
1217 }
1218
1219 ssize_t groupIndex = mMediaGroups.indexOfKey(groupID);
1220 sp<MediaGroup> group;
1221
1222 if (groupIndex < 0) {
1223 group = new MediaGroup(groupType);
1224 mMediaGroups.add(groupID, group);
1225 } else {
1226 group = mMediaGroups.valueAt(groupIndex);
1227
1228 if (group->type() != groupType) {
1229 ALOGE("Attempt to put media item under group of different type "
1230 "(groupType = %d, item type = %d",
1231 group->type(),
1232 groupType);
1233
1234 return ERROR_MALFORMED;
1235 }
1236 }
1237
1238 return group->addMedia(
1239 groupName.c_str(),
1240 haveGroupURI ? groupURI.c_str() : NULL,
1241 haveGroupLanguage ? groupLanguage.c_str() : NULL,
1242 flags);
1243 }
1244
1245 // static
parseDiscontinuitySequence(const AString & line,size_t * seq)1246 status_t M3UParser::parseDiscontinuitySequence(const AString &line, size_t *seq) {
1247 ssize_t colonPos = line.find(":");
1248
1249 if (colonPos < 0) {
1250 return ERROR_MALFORMED;
1251 }
1252
1253 int32_t x;
1254 status_t err = ParseInt32(line.c_str() + colonPos + 1, &x);
1255 if (err != OK) {
1256 return err;
1257 }
1258
1259 if (x < 0) {
1260 return ERROR_MALFORMED;
1261 }
1262
1263 if (seq) {
1264 *seq = x;
1265 }
1266 return OK;
1267 }
1268
1269 // static
ParseInt32(const char * s,int32_t * x)1270 status_t M3UParser::ParseInt32(const char *s, int32_t *x) {
1271 char *end;
1272 long lval = strtol(s, &end, 10);
1273
1274 if (end == s || (*end != '\0' && *end != ',')) {
1275 return ERROR_MALFORMED;
1276 }
1277
1278 *x = (int32_t)lval;
1279
1280 return OK;
1281 }
1282
1283 // static
ParseDouble(const char * s,double * x)1284 status_t M3UParser::ParseDouble(const char *s, double *x) {
1285 char *end;
1286 double dval = strtod(s, &end);
1287
1288 if (end == s || (*end != '\0' && *end != ',')) {
1289 return ERROR_MALFORMED;
1290 }
1291
1292 *x = dval;
1293
1294 return OK;
1295 }
1296
1297 // static
isQuotedString(const AString & str)1298 bool M3UParser::isQuotedString(const AString &str) {
1299 if (str.size() < 2
1300 || str.c_str()[0] != '"'
1301 || str.c_str()[str.size() - 1] != '"') {
1302 return false;
1303 }
1304 return true;
1305 }
1306
1307 // static
unquoteString(const AString & str)1308 AString M3UParser::unquoteString(const AString &str) {
1309 if (!isQuotedString(str)) {
1310 return str;
1311 }
1312 return AString(str, 1, str.size() - 2);
1313 }
1314
1315 // static
codecIsType(const AString & codec,const char * type)1316 bool M3UParser::codecIsType(const AString &codec, const char *type) {
1317 if (codec.size() < 4) {
1318 return false;
1319 }
1320 const char *c = codec.c_str();
1321 switch (FOURCC(c[0], c[1], c[2], c[3])) {
1322 // List extracted from http://www.mp4ra.org/codecs.html
1323 case 'ac-3':
1324 case 'alac':
1325 case 'dra1':
1326 case 'dtsc':
1327 case 'dtse':
1328 case 'dtsh':
1329 case 'dtsl':
1330 case 'ec-3':
1331 case 'enca':
1332 case 'g719':
1333 case 'g726':
1334 case 'm4ae':
1335 case 'mlpa':
1336 case 'mp4a':
1337 case 'raw ':
1338 case 'samr':
1339 case 'sawb':
1340 case 'sawp':
1341 case 'sevc':
1342 case 'sqcp':
1343 case 'ssmv':
1344 case 'twos':
1345 case 'agsm':
1346 case 'alaw':
1347 case 'dvi ':
1348 case 'fl32':
1349 case 'fl64':
1350 case 'ima4':
1351 case 'in24':
1352 case 'in32':
1353 case 'lpcm':
1354 case 'Qclp':
1355 case 'QDM2':
1356 case 'QDMC':
1357 case 'ulaw':
1358 case 'vdva':
1359 return !strcmp("audio", type);
1360
1361 case 'avc1':
1362 case 'avc2':
1363 case 'avcp':
1364 case 'drac':
1365 case 'encv':
1366 case 'mjp2':
1367 case 'mp4v':
1368 case 'mvc1':
1369 case 'mvc2':
1370 case 'resv':
1371 case 's263':
1372 case 'svc1':
1373 case 'vc-1':
1374 case 'CFHD':
1375 case 'civd':
1376 case 'DV10':
1377 case 'dvh5':
1378 case 'dvh6':
1379 case 'dvhp':
1380 case 'DVOO':
1381 case 'DVOR':
1382 case 'DVTV':
1383 case 'DVVT':
1384 case 'flic':
1385 case 'gif ':
1386 case 'h261':
1387 case 'h263':
1388 case 'HD10':
1389 case 'jpeg':
1390 case 'M105':
1391 case 'mjpa':
1392 case 'mjpb':
1393 case 'png ':
1394 case 'PNTG':
1395 case 'rle ':
1396 case 'rpza':
1397 case 'Shr0':
1398 case 'Shr1':
1399 case 'Shr2':
1400 case 'Shr3':
1401 case 'Shr4':
1402 case 'SVQ1':
1403 case 'SVQ3':
1404 case 'tga ':
1405 case 'tiff':
1406 case 'WRLE':
1407 return !strcmp("video", type);
1408
1409 default:
1410 return false;
1411 }
1412 }
1413
1414 } // namespace android
1415