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