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-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