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