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