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