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