M3UParser.cpp revision 99cef1ef1cf1232966fabf3793ce7964c01474d7
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    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      mDiscontinuitySeq(0),
254      mSelectedIndex(-1) {
255    mInitCheck = parse(data, size);
256}
257
258M3UParser::~M3UParser() {
259}
260
261status_t M3UParser::initCheck() const {
262    return mInitCheck;
263}
264
265bool M3UParser::isExtM3U() const {
266    return mIsExtM3U;
267}
268
269bool M3UParser::isVariantPlaylist() const {
270    return mIsVariantPlaylist;
271}
272
273bool M3UParser::isComplete() const {
274    return mIsComplete;
275}
276
277bool M3UParser::isEvent() const {
278    return mIsEvent;
279}
280
281size_t M3UParser::getDiscontinuitySeq() const {
282    return mDiscontinuitySeq;
283}
284
285sp<AMessage> M3UParser::meta() {
286    return mMeta;
287}
288
289size_t M3UParser::size() {
290    return mItems.size();
291}
292
293bool M3UParser::itemAt(size_t index, AString *uri, sp<AMessage> *meta) {
294    if (uri) {
295        uri->clear();
296    }
297
298    if (meta) {
299        *meta = NULL;
300    }
301
302    if (index >= mItems.size()) {
303        return false;
304    }
305
306    if (uri) {
307        *uri = mItems.itemAt(index).mURI;
308    }
309
310    if (meta) {
311        *meta = mItems.itemAt(index).mMeta;
312    }
313
314    return true;
315}
316
317void M3UParser::pickRandomMediaItems() {
318    for (size_t i = 0; i < mMediaGroups.size(); ++i) {
319        mMediaGroups.valueAt(i)->pickRandomMediaItems();
320    }
321}
322
323status_t M3UParser::selectTrack(size_t index, bool select) {
324    for (size_t i = 0, ii = index; i < mMediaGroups.size(); ++i) {
325        sp<MediaGroup> group = mMediaGroups.valueAt(i);
326        size_t tracks = group->countTracks();
327        if (ii < tracks) {
328            status_t err = group->selectTrack(ii, select);
329            if (err == OK) {
330                mSelectedIndex = select ? index : -1;
331            }
332            return err;
333        }
334        ii -= tracks;
335    }
336    return INVALID_OPERATION;
337}
338
339size_t M3UParser::getTrackCount() const {
340    size_t trackCount = 0;
341    for (size_t i = 0; i < mMediaGroups.size(); ++i) {
342        trackCount += mMediaGroups.valueAt(i)->countTracks();
343    }
344    return trackCount;
345}
346
347sp<AMessage> M3UParser::getTrackInfo(size_t index) const {
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            return group->getTrackInfo(ii);
353        }
354        ii -= tracks;
355    }
356    return NULL;
357}
358
359ssize_t M3UParser::getSelectedIndex() const {
360    return mSelectedIndex;
361}
362
363ssize_t M3UParser::getSelectedTrack(media_track_type type) const {
364    MediaGroup::Type groupType;
365    switch (type) {
366        case MEDIA_TRACK_TYPE_VIDEO:
367            groupType = MediaGroup::TYPE_VIDEO;
368            break;
369
370        case MEDIA_TRACK_TYPE_AUDIO:
371            groupType = MediaGroup::TYPE_AUDIO;
372            break;
373
374        case MEDIA_TRACK_TYPE_SUBTITLE:
375            groupType = MediaGroup::TYPE_SUBS;
376            break;
377
378        default:
379            return -1;
380    }
381
382    for (size_t i = 0, ii = 0; i < mMediaGroups.size(); ++i) {
383        sp<MediaGroup> group = mMediaGroups.valueAt(i);
384        size_t tracks = group->countTracks();
385        if (groupType != group->mType) {
386            ii += tracks;
387        } else if (group->mSelectedIndex >= 0) {
388            return ii + group->mSelectedIndex;
389        }
390    }
391
392    return -1;
393}
394
395bool M3UParser::getTypeURI(size_t index, const char *key, AString *uri) const {
396    if (!mIsVariantPlaylist) {
397        *uri = mBaseURI;
398
399        // Assume media without any more specific attribute contains
400        // audio and video, but no subtitles.
401        return !strcmp("audio", key) || !strcmp("video", key);
402    }
403
404    CHECK_LT(index, mItems.size());
405
406    sp<AMessage> meta = mItems.itemAt(index).mMeta;
407
408    AString groupID;
409    if (!meta->findString(key, &groupID)) {
410        *uri = mItems.itemAt(index).mURI;
411
412        AString codecs;
413        if (!meta->findString("codecs", &codecs)) {
414            // Assume media without any more specific attribute contains
415            // audio and video, but no subtitles.
416            return !strcmp("audio", key) || !strcmp("video", key);
417        } else {
418            // Split the comma separated list of codecs.
419            size_t offset = 0;
420            ssize_t commaPos = -1;
421            codecs.append(',');
422            while ((commaPos = codecs.find(",", offset)) >= 0) {
423                AString codec(codecs, offset, commaPos - offset);
424                codec.trim();
425                // return true only if a codec of type `key` ("audio"/"video")
426                // is found.
427                if (codecIsType(codec, key)) {
428                    return true;
429                }
430                offset = commaPos + 1;
431            }
432            return false;
433        }
434    }
435
436    sp<MediaGroup> group = mMediaGroups.valueFor(groupID);
437    if (!group->getActiveURI(uri)) {
438        return false;
439    }
440
441    if ((*uri).empty()) {
442        *uri = mItems.itemAt(index).mURI;
443    }
444
445    return true;
446}
447
448static bool MakeURL(const char *baseURL, const char *url, AString *out) {
449    out->clear();
450
451    if (strncasecmp("http://", baseURL, 7)
452            && strncasecmp("https://", baseURL, 8)
453            && strncasecmp("file://", baseURL, 7)) {
454        // Base URL must be absolute
455        return false;
456    }
457    const size_t schemeEnd = (strstr(baseURL, "//") - baseURL) + 2;
458    CHECK(schemeEnd == 7 || schemeEnd == 8);
459
460    if (!strncasecmp("http://", url, 7) || !strncasecmp("https://", url, 8)) {
461        // "url" is already an absolute URL, ignore base URL.
462        out->setTo(url);
463
464        ALOGV("base:'%s', url:'%s' => '%s'", baseURL, url, out->c_str());
465
466        return true;
467    }
468
469    if (url[0] == '/') {
470        // URL is an absolute path.
471
472        char *protocolEnd = strstr(baseURL, "//") + 2;
473        char *pathStart = strchr(protocolEnd, '/');
474
475        if (pathStart != NULL) {
476            out->setTo(baseURL, pathStart - baseURL);
477        } else {
478            out->setTo(baseURL);
479        }
480
481        out->append(url);
482    } else {
483        // URL is a relative path
484
485        // Check for a possible query string
486        const char *qsPos = strchr(baseURL, '?');
487        size_t end;
488        if (qsPos != NULL) {
489            end = qsPos - baseURL;
490        } else {
491            end = strlen(baseURL);
492        }
493        // Check for the last slash before a potential query string
494        for (ssize_t pos = end - 1; pos >= 0; pos--) {
495            if (baseURL[pos] == '/') {
496                end = pos;
497                break;
498            }
499        }
500
501        // Check whether the found slash actually is part of the path
502        // and not part of the "http://".
503        if (end >= schemeEnd) {
504            out->setTo(baseURL, end);
505        } else {
506            out->setTo(baseURL);
507        }
508
509        out->append("/");
510        out->append(url);
511    }
512
513    ALOGV("base:'%s', url:'%s' => '%s'", baseURL, url, out->c_str());
514
515    return true;
516}
517
518status_t M3UParser::parse(const void *_data, size_t size) {
519    int32_t lineNo = 0;
520
521    sp<AMessage> itemMeta;
522
523    const char *data = (const char *)_data;
524    size_t offset = 0;
525    uint64_t segmentRangeOffset = 0;
526    while (offset < size) {
527        size_t offsetLF = offset;
528        while (offsetLF < size && data[offsetLF] != '\n') {
529            ++offsetLF;
530        }
531
532        AString line;
533        if (offsetLF > offset && data[offsetLF - 1] == '\r') {
534            line.setTo(&data[offset], offsetLF - offset - 1);
535        } else {
536            line.setTo(&data[offset], offsetLF - offset);
537        }
538
539        // ALOGI("#%s#", line.c_str());
540
541        if (line.empty()) {
542            offset = offsetLF + 1;
543            continue;
544        }
545
546        if (lineNo == 0 && line == "#EXTM3U") {
547            mIsExtM3U = true;
548        }
549
550        if (mIsExtM3U) {
551            status_t err = OK;
552
553            if (line.startsWith("#EXT-X-TARGETDURATION")) {
554                if (mIsVariantPlaylist) {
555                    return ERROR_MALFORMED;
556                }
557                err = parseMetaData(line, &mMeta, "target-duration");
558            } else if (line.startsWith("#EXT-X-MEDIA-SEQUENCE")) {
559                if (mIsVariantPlaylist) {
560                    return ERROR_MALFORMED;
561                }
562                err = parseMetaData(line, &mMeta, "media-sequence");
563            } else if (line.startsWith("#EXT-X-KEY")) {
564                if (mIsVariantPlaylist) {
565                    return ERROR_MALFORMED;
566                }
567                err = parseCipherInfo(line, &itemMeta, mBaseURI);
568            } else if (line.startsWith("#EXT-X-ENDLIST")) {
569                mIsComplete = true;
570            } else if (line.startsWith("#EXT-X-PLAYLIST-TYPE:EVENT")) {
571                mIsEvent = true;
572            } else if (line.startsWith("#EXTINF")) {
573                if (mIsVariantPlaylist) {
574                    return ERROR_MALFORMED;
575                }
576                err = parseMetaDataDuration(line, &itemMeta, "durationUs");
577            } else if (line.startsWith("#EXT-X-DISCONTINUITY")) {
578                if (mIsVariantPlaylist) {
579                    return ERROR_MALFORMED;
580                }
581                if (itemMeta == NULL) {
582                    itemMeta = new AMessage;
583                }
584                itemMeta->setInt32("discontinuity", true);
585            } else if (line.startsWith("#EXT-X-STREAM-INF")) {
586                if (mMeta != NULL) {
587                    return ERROR_MALFORMED;
588                }
589                mIsVariantPlaylist = true;
590                err = parseStreamInf(line, &itemMeta);
591            } else if (line.startsWith("#EXT-X-BYTERANGE")) {
592                if (mIsVariantPlaylist) {
593                    return ERROR_MALFORMED;
594                }
595
596                uint64_t length, offset;
597                err = parseByteRange(line, segmentRangeOffset, &length, &offset);
598
599                if (err == OK) {
600                    if (itemMeta == NULL) {
601                        itemMeta = new AMessage;
602                    }
603
604                    itemMeta->setInt64("range-offset", offset);
605                    itemMeta->setInt64("range-length", length);
606
607                    segmentRangeOffset = offset + length;
608                }
609            } else if (line.startsWith("#EXT-X-MEDIA")) {
610                err = parseMedia(line);
611            } else if (line.startsWith("#EXT-X-DISCONTINUITY-SEQUENCE")) {
612                size_t seq;
613                err = parseDiscontinuitySequence(line, &seq);
614                if (err == OK) {
615                    mDiscontinuitySeq = seq;
616                }
617            }
618
619            if (err != OK) {
620                return err;
621            }
622        }
623
624        if (!line.startsWith("#")) {
625            if (!mIsVariantPlaylist) {
626                int64_t durationUs;
627                if (itemMeta == NULL
628                        || !itemMeta->findInt64("durationUs", &durationUs)) {
629                    return ERROR_MALFORMED;
630                }
631            }
632
633            mItems.push();
634            Item *item = &mItems.editItemAt(mItems.size() - 1);
635
636            CHECK(MakeURL(mBaseURI.c_str(), line.c_str(), &item->mURI));
637
638            item->mMeta = itemMeta;
639
640            itemMeta.clear();
641        }
642
643        offset = offsetLF + 1;
644        ++lineNo;
645    }
646
647    return OK;
648}
649
650// static
651status_t M3UParser::parseMetaData(
652        const AString &line, sp<AMessage> *meta, const char *key) {
653    ssize_t colonPos = line.find(":");
654
655    if (colonPos < 0) {
656        return ERROR_MALFORMED;
657    }
658
659    int32_t x;
660    status_t err = ParseInt32(line.c_str() + colonPos + 1, &x);
661
662    if (err != OK) {
663        return err;
664    }
665
666    if (meta->get() == NULL) {
667        *meta = new AMessage;
668    }
669    (*meta)->setInt32(key, x);
670
671    return OK;
672}
673
674// static
675status_t M3UParser::parseMetaDataDuration(
676        const AString &line, sp<AMessage> *meta, const char *key) {
677    ssize_t colonPos = line.find(":");
678
679    if (colonPos < 0) {
680        return ERROR_MALFORMED;
681    }
682
683    double x;
684    status_t err = ParseDouble(line.c_str() + colonPos + 1, &x);
685
686    if (err != OK) {
687        return err;
688    }
689
690    if (meta->get() == NULL) {
691        *meta = new AMessage;
692    }
693    (*meta)->setInt64(key, (int64_t)(x * 1E6));
694
695    return OK;
696}
697
698// Find the next occurence of the character "what" at or after "offset",
699// but ignore occurences between quotation marks.
700// Return the index of the occurrence or -1 if not found.
701static ssize_t FindNextUnquoted(
702        const AString &line, char what, size_t offset) {
703    CHECK_NE((int)what, (int)'"');
704
705    bool quoted = false;
706    while (offset < line.size()) {
707        char c = line.c_str()[offset];
708
709        if (c == '"') {
710            quoted = !quoted;
711        } else if (c == what && !quoted) {
712            return offset;
713        }
714
715        ++offset;
716    }
717
718    return -1;
719}
720
721status_t M3UParser::parseStreamInf(
722        const AString &line, sp<AMessage> *meta) const {
723    ssize_t colonPos = line.find(":");
724
725    if (colonPos < 0) {
726        return ERROR_MALFORMED;
727    }
728
729    size_t offset = colonPos + 1;
730
731    while (offset < line.size()) {
732        ssize_t end = FindNextUnquoted(line, ',', offset);
733        if (end < 0) {
734            end = line.size();
735        }
736
737        AString attr(line, offset, end - offset);
738        attr.trim();
739
740        offset = end + 1;
741
742        ssize_t equalPos = attr.find("=");
743        if (equalPos < 0) {
744            continue;
745        }
746
747        AString key(attr, 0, equalPos);
748        key.trim();
749
750        AString val(attr, equalPos + 1, attr.size() - equalPos - 1);
751        val.trim();
752
753        ALOGV("key=%s value=%s", key.c_str(), val.c_str());
754
755        if (!strcasecmp("bandwidth", key.c_str())) {
756            const char *s = val.c_str();
757            char *end;
758            unsigned long x = strtoul(s, &end, 10);
759
760            if (end == s || *end != '\0') {
761                // malformed
762                continue;
763            }
764
765            if (meta->get() == NULL) {
766                *meta = new AMessage;
767            }
768            (*meta)->setInt32("bandwidth", x);
769        } else if (!strcasecmp("codecs", key.c_str())) {
770            if (!isQuotedString(val)) {
771                ALOGE("Expected quoted string for %s attribute, "
772                      "got '%s' instead.",
773                      key.c_str(), val.c_str());;
774
775                return ERROR_MALFORMED;
776            }
777
778            key.tolower();
779            const AString &codecs = unquoteString(val);
780            if (meta->get() == NULL) {
781                *meta = new AMessage;
782            }
783            (*meta)->setString(key.c_str(), codecs.c_str());
784        } else if (!strcasecmp("audio", key.c_str())
785                || !strcasecmp("video", key.c_str())
786                || !strcasecmp("subtitles", key.c_str())) {
787            if (!isQuotedString(val)) {
788                ALOGE("Expected quoted string for %s attribute, "
789                      "got '%s' instead.",
790                      key.c_str(), val.c_str());
791
792                return ERROR_MALFORMED;
793            }
794
795            const AString &groupID = unquoteString(val);
796            ssize_t groupIndex = mMediaGroups.indexOfKey(groupID);
797
798            if (groupIndex < 0) {
799                ALOGE("Undefined media group '%s' referenced in stream info.",
800                      groupID.c_str());
801
802                return ERROR_MALFORMED;
803            }
804
805            key.tolower();
806            if (meta->get() == NULL) {
807                *meta = new AMessage;
808            }
809            (*meta)->setString(key.c_str(), groupID.c_str());
810        }
811    }
812
813    return OK;
814}
815
816// static
817status_t M3UParser::parseCipherInfo(
818        const AString &line, sp<AMessage> *meta, const AString &baseURI) {
819    ssize_t colonPos = line.find(":");
820
821    if (colonPos < 0) {
822        return ERROR_MALFORMED;
823    }
824
825    size_t offset = colonPos + 1;
826
827    while (offset < line.size()) {
828        ssize_t end = FindNextUnquoted(line, ',', offset);
829        if (end < 0) {
830            end = line.size();
831        }
832
833        AString attr(line, offset, end - offset);
834        attr.trim();
835
836        offset = end + 1;
837
838        ssize_t equalPos = attr.find("=");
839        if (equalPos < 0) {
840            continue;
841        }
842
843        AString key(attr, 0, equalPos);
844        key.trim();
845
846        AString val(attr, equalPos + 1, attr.size() - equalPos - 1);
847        val.trim();
848
849        ALOGV("key=%s value=%s", key.c_str(), val.c_str());
850
851        key.tolower();
852
853        if (key == "method" || key == "uri" || key == "iv") {
854            if (meta->get() == NULL) {
855                *meta = new AMessage;
856            }
857
858            if (key == "uri") {
859                if (val.size() >= 2
860                        && val.c_str()[0] == '"'
861                        && val.c_str()[val.size() - 1] == '"') {
862                    // Remove surrounding quotes.
863                    AString tmp(val, 1, val.size() - 2);
864                    val = tmp;
865                }
866
867                AString absURI;
868                if (MakeURL(baseURI.c_str(), val.c_str(), &absURI)) {
869                    val = absURI;
870                } else {
871                    ALOGE("failed to make absolute url for %s.",
872                            uriDebugString(baseURI).c_str());
873                }
874            }
875
876            key.insert(AString("cipher-"), 0);
877
878            (*meta)->setString(key.c_str(), val.c_str(), val.size());
879        }
880    }
881
882    return OK;
883}
884
885// static
886status_t M3UParser::parseByteRange(
887        const AString &line, uint64_t curOffset,
888        uint64_t *length, uint64_t *offset) {
889    ssize_t colonPos = line.find(":");
890
891    if (colonPos < 0) {
892        return ERROR_MALFORMED;
893    }
894
895    ssize_t atPos = line.find("@", colonPos + 1);
896
897    AString lenStr;
898    if (atPos < 0) {
899        lenStr = AString(line, colonPos + 1, line.size() - colonPos - 1);
900    } else {
901        lenStr = AString(line, colonPos + 1, atPos - colonPos - 1);
902    }
903
904    lenStr.trim();
905
906    const char *s = lenStr.c_str();
907    char *end;
908    *length = strtoull(s, &end, 10);
909
910    if (s == end || *end != '\0') {
911        return ERROR_MALFORMED;
912    }
913
914    if (atPos >= 0) {
915        AString offStr = AString(line, atPos + 1, line.size() - atPos - 1);
916        offStr.trim();
917
918        const char *s = offStr.c_str();
919        *offset = strtoull(s, &end, 10);
920
921        if (s == end || *end != '\0') {
922            return ERROR_MALFORMED;
923        }
924    } else {
925        *offset = curOffset;
926    }
927
928    return OK;
929}
930
931status_t M3UParser::parseMedia(const AString &line) {
932    ssize_t colonPos = line.find(":");
933
934    if (colonPos < 0) {
935        return ERROR_MALFORMED;
936    }
937
938    bool haveGroupType = false;
939    MediaGroup::Type groupType = MediaGroup::TYPE_AUDIO;
940
941    bool haveGroupID = false;
942    AString groupID;
943
944    bool haveGroupLanguage = false;
945    AString groupLanguage;
946
947    bool haveGroupName = false;
948    AString groupName;
949
950    bool haveGroupAutoselect = false;
951    bool groupAutoselect = false;
952
953    bool haveGroupDefault = false;
954    bool groupDefault = false;
955
956    bool haveGroupForced = false;
957    bool groupForced = false;
958
959    bool haveGroupURI = false;
960    AString groupURI;
961
962    size_t offset = colonPos + 1;
963
964    while (offset < line.size()) {
965        ssize_t end = FindNextUnquoted(line, ',', offset);
966        if (end < 0) {
967            end = line.size();
968        }
969
970        AString attr(line, offset, end - offset);
971        attr.trim();
972
973        offset = end + 1;
974
975        ssize_t equalPos = attr.find("=");
976        if (equalPos < 0) {
977            continue;
978        }
979
980        AString key(attr, 0, equalPos);
981        key.trim();
982
983        AString val(attr, equalPos + 1, attr.size() - equalPos - 1);
984        val.trim();
985
986        ALOGV("key=%s value=%s", key.c_str(), val.c_str());
987
988        if (!strcasecmp("type", key.c_str())) {
989            if (!strcasecmp("subtitles", val.c_str())) {
990                groupType = MediaGroup::TYPE_SUBS;
991            } else if (!strcasecmp("audio", val.c_str())) {
992                groupType = MediaGroup::TYPE_AUDIO;
993            } else if (!strcasecmp("video", val.c_str())) {
994                groupType = MediaGroup::TYPE_VIDEO;
995            } else if (!strcasecmp("closed-captions", val.c_str())){
996                groupType = MediaGroup::TYPE_CC;
997            } else {
998                ALOGE("Invalid media group type '%s'", val.c_str());
999                return ERROR_MALFORMED;
1000            }
1001
1002            haveGroupType = true;
1003        } else if (!strcasecmp("group-id", key.c_str())) {
1004            if (val.size() < 2
1005                    || val.c_str()[0] != '"'
1006                    || val.c_str()[val.size() - 1] != '"') {
1007                ALOGE("Expected quoted string for GROUP-ID, got '%s' instead.",
1008                      val.c_str());
1009
1010                return ERROR_MALFORMED;
1011            }
1012
1013            groupID.setTo(val, 1, val.size() - 2);
1014            haveGroupID = true;
1015        } else if (!strcasecmp("language", key.c_str())) {
1016            if (val.size() < 2
1017                    || val.c_str()[0] != '"'
1018                    || val.c_str()[val.size() - 1] != '"') {
1019                ALOGE("Expected quoted string for LANGUAGE, got '%s' instead.",
1020                      val.c_str());
1021
1022                return ERROR_MALFORMED;
1023            }
1024
1025            groupLanguage.setTo(val, 1, val.size() - 2);
1026            haveGroupLanguage = true;
1027        } else if (!strcasecmp("name", key.c_str())) {
1028            if (val.size() < 2
1029                    || val.c_str()[0] != '"'
1030                    || val.c_str()[val.size() - 1] != '"') {
1031                ALOGE("Expected quoted string for NAME, got '%s' instead.",
1032                      val.c_str());
1033
1034                return ERROR_MALFORMED;
1035            }
1036
1037            groupName.setTo(val, 1, val.size() - 2);
1038            haveGroupName = true;
1039        } else if (!strcasecmp("autoselect", key.c_str())) {
1040            groupAutoselect = false;
1041            if (!strcasecmp("YES", val.c_str())) {
1042                groupAutoselect = true;
1043            } else if (!strcasecmp("NO", val.c_str())) {
1044                groupAutoselect = false;
1045            } else {
1046                ALOGE("Expected YES or NO for AUTOSELECT attribute, "
1047                      "got '%s' instead.",
1048                      val.c_str());
1049
1050                return ERROR_MALFORMED;
1051            }
1052
1053            haveGroupAutoselect = true;
1054        } else if (!strcasecmp("default", key.c_str())) {
1055            groupDefault = false;
1056            if (!strcasecmp("YES", val.c_str())) {
1057                groupDefault = true;
1058            } else if (!strcasecmp("NO", val.c_str())) {
1059                groupDefault = false;
1060            } else {
1061                ALOGE("Expected YES or NO for DEFAULT attribute, "
1062                      "got '%s' instead.",
1063                      val.c_str());
1064
1065                return ERROR_MALFORMED;
1066            }
1067
1068            haveGroupDefault = true;
1069        } else if (!strcasecmp("forced", key.c_str())) {
1070            groupForced = false;
1071            if (!strcasecmp("YES", val.c_str())) {
1072                groupForced = true;
1073            } else if (!strcasecmp("NO", val.c_str())) {
1074                groupForced = false;
1075            } else {
1076                ALOGE("Expected YES or NO for FORCED attribute, "
1077                      "got '%s' instead.",
1078                      val.c_str());
1079
1080                return ERROR_MALFORMED;
1081            }
1082
1083            haveGroupForced = true;
1084        } else if (!strcasecmp("uri", key.c_str())) {
1085            if (val.size() < 2
1086                    || val.c_str()[0] != '"'
1087                    || val.c_str()[val.size() - 1] != '"') {
1088                ALOGE("Expected quoted string for URI, got '%s' instead.",
1089                      val.c_str());
1090
1091                return ERROR_MALFORMED;
1092            }
1093
1094            AString tmp(val, 1, val.size() - 2);
1095
1096            if (!MakeURL(mBaseURI.c_str(), tmp.c_str(), &groupURI)) {
1097                ALOGI("Failed to make absolute URI from '%s'.", tmp.c_str());
1098            }
1099
1100            haveGroupURI = true;
1101        }
1102    }
1103
1104    if (!haveGroupType || !haveGroupID || !haveGroupName) {
1105        ALOGE("Incomplete EXT-X-MEDIA element.");
1106        return ERROR_MALFORMED;
1107    }
1108
1109    if (groupType == MediaGroup::TYPE_CC) {
1110        // TODO: ignore this for now.
1111        // CC track will be detected by CCDecoder. But we still need to
1112        // pass the CC track flags (lang, auto) to the app in the future.
1113        return OK;
1114    }
1115
1116    uint32_t flags = 0;
1117    if (haveGroupAutoselect && groupAutoselect) {
1118        flags |= MediaGroup::FLAG_AUTOSELECT;
1119    }
1120    if (haveGroupDefault && groupDefault) {
1121        flags |= MediaGroup::FLAG_DEFAULT;
1122    }
1123    if (haveGroupForced) {
1124        if (groupType != MediaGroup::TYPE_SUBS) {
1125            ALOGE("The FORCED attribute MUST not be present on anything "
1126                  "but SUBS media.");
1127
1128            return ERROR_MALFORMED;
1129        }
1130
1131        if (groupForced) {
1132            flags |= MediaGroup::FLAG_FORCED;
1133        }
1134    }
1135    if (haveGroupLanguage) {
1136        flags |= MediaGroup::FLAG_HAS_LANGUAGE;
1137    }
1138    if (haveGroupURI) {
1139        flags |= MediaGroup::FLAG_HAS_URI;
1140    }
1141
1142    ssize_t groupIndex = mMediaGroups.indexOfKey(groupID);
1143    sp<MediaGroup> group;
1144
1145    if (groupIndex < 0) {
1146        group = new MediaGroup(groupType);
1147        mMediaGroups.add(groupID, group);
1148    } else {
1149        group = mMediaGroups.valueAt(groupIndex);
1150
1151        if (group->type() != groupType) {
1152            ALOGE("Attempt to put media item under group of different type "
1153                  "(groupType = %d, item type = %d",
1154                  group->type(),
1155                  groupType);
1156
1157            return ERROR_MALFORMED;
1158        }
1159    }
1160
1161    return group->addMedia(
1162            groupName.c_str(),
1163            haveGroupURI ? groupURI.c_str() : NULL,
1164            haveGroupLanguage ? groupLanguage.c_str() : NULL,
1165            flags);
1166}
1167
1168// static
1169status_t M3UParser::parseDiscontinuitySequence(const AString &line, size_t *seq) {
1170    ssize_t colonPos = line.find(":");
1171
1172    if (colonPos < 0) {
1173        return ERROR_MALFORMED;
1174    }
1175
1176    int32_t x;
1177    status_t err = ParseInt32(line.c_str() + colonPos + 1, &x);
1178    if (err != OK) {
1179        return err;
1180    }
1181
1182    if (x < 0) {
1183        return ERROR_MALFORMED;
1184    }
1185
1186    if (seq) {
1187        *seq = x;
1188    }
1189    return OK;
1190}
1191
1192// static
1193status_t M3UParser::ParseInt32(const char *s, int32_t *x) {
1194    char *end;
1195    long lval = strtol(s, &end, 10);
1196
1197    if (end == s || (*end != '\0' && *end != ',')) {
1198        return ERROR_MALFORMED;
1199    }
1200
1201    *x = (int32_t)lval;
1202
1203    return OK;
1204}
1205
1206// static
1207status_t M3UParser::ParseDouble(const char *s, double *x) {
1208    char *end;
1209    double dval = strtod(s, &end);
1210
1211    if (end == s || (*end != '\0' && *end != ',')) {
1212        return ERROR_MALFORMED;
1213    }
1214
1215    *x = dval;
1216
1217    return OK;
1218}
1219
1220// static
1221bool M3UParser::isQuotedString(const AString &str) {
1222    if (str.size() < 2
1223            || str.c_str()[0] != '"'
1224            || str.c_str()[str.size() - 1] != '"') {
1225        return false;
1226    }
1227    return true;
1228}
1229
1230// static
1231AString M3UParser::unquoteString(const AString &str) {
1232     if (!isQuotedString(str)) {
1233         return str;
1234     }
1235     return AString(str, 1, str.size() - 2);
1236}
1237
1238// static
1239bool M3UParser::codecIsType(const AString &codec, const char *type) {
1240    if (codec.size() < 4) {
1241        return false;
1242    }
1243    const char *c = codec.c_str();
1244    switch (FOURCC(c[0], c[1], c[2], c[3])) {
1245        // List extracted from http://www.mp4ra.org/codecs.html
1246        case 'ac-3':
1247        case 'alac':
1248        case 'dra1':
1249        case 'dtsc':
1250        case 'dtse':
1251        case 'dtsh':
1252        case 'dtsl':
1253        case 'ec-3':
1254        case 'enca':
1255        case 'g719':
1256        case 'g726':
1257        case 'm4ae':
1258        case 'mlpa':
1259        case 'mp4a':
1260        case 'raw ':
1261        case 'samr':
1262        case 'sawb':
1263        case 'sawp':
1264        case 'sevc':
1265        case 'sqcp':
1266        case 'ssmv':
1267        case 'twos':
1268        case 'agsm':
1269        case 'alaw':
1270        case 'dvi ':
1271        case 'fl32':
1272        case 'fl64':
1273        case 'ima4':
1274        case 'in24':
1275        case 'in32':
1276        case 'lpcm':
1277        case 'Qclp':
1278        case 'QDM2':
1279        case 'QDMC':
1280        case 'ulaw':
1281        case 'vdva':
1282            return !strcmp("audio", type);
1283
1284        case 'avc1':
1285        case 'avc2':
1286        case 'avcp':
1287        case 'drac':
1288        case 'encv':
1289        case 'mjp2':
1290        case 'mp4v':
1291        case 'mvc1':
1292        case 'mvc2':
1293        case 'resv':
1294        case 's263':
1295        case 'svc1':
1296        case 'vc-1':
1297        case 'CFHD':
1298        case 'civd':
1299        case 'DV10':
1300        case 'dvh5':
1301        case 'dvh6':
1302        case 'dvhp':
1303        case 'DVOO':
1304        case 'DVOR':
1305        case 'DVTV':
1306        case 'DVVT':
1307        case 'flic':
1308        case 'gif ':
1309        case 'h261':
1310        case 'h263':
1311        case 'HD10':
1312        case 'jpeg':
1313        case 'M105':
1314        case 'mjpa':
1315        case 'mjpb':
1316        case 'png ':
1317        case 'PNTG':
1318        case 'rle ':
1319        case 'rpza':
1320        case 'Shr0':
1321        case 'Shr1':
1322        case 'Shr2':
1323        case 'Shr3':
1324        case 'Shr4':
1325        case 'SVQ1':
1326        case 'SVQ3':
1327        case 'tga ':
1328        case 'tiff':
1329        case 'WRLE':
1330            return !strcmp("video", type);
1331
1332        default:
1333            return false;
1334    }
1335}
1336
1337}  // namespace android
1338