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