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