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