M3UParser.cpp revision d411b4ca2945cd8974a3a78199fce94646950128
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 == 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 <URL suppressed>.");
784                }
785            }
786
787            key.insert(AString("cipher-"), 0);
788
789            (*meta)->setString(key.c_str(), val.c_str(), val.size());
790        }
791    }
792
793    return OK;
794}
795
796// static
797status_t M3UParser::parseByteRange(
798        const AString &line, uint64_t curOffset,
799        uint64_t *length, uint64_t *offset) {
800    ssize_t colonPos = line.find(":");
801
802    if (colonPos < 0) {
803        return ERROR_MALFORMED;
804    }
805
806    ssize_t atPos = line.find("@", colonPos + 1);
807
808    AString lenStr;
809    if (atPos < 0) {
810        lenStr = AString(line, colonPos + 1, line.size() - colonPos - 1);
811    } else {
812        lenStr = AString(line, colonPos + 1, atPos - colonPos - 1);
813    }
814
815    lenStr.trim();
816
817    const char *s = lenStr.c_str();
818    char *end;
819    *length = strtoull(s, &end, 10);
820
821    if (s == end || *end != '\0') {
822        return ERROR_MALFORMED;
823    }
824
825    if (atPos >= 0) {
826        AString offStr = AString(line, atPos + 1, line.size() - atPos - 1);
827        offStr.trim();
828
829        const char *s = offStr.c_str();
830        *offset = strtoull(s, &end, 10);
831
832        if (s == end || *end != '\0') {
833            return ERROR_MALFORMED;
834        }
835    } else {
836        *offset = curOffset;
837    }
838
839    return OK;
840}
841
842status_t M3UParser::parseMedia(const AString &line) {
843    ssize_t colonPos = line.find(":");
844
845    if (colonPos < 0) {
846        return ERROR_MALFORMED;
847    }
848
849    bool haveGroupType = false;
850    MediaGroup::Type groupType = MediaGroup::TYPE_AUDIO;
851
852    bool haveGroupID = false;
853    AString groupID;
854
855    bool haveGroupLanguage = false;
856    AString groupLanguage;
857
858    bool haveGroupName = false;
859    AString groupName;
860
861    bool haveGroupAutoselect = false;
862    bool groupAutoselect = false;
863
864    bool haveGroupDefault = false;
865    bool groupDefault = false;
866
867    bool haveGroupForced = false;
868    bool groupForced = false;
869
870    bool haveGroupURI = false;
871    AString groupURI;
872
873    size_t offset = colonPos + 1;
874
875    while (offset < line.size()) {
876        ssize_t end = FindNextUnquoted(line, ',', offset);
877        if (end < 0) {
878            end = line.size();
879        }
880
881        AString attr(line, offset, end - offset);
882        attr.trim();
883
884        offset = end + 1;
885
886        ssize_t equalPos = attr.find("=");
887        if (equalPos < 0) {
888            continue;
889        }
890
891        AString key(attr, 0, equalPos);
892        key.trim();
893
894        AString val(attr, equalPos + 1, attr.size() - equalPos - 1);
895        val.trim();
896
897        ALOGV("key=%s value=%s", key.c_str(), val.c_str());
898
899        if (!strcasecmp("type", key.c_str())) {
900            if (!strcasecmp("subtitles", val.c_str())) {
901                groupType = MediaGroup::TYPE_SUBS;
902            } else if (!strcasecmp("audio", val.c_str())) {
903                groupType = MediaGroup::TYPE_AUDIO;
904            } else if (!strcasecmp("video", val.c_str())) {
905                groupType = MediaGroup::TYPE_VIDEO;
906            } else {
907                ALOGE("Invalid media group type '%s'", val.c_str());
908                return ERROR_MALFORMED;
909            }
910
911            haveGroupType = true;
912        } else if (!strcasecmp("group-id", key.c_str())) {
913            if (val.size() < 2
914                    || val.c_str()[0] != '"'
915                    || val.c_str()[val.size() - 1] != '"') {
916                ALOGE("Expected quoted string for GROUP-ID, got '%s' instead.",
917                      val.c_str());
918
919                return ERROR_MALFORMED;
920            }
921
922            groupID.setTo(val, 1, val.size() - 2);
923            haveGroupID = true;
924        } else if (!strcasecmp("language", key.c_str())) {
925            if (val.size() < 2
926                    || val.c_str()[0] != '"'
927                    || val.c_str()[val.size() - 1] != '"') {
928                ALOGE("Expected quoted string for LANGUAGE, got '%s' instead.",
929                      val.c_str());
930
931                return ERROR_MALFORMED;
932            }
933
934            groupLanguage.setTo(val, 1, val.size() - 2);
935            haveGroupLanguage = true;
936        } else if (!strcasecmp("name", key.c_str())) {
937            if (val.size() < 2
938                    || val.c_str()[0] != '"'
939                    || val.c_str()[val.size() - 1] != '"') {
940                ALOGE("Expected quoted string for NAME, got '%s' instead.",
941                      val.c_str());
942
943                return ERROR_MALFORMED;
944            }
945
946            groupName.setTo(val, 1, val.size() - 2);
947            haveGroupName = true;
948        } else if (!strcasecmp("autoselect", key.c_str())) {
949            groupAutoselect = false;
950            if (!strcasecmp("YES", val.c_str())) {
951                groupAutoselect = true;
952            } else if (!strcasecmp("NO", val.c_str())) {
953                groupAutoselect = false;
954            } else {
955                ALOGE("Expected YES or NO for AUTOSELECT attribute, "
956                      "got '%s' instead.",
957                      val.c_str());
958
959                return ERROR_MALFORMED;
960            }
961
962            haveGroupAutoselect = true;
963        } else if (!strcasecmp("default", key.c_str())) {
964            groupDefault = false;
965            if (!strcasecmp("YES", val.c_str())) {
966                groupDefault = true;
967            } else if (!strcasecmp("NO", val.c_str())) {
968                groupDefault = false;
969            } else {
970                ALOGE("Expected YES or NO for DEFAULT attribute, "
971                      "got '%s' instead.",
972                      val.c_str());
973
974                return ERROR_MALFORMED;
975            }
976
977            haveGroupDefault = true;
978        } else if (!strcasecmp("forced", key.c_str())) {
979            groupForced = false;
980            if (!strcasecmp("YES", val.c_str())) {
981                groupForced = true;
982            } else if (!strcasecmp("NO", val.c_str())) {
983                groupForced = false;
984            } else {
985                ALOGE("Expected YES or NO for FORCED attribute, "
986                      "got '%s' instead.",
987                      val.c_str());
988
989                return ERROR_MALFORMED;
990            }
991
992            haveGroupForced = true;
993        } else if (!strcasecmp("uri", key.c_str())) {
994            if (val.size() < 2
995                    || val.c_str()[0] != '"'
996                    || val.c_str()[val.size() - 1] != '"') {
997                ALOGE("Expected quoted string for URI, got '%s' instead.",
998                      val.c_str());
999
1000                return ERROR_MALFORMED;
1001            }
1002
1003            AString tmp(val, 1, val.size() - 2);
1004
1005            if (!MakeURL(mBaseURI.c_str(), tmp.c_str(), &groupURI)) {
1006                ALOGI("Failed to make absolute URI from '%s'.", tmp.c_str());
1007            }
1008
1009            haveGroupURI = true;
1010        }
1011    }
1012
1013    if (!haveGroupType || !haveGroupID || !haveGroupName) {
1014        ALOGE("Incomplete EXT-X-MEDIA element.");
1015        return ERROR_MALFORMED;
1016    }
1017
1018    uint32_t flags = 0;
1019    if (haveGroupAutoselect && groupAutoselect) {
1020        flags |= MediaGroup::FLAG_AUTOSELECT;
1021    }
1022    if (haveGroupDefault && groupDefault) {
1023        flags |= MediaGroup::FLAG_DEFAULT;
1024    }
1025    if (haveGroupForced) {
1026        if (groupType != MediaGroup::TYPE_SUBS) {
1027            ALOGE("The FORCED attribute MUST not be present on anything "
1028                  "but SUBS media.");
1029
1030            return ERROR_MALFORMED;
1031        }
1032
1033        if (groupForced) {
1034            flags |= MediaGroup::FLAG_FORCED;
1035        }
1036    }
1037    if (haveGroupLanguage) {
1038        flags |= MediaGroup::FLAG_HAS_LANGUAGE;
1039    }
1040    if (haveGroupURI) {
1041        flags |= MediaGroup::FLAG_HAS_URI;
1042    }
1043
1044    ssize_t groupIndex = mMediaGroups.indexOfKey(groupID);
1045    sp<MediaGroup> group;
1046
1047    if (groupIndex < 0) {
1048        group = new MediaGroup(groupType);
1049        mMediaGroups.add(groupID, group);
1050    } else {
1051        group = mMediaGroups.valueAt(groupIndex);
1052
1053        if (group->type() != groupType) {
1054            ALOGE("Attempt to put media item under group of different type "
1055                  "(groupType = %d, item type = %d",
1056                  group->type(),
1057                  groupType);
1058
1059            return ERROR_MALFORMED;
1060        }
1061    }
1062
1063    return group->addMedia(
1064            groupName.c_str(),
1065            haveGroupURI ? groupURI.c_str() : NULL,
1066            haveGroupLanguage ? groupLanguage.c_str() : NULL,
1067            flags);
1068}
1069
1070// static
1071status_t M3UParser::ParseInt32(const char *s, int32_t *x) {
1072    char *end;
1073    long lval = strtol(s, &end, 10);
1074
1075    if (end == s || (*end != '\0' && *end != ',')) {
1076        return ERROR_MALFORMED;
1077    }
1078
1079    *x = (int32_t)lval;
1080
1081    return OK;
1082}
1083
1084// static
1085status_t M3UParser::ParseDouble(const char *s, double *x) {
1086    char *end;
1087    double dval = strtod(s, &end);
1088
1089    if (end == s || (*end != '\0' && *end != ',')) {
1090        return ERROR_MALFORMED;
1091    }
1092
1093    *x = dval;
1094
1095    return OK;
1096}
1097
1098}  // namespace android
1099