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