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