M3UParser.cpp revision 309aa8bf5e4cd66fe988adf2654cac3fadc2a1c3
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/MediaDefs.h>
27#include <media/stagefright/MediaErrors.h>
28#include <media/stagefright/Utils.h>
29#include <media/mediaplayer.h>
30
31namespace android {
32
33struct M3UParser::MediaGroup : public RefBase {
34    enum Type {
35        TYPE_AUDIO,
36        TYPE_VIDEO,
37        TYPE_SUBS,
38    };
39
40    enum FlagBits {
41        FLAG_AUTOSELECT         = 1,
42        FLAG_DEFAULT            = 2,
43        FLAG_FORCED             = 4,
44        FLAG_HAS_LANGUAGE       = 8,
45        FLAG_HAS_URI            = 16,
46    };
47
48    MediaGroup(Type type);
49
50    Type type() const;
51
52    status_t addMedia(
53            const char *name,
54            const char *uri,
55            const char *language,
56            uint32_t flags);
57
58    bool getActiveURI(AString *uri) const;
59
60    void pickRandomMediaItems();
61    status_t selectTrack(size_t index, bool select);
62    size_t countTracks() const;
63    sp<AMessage> getTrackInfo(size_t index) const;
64
65protected:
66    virtual ~MediaGroup();
67
68private:
69    struct Media {
70        AString mName;
71        AString mURI;
72        AString mLanguage;
73        uint32_t mFlags;
74    };
75
76    Type mType;
77    Vector<Media> mMediaItems;
78
79    ssize_t mSelectedIndex;
80
81    DISALLOW_EVIL_CONSTRUCTORS(MediaGroup);
82};
83
84M3UParser::MediaGroup::MediaGroup(Type type)
85    : mType(type),
86      mSelectedIndex(-1) {
87}
88
89M3UParser::MediaGroup::~MediaGroup() {
90}
91
92M3UParser::MediaGroup::Type M3UParser::MediaGroup::type() const {
93    return mType;
94}
95
96status_t M3UParser::MediaGroup::addMedia(
97        const char *name,
98        const char *uri,
99        const char *language,
100        uint32_t flags) {
101    mMediaItems.push();
102    Media &item = mMediaItems.editItemAt(mMediaItems.size() - 1);
103
104    item.mName = name;
105
106    if (uri) {
107        item.mURI = uri;
108    }
109
110    if (language) {
111        item.mLanguage = language;
112    }
113
114    item.mFlags = flags;
115
116    return OK;
117}
118
119void M3UParser::MediaGroup::pickRandomMediaItems() {
120#if 1
121    switch (mType) {
122        case TYPE_AUDIO:
123        {
124            char value[PROPERTY_VALUE_MAX];
125            if (property_get("media.httplive.audio-index", value, NULL)) {
126                char *end;
127                mSelectedIndex = strtoul(value, &end, 10);
128                CHECK(end > value && *end == '\0');
129
130                if (mSelectedIndex >= (ssize_t)mMediaItems.size()) {
131                    mSelectedIndex = mMediaItems.size() - 1;
132                }
133            } else {
134                mSelectedIndex = 0;
135            }
136            break;
137        }
138
139        case TYPE_VIDEO:
140        {
141            mSelectedIndex = 0;
142            break;
143        }
144
145        case TYPE_SUBS:
146        {
147            mSelectedIndex = -1;
148            break;
149        }
150
151        default:
152            TRESPASS();
153    }
154#else
155    mSelectedIndex = (rand() * mMediaItems.size()) / RAND_MAX;
156#endif
157}
158
159status_t M3UParser::MediaGroup::selectTrack(size_t index, bool select) {
160    if (mType != TYPE_SUBS && mType != TYPE_AUDIO) {
161        ALOGE("only select subtitile/audio tracks for now!");
162        return INVALID_OPERATION;
163    }
164
165    if (select) {
166        if (index >= mMediaItems.size()) {
167            ALOGE("track %zu does not exist", index);
168            return INVALID_OPERATION;
169        }
170        if (mSelectedIndex == (ssize_t)index) {
171            ALOGE("track %zu already selected", index);
172            return BAD_VALUE;
173        }
174        ALOGV("selected track %zu", index);
175        mSelectedIndex = index;
176    } else {
177        if (mSelectedIndex != (ssize_t)index) {
178            ALOGE("track %zu is not selected", index);
179            return BAD_VALUE;
180        }
181        ALOGV("unselected track %zu", index);
182        mSelectedIndex = -1;
183    }
184
185    return OK;
186}
187
188size_t M3UParser::MediaGroup::countTracks() const {
189    return mMediaItems.size();
190}
191
192sp<AMessage> M3UParser::MediaGroup::getTrackInfo(size_t index) const {
193    if (index >= mMediaItems.size()) {
194        return NULL;
195    }
196
197    sp<AMessage> format = new AMessage();
198
199    int32_t trackType;
200    if (mType == TYPE_AUDIO) {
201        trackType = MEDIA_TRACK_TYPE_AUDIO;
202    } else if (mType == TYPE_VIDEO) {
203        trackType = MEDIA_TRACK_TYPE_VIDEO;
204    } else if (mType == TYPE_SUBS) {
205        trackType = MEDIA_TRACK_TYPE_SUBTITLE;
206    } else {
207        trackType = MEDIA_TRACK_TYPE_UNKNOWN;
208    }
209    format->setInt32("type", trackType);
210
211    const Media &item = mMediaItems.itemAt(index);
212    const char *lang = item.mLanguage.empty() ? "und" : item.mLanguage.c_str();
213    format->setString("language", lang);
214
215    if (mType == TYPE_SUBS) {
216        // TO-DO: pass in a MediaFormat instead
217        format->setString("mime", MEDIA_MIMETYPE_TEXT_VTT);
218        format->setInt32("auto", !!(item.mFlags & MediaGroup::FLAG_AUTOSELECT));
219        format->setInt32("default", !!(item.mFlags & MediaGroup::FLAG_DEFAULT));
220        format->setInt32("forced", !!(item.mFlags & MediaGroup::FLAG_FORCED));
221    }
222
223    return format;
224}
225
226bool M3UParser::MediaGroup::getActiveURI(AString *uri) const {
227    for (size_t i = 0; i < mMediaItems.size(); ++i) {
228        if (mSelectedIndex >= 0 && i == (size_t)mSelectedIndex) {
229            const Media &item = mMediaItems.itemAt(i);
230
231            *uri = item.mURI;
232            return true;
233        }
234    }
235
236    return false;
237}
238
239////////////////////////////////////////////////////////////////////////////////
240
241M3UParser::M3UParser(
242        const char *baseURI, const void *data, size_t size)
243    : mInitCheck(NO_INIT),
244      mBaseURI(baseURI),
245      mIsExtM3U(false),
246      mIsVariantPlaylist(false),
247      mIsComplete(false),
248      mIsEvent(false),
249      mDiscontinuitySeq(0),
250      mSelectedIndex(-1) {
251    mInitCheck = parse(data, size);
252}
253
254M3UParser::~M3UParser() {
255}
256
257status_t M3UParser::initCheck() const {
258    return mInitCheck;
259}
260
261bool M3UParser::isExtM3U() const {
262    return mIsExtM3U;
263}
264
265bool M3UParser::isVariantPlaylist() const {
266    return mIsVariantPlaylist;
267}
268
269bool M3UParser::isComplete() const {
270    return mIsComplete;
271}
272
273bool M3UParser::isEvent() const {
274    return mIsEvent;
275}
276
277size_t M3UParser::getDiscontinuitySeq() const {
278    return mDiscontinuitySeq;
279}
280
281sp<AMessage> M3UParser::meta() {
282    return mMeta;
283}
284
285size_t M3UParser::size() {
286    return mItems.size();
287}
288
289bool M3UParser::itemAt(size_t index, AString *uri, sp<AMessage> *meta) {
290    if (uri) {
291        uri->clear();
292    }
293
294    if (meta) {
295        *meta = NULL;
296    }
297
298    if (index >= mItems.size()) {
299        return false;
300    }
301
302    if (uri) {
303        *uri = mItems.itemAt(index).mURI;
304    }
305
306    if (meta) {
307        *meta = mItems.itemAt(index).mMeta;
308    }
309
310    return true;
311}
312
313void M3UParser::pickRandomMediaItems() {
314    for (size_t i = 0; i < mMediaGroups.size(); ++i) {
315        mMediaGroups.valueAt(i)->pickRandomMediaItems();
316    }
317}
318
319status_t M3UParser::selectTrack(size_t index, bool select) {
320    for (size_t i = 0, ii = index; i < mMediaGroups.size(); ++i) {
321        sp<MediaGroup> group = mMediaGroups.valueAt(i);
322        size_t tracks = group->countTracks();
323        if (ii < tracks) {
324            status_t err = group->selectTrack(ii, select);
325            if (err == OK) {
326                mSelectedIndex = select ? index : -1;
327            }
328            return err;
329        }
330        ii -= tracks;
331    }
332    return INVALID_OPERATION;
333}
334
335size_t M3UParser::getTrackCount() const {
336    size_t trackCount = 0;
337    for (size_t i = 0; i < mMediaGroups.size(); ++i) {
338        trackCount += mMediaGroups.valueAt(i)->countTracks();
339    }
340    return trackCount;
341}
342
343sp<AMessage> M3UParser::getTrackInfo(size_t index) const {
344    for (size_t i = 0, ii = index; i < mMediaGroups.size(); ++i) {
345        sp<MediaGroup> group = mMediaGroups.valueAt(i);
346        size_t tracks = group->countTracks();
347        if (ii < tracks) {
348            return group->getTrackInfo(ii);
349        }
350        ii -= tracks;
351    }
352    return NULL;
353}
354
355ssize_t M3UParser::getSelectedIndex() const {
356    return mSelectedIndex;
357}
358
359bool M3UParser::getTypeURI(size_t index, const char *key, AString *uri) const {
360    if (!mIsVariantPlaylist) {
361        *uri = mBaseURI;
362
363        // Assume media without any more specific attribute contains
364        // audio and video, but no subtitles.
365        return !strcmp("audio", key) || !strcmp("video", key);
366    }
367
368    CHECK_LT(index, mItems.size());
369
370    sp<AMessage> meta = mItems.itemAt(index).mMeta;
371
372    AString groupID;
373    if (!meta->findString(key, &groupID)) {
374        *uri = mItems.itemAt(index).mURI;
375
376        AString codecs;
377        if (!meta->findString("codecs", &codecs)) {
378            // Assume media without any more specific attribute contains
379            // audio and video, but no subtitles.
380            return !strcmp("audio", key) || !strcmp("video", key);
381        } else {
382            // Split the comma separated list of codecs.
383            size_t offset = 0;
384            ssize_t commaPos = -1;
385            codecs.append(',');
386            while ((commaPos = codecs.find(",", offset)) >= 0) {
387                AString codec(codecs, offset, commaPos - offset);
388                codec.trim();
389                // return true only if a codec of type `key` ("audio"/"video")
390                // is found.
391                if (codecIsType(codec, key)) {
392                    return true;
393                }
394                offset = commaPos + 1;
395            }
396            return false;
397        }
398    }
399
400    sp<MediaGroup> group = mMediaGroups.valueFor(groupID);
401    if (!group->getActiveURI(uri)) {
402        return false;
403    }
404
405    if ((*uri).empty()) {
406        *uri = mItems.itemAt(index).mURI;
407    }
408
409    return true;
410}
411
412static bool MakeURL(const char *baseURL, const char *url, AString *out) {
413    out->clear();
414
415    if (strncasecmp("http://", baseURL, 7)
416            && strncasecmp("https://", baseURL, 8)
417            && strncasecmp("file://", baseURL, 7)) {
418        // Base URL must be absolute
419        return false;
420    }
421    const size_t schemeEnd = (strstr(baseURL, "//") - baseURL) + 2;
422    CHECK(schemeEnd == 7 || schemeEnd == 8);
423
424    if (!strncasecmp("http://", url, 7) || !strncasecmp("https://", url, 8)) {
425        // "url" is already an absolute URL, ignore base URL.
426        out->setTo(url);
427
428        ALOGV("base:'%s', url:'%s' => '%s'", baseURL, url, out->c_str());
429
430        return true;
431    }
432
433    if (url[0] == '/') {
434        // URL is an absolute path.
435
436        char *protocolEnd = strstr(baseURL, "//") + 2;
437        char *pathStart = strchr(protocolEnd, '/');
438
439        if (pathStart != NULL) {
440            out->setTo(baseURL, pathStart - baseURL);
441        } else {
442            out->setTo(baseURL);
443        }
444
445        out->append(url);
446    } else {
447        // URL is a relative path
448
449        // Check for a possible query string
450        const char *qsPos = strchr(baseURL, '?');
451        size_t end;
452        if (qsPos != NULL) {
453            end = qsPos - baseURL;
454        } else {
455            end = strlen(baseURL);
456        }
457        // Check for the last slash before a potential query string
458        for (ssize_t pos = end - 1; pos >= 0; pos--) {
459            if (baseURL[pos] == '/') {
460                end = pos;
461                break;
462            }
463        }
464
465        // Check whether the found slash actually is part of the path
466        // and not part of the "http://".
467        if (end >= schemeEnd) {
468            out->setTo(baseURL, end);
469        } else {
470            out->setTo(baseURL);
471        }
472
473        out->append("/");
474        out->append(url);
475    }
476
477    ALOGV("base:'%s', url:'%s' => '%s'", baseURL, url, out->c_str());
478
479    return true;
480}
481
482status_t M3UParser::parse(const void *_data, size_t size) {
483    int32_t lineNo = 0;
484
485    sp<AMessage> itemMeta;
486
487    const char *data = (const char *)_data;
488    size_t offset = 0;
489    uint64_t segmentRangeOffset = 0;
490    while (offset < size) {
491        size_t offsetLF = offset;
492        while (offsetLF < size && data[offsetLF] != '\n') {
493            ++offsetLF;
494        }
495
496        AString line;
497        if (offsetLF > offset && data[offsetLF - 1] == '\r') {
498            line.setTo(&data[offset], offsetLF - offset - 1);
499        } else {
500            line.setTo(&data[offset], offsetLF - offset);
501        }
502
503        // ALOGI("#%s#", line.c_str());
504
505        if (line.empty()) {
506            offset = offsetLF + 1;
507            continue;
508        }
509
510        if (lineNo == 0 && line == "#EXTM3U") {
511            mIsExtM3U = true;
512        }
513
514        if (mIsExtM3U) {
515            status_t err = OK;
516
517            if (line.startsWith("#EXT-X-TARGETDURATION")) {
518                if (mIsVariantPlaylist) {
519                    return ERROR_MALFORMED;
520                }
521                err = parseMetaData(line, &mMeta, "target-duration");
522            } else if (line.startsWith("#EXT-X-MEDIA-SEQUENCE")) {
523                if (mIsVariantPlaylist) {
524                    return ERROR_MALFORMED;
525                }
526                err = parseMetaData(line, &mMeta, "media-sequence");
527            } else if (line.startsWith("#EXT-X-KEY")) {
528                if (mIsVariantPlaylist) {
529                    return ERROR_MALFORMED;
530                }
531                err = parseCipherInfo(line, &itemMeta, mBaseURI);
532            } else if (line.startsWith("#EXT-X-ENDLIST")) {
533                mIsComplete = true;
534            } else if (line.startsWith("#EXT-X-PLAYLIST-TYPE:EVENT")) {
535                mIsEvent = true;
536            } else if (line.startsWith("#EXTINF")) {
537                if (mIsVariantPlaylist) {
538                    return ERROR_MALFORMED;
539                }
540                err = parseMetaDataDuration(line, &itemMeta, "durationUs");
541            } else if (line.startsWith("#EXT-X-DISCONTINUITY")) {
542                if (mIsVariantPlaylist) {
543                    return ERROR_MALFORMED;
544                }
545                if (itemMeta == NULL) {
546                    itemMeta = new AMessage;
547                }
548                itemMeta->setInt32("discontinuity", true);
549            } else if (line.startsWith("#EXT-X-STREAM-INF")) {
550                if (mMeta != NULL) {
551                    return ERROR_MALFORMED;
552                }
553                mIsVariantPlaylist = true;
554                err = parseStreamInf(line, &itemMeta);
555            } else if (line.startsWith("#EXT-X-BYTERANGE")) {
556                if (mIsVariantPlaylist) {
557                    return ERROR_MALFORMED;
558                }
559
560                uint64_t length, offset;
561                err = parseByteRange(line, segmentRangeOffset, &length, &offset);
562
563                if (err == OK) {
564                    if (itemMeta == NULL) {
565                        itemMeta = new AMessage;
566                    }
567
568                    itemMeta->setInt64("range-offset", offset);
569                    itemMeta->setInt64("range-length", length);
570
571                    segmentRangeOffset = offset + length;
572                }
573            } else if (line.startsWith("#EXT-X-MEDIA")) {
574                err = parseMedia(line);
575            } else if (line.startsWith("#EXT-X-DISCONTINUITY-SEQUENCE")) {
576                size_t seq;
577                err = parseDiscontinuitySequence(line, &seq);
578                if (err == OK) {
579                    mDiscontinuitySeq = seq;
580                }
581            }
582
583            if (err != OK) {
584                return err;
585            }
586        }
587
588        if (!line.startsWith("#")) {
589            if (!mIsVariantPlaylist) {
590                int64_t durationUs;
591                if (itemMeta == NULL
592                        || !itemMeta->findInt64("durationUs", &durationUs)) {
593                    return ERROR_MALFORMED;
594                }
595            }
596
597            mItems.push();
598            Item *item = &mItems.editItemAt(mItems.size() - 1);
599
600            CHECK(MakeURL(mBaseURI.c_str(), line.c_str(), &item->mURI));
601
602            item->mMeta = itemMeta;
603
604            itemMeta.clear();
605        }
606
607        offset = offsetLF + 1;
608        ++lineNo;
609    }
610
611    return OK;
612}
613
614// static
615status_t M3UParser::parseMetaData(
616        const AString &line, sp<AMessage> *meta, const char *key) {
617    ssize_t colonPos = line.find(":");
618
619    if (colonPos < 0) {
620        return ERROR_MALFORMED;
621    }
622
623    int32_t x;
624    status_t err = ParseInt32(line.c_str() + colonPos + 1, &x);
625
626    if (err != OK) {
627        return err;
628    }
629
630    if (meta->get() == NULL) {
631        *meta = new AMessage;
632    }
633    (*meta)->setInt32(key, x);
634
635    return OK;
636}
637
638// static
639status_t M3UParser::parseMetaDataDuration(
640        const AString &line, sp<AMessage> *meta, const char *key) {
641    ssize_t colonPos = line.find(":");
642
643    if (colonPos < 0) {
644        return ERROR_MALFORMED;
645    }
646
647    double x;
648    status_t err = ParseDouble(line.c_str() + colonPos + 1, &x);
649
650    if (err != OK) {
651        return err;
652    }
653
654    if (meta->get() == NULL) {
655        *meta = new AMessage;
656    }
657    (*meta)->setInt64(key, (int64_t)(x * 1E6));
658
659    return OK;
660}
661
662// Find the next occurence of the character "what" at or after "offset",
663// but ignore occurences between quotation marks.
664// Return the index of the occurrence or -1 if not found.
665static ssize_t FindNextUnquoted(
666        const AString &line, char what, size_t offset) {
667    CHECK_NE((int)what, (int)'"');
668
669    bool quoted = false;
670    while (offset < line.size()) {
671        char c = line.c_str()[offset];
672
673        if (c == '"') {
674            quoted = !quoted;
675        } else if (c == what && !quoted) {
676            return offset;
677        }
678
679        ++offset;
680    }
681
682    return -1;
683}
684
685status_t M3UParser::parseStreamInf(
686        const AString &line, sp<AMessage> *meta) const {
687    ssize_t colonPos = line.find(":");
688
689    if (colonPos < 0) {
690        return ERROR_MALFORMED;
691    }
692
693    size_t offset = colonPos + 1;
694
695    while (offset < line.size()) {
696        ssize_t end = FindNextUnquoted(line, ',', offset);
697        if (end < 0) {
698            end = line.size();
699        }
700
701        AString attr(line, offset, end - offset);
702        attr.trim();
703
704        offset = end + 1;
705
706        ssize_t equalPos = attr.find("=");
707        if (equalPos < 0) {
708            continue;
709        }
710
711        AString key(attr, 0, equalPos);
712        key.trim();
713
714        AString val(attr, equalPos + 1, attr.size() - equalPos - 1);
715        val.trim();
716
717        ALOGV("key=%s value=%s", key.c_str(), val.c_str());
718
719        if (!strcasecmp("bandwidth", key.c_str())) {
720            const char *s = val.c_str();
721            char *end;
722            unsigned long x = strtoul(s, &end, 10);
723
724            if (end == s || *end != '\0') {
725                // malformed
726                continue;
727            }
728
729            if (meta->get() == NULL) {
730                *meta = new AMessage;
731            }
732            (*meta)->setInt32("bandwidth", x);
733        } else if (!strcasecmp("codecs", key.c_str())) {
734            if (!isQuotedString(val)) {
735                ALOGE("Expected quoted string for %s attribute, "
736                      "got '%s' instead.",
737                      key.c_str(), val.c_str());;
738
739                return ERROR_MALFORMED;
740            }
741
742            key.tolower();
743            const AString &codecs = unquoteString(val);
744            if (meta->get() == NULL) {
745                *meta = new AMessage;
746            }
747            (*meta)->setString(key.c_str(), codecs.c_str());
748        } else if (!strcasecmp("audio", key.c_str())
749                || !strcasecmp("video", key.c_str())
750                || !strcasecmp("subtitles", key.c_str())) {
751            if (!isQuotedString(val)) {
752                ALOGE("Expected quoted string for %s attribute, "
753                      "got '%s' instead.",
754                      key.c_str(), val.c_str());
755
756                return ERROR_MALFORMED;
757            }
758
759            const AString &groupID = unquoteString(val);
760            ssize_t groupIndex = mMediaGroups.indexOfKey(groupID);
761
762            if (groupIndex < 0) {
763                ALOGE("Undefined media group '%s' referenced in stream info.",
764                      groupID.c_str());
765
766                return ERROR_MALFORMED;
767            }
768
769            key.tolower();
770            if (meta->get() == NULL) {
771                *meta = new AMessage;
772            }
773            (*meta)->setString(key.c_str(), groupID.c_str());
774        }
775    }
776
777    return OK;
778}
779
780// static
781status_t M3UParser::parseCipherInfo(
782        const AString &line, sp<AMessage> *meta, const AString &baseURI) {
783    ssize_t colonPos = line.find(":");
784
785    if (colonPos < 0) {
786        return ERROR_MALFORMED;
787    }
788
789    size_t offset = colonPos + 1;
790
791    while (offset < line.size()) {
792        ssize_t end = FindNextUnquoted(line, ',', offset);
793        if (end < 0) {
794            end = line.size();
795        }
796
797        AString attr(line, offset, end - offset);
798        attr.trim();
799
800        offset = end + 1;
801
802        ssize_t equalPos = attr.find("=");
803        if (equalPos < 0) {
804            continue;
805        }
806
807        AString key(attr, 0, equalPos);
808        key.trim();
809
810        AString val(attr, equalPos + 1, attr.size() - equalPos - 1);
811        val.trim();
812
813        ALOGV("key=%s value=%s", key.c_str(), val.c_str());
814
815        key.tolower();
816
817        if (key == "method" || key == "uri" || key == "iv") {
818            if (meta->get() == NULL) {
819                *meta = new AMessage;
820            }
821
822            if (key == "uri") {
823                if (val.size() >= 2
824                        && val.c_str()[0] == '"'
825                        && val.c_str()[val.size() - 1] == '"') {
826                    // Remove surrounding quotes.
827                    AString tmp(val, 1, val.size() - 2);
828                    val = tmp;
829                }
830
831                AString absURI;
832                if (MakeURL(baseURI.c_str(), val.c_str(), &absURI)) {
833                    val = absURI;
834                } else {
835                    ALOGE("failed to make absolute url for %s.",
836                            uriDebugString(baseURI).c_str());
837                }
838            }
839
840            key.insert(AString("cipher-"), 0);
841
842            (*meta)->setString(key.c_str(), val.c_str(), val.size());
843        }
844    }
845
846    return OK;
847}
848
849// static
850status_t M3UParser::parseByteRange(
851        const AString &line, uint64_t curOffset,
852        uint64_t *length, uint64_t *offset) {
853    ssize_t colonPos = line.find(":");
854
855    if (colonPos < 0) {
856        return ERROR_MALFORMED;
857    }
858
859    ssize_t atPos = line.find("@", colonPos + 1);
860
861    AString lenStr;
862    if (atPos < 0) {
863        lenStr = AString(line, colonPos + 1, line.size() - colonPos - 1);
864    } else {
865        lenStr = AString(line, colonPos + 1, atPos - colonPos - 1);
866    }
867
868    lenStr.trim();
869
870    const char *s = lenStr.c_str();
871    char *end;
872    *length = strtoull(s, &end, 10);
873
874    if (s == end || *end != '\0') {
875        return ERROR_MALFORMED;
876    }
877
878    if (atPos >= 0) {
879        AString offStr = AString(line, atPos + 1, line.size() - atPos - 1);
880        offStr.trim();
881
882        const char *s = offStr.c_str();
883        *offset = strtoull(s, &end, 10);
884
885        if (s == end || *end != '\0') {
886            return ERROR_MALFORMED;
887        }
888    } else {
889        *offset = curOffset;
890    }
891
892    return OK;
893}
894
895status_t M3UParser::parseMedia(const AString &line) {
896    ssize_t colonPos = line.find(":");
897
898    if (colonPos < 0) {
899        return ERROR_MALFORMED;
900    }
901
902    bool haveGroupType = false;
903    MediaGroup::Type groupType = MediaGroup::TYPE_AUDIO;
904
905    bool haveGroupID = false;
906    AString groupID;
907
908    bool haveGroupLanguage = false;
909    AString groupLanguage;
910
911    bool haveGroupName = false;
912    AString groupName;
913
914    bool haveGroupAutoselect = false;
915    bool groupAutoselect = false;
916
917    bool haveGroupDefault = false;
918    bool groupDefault = false;
919
920    bool haveGroupForced = false;
921    bool groupForced = false;
922
923    bool haveGroupURI = false;
924    AString groupURI;
925
926    size_t offset = colonPos + 1;
927
928    while (offset < line.size()) {
929        ssize_t end = FindNextUnquoted(line, ',', offset);
930        if (end < 0) {
931            end = line.size();
932        }
933
934        AString attr(line, offset, end - offset);
935        attr.trim();
936
937        offset = end + 1;
938
939        ssize_t equalPos = attr.find("=");
940        if (equalPos < 0) {
941            continue;
942        }
943
944        AString key(attr, 0, equalPos);
945        key.trim();
946
947        AString val(attr, equalPos + 1, attr.size() - equalPos - 1);
948        val.trim();
949
950        ALOGV("key=%s value=%s", key.c_str(), val.c_str());
951
952        if (!strcasecmp("type", key.c_str())) {
953            if (!strcasecmp("subtitles", val.c_str())) {
954                groupType = MediaGroup::TYPE_SUBS;
955            } else if (!strcasecmp("audio", val.c_str())) {
956                groupType = MediaGroup::TYPE_AUDIO;
957            } else if (!strcasecmp("video", val.c_str())) {
958                groupType = MediaGroup::TYPE_VIDEO;
959            } else {
960                ALOGE("Invalid media group type '%s'", val.c_str());
961                return ERROR_MALFORMED;
962            }
963
964            haveGroupType = true;
965        } else if (!strcasecmp("group-id", key.c_str())) {
966            if (val.size() < 2
967                    || val.c_str()[0] != '"'
968                    || val.c_str()[val.size() - 1] != '"') {
969                ALOGE("Expected quoted string for GROUP-ID, got '%s' instead.",
970                      val.c_str());
971
972                return ERROR_MALFORMED;
973            }
974
975            groupID.setTo(val, 1, val.size() - 2);
976            haveGroupID = true;
977        } else if (!strcasecmp("language", key.c_str())) {
978            if (val.size() < 2
979                    || val.c_str()[0] != '"'
980                    || val.c_str()[val.size() - 1] != '"') {
981                ALOGE("Expected quoted string for LANGUAGE, got '%s' instead.",
982                      val.c_str());
983
984                return ERROR_MALFORMED;
985            }
986
987            groupLanguage.setTo(val, 1, val.size() - 2);
988            haveGroupLanguage = true;
989        } else if (!strcasecmp("name", key.c_str())) {
990            if (val.size() < 2
991                    || val.c_str()[0] != '"'
992                    || val.c_str()[val.size() - 1] != '"') {
993                ALOGE("Expected quoted string for NAME, got '%s' instead.",
994                      val.c_str());
995
996                return ERROR_MALFORMED;
997            }
998
999            groupName.setTo(val, 1, val.size() - 2);
1000            haveGroupName = true;
1001        } else if (!strcasecmp("autoselect", key.c_str())) {
1002            groupAutoselect = false;
1003            if (!strcasecmp("YES", val.c_str())) {
1004                groupAutoselect = true;
1005            } else if (!strcasecmp("NO", val.c_str())) {
1006                groupAutoselect = false;
1007            } else {
1008                ALOGE("Expected YES or NO for AUTOSELECT attribute, "
1009                      "got '%s' instead.",
1010                      val.c_str());
1011
1012                return ERROR_MALFORMED;
1013            }
1014
1015            haveGroupAutoselect = true;
1016        } else if (!strcasecmp("default", key.c_str())) {
1017            groupDefault = false;
1018            if (!strcasecmp("YES", val.c_str())) {
1019                groupDefault = true;
1020            } else if (!strcasecmp("NO", val.c_str())) {
1021                groupDefault = false;
1022            } else {
1023                ALOGE("Expected YES or NO for DEFAULT attribute, "
1024                      "got '%s' instead.",
1025                      val.c_str());
1026
1027                return ERROR_MALFORMED;
1028            }
1029
1030            haveGroupDefault = true;
1031        } else if (!strcasecmp("forced", key.c_str())) {
1032            groupForced = false;
1033            if (!strcasecmp("YES", val.c_str())) {
1034                groupForced = true;
1035            } else if (!strcasecmp("NO", val.c_str())) {
1036                groupForced = false;
1037            } else {
1038                ALOGE("Expected YES or NO for FORCED attribute, "
1039                      "got '%s' instead.",
1040                      val.c_str());
1041
1042                return ERROR_MALFORMED;
1043            }
1044
1045            haveGroupForced = true;
1046        } else if (!strcasecmp("uri", key.c_str())) {
1047            if (val.size() < 2
1048                    || val.c_str()[0] != '"'
1049                    || val.c_str()[val.size() - 1] != '"') {
1050                ALOGE("Expected quoted string for URI, got '%s' instead.",
1051                      val.c_str());
1052
1053                return ERROR_MALFORMED;
1054            }
1055
1056            AString tmp(val, 1, val.size() - 2);
1057
1058            if (!MakeURL(mBaseURI.c_str(), tmp.c_str(), &groupURI)) {
1059                ALOGI("Failed to make absolute URI from '%s'.", tmp.c_str());
1060            }
1061
1062            haveGroupURI = true;
1063        }
1064    }
1065
1066    if (!haveGroupType || !haveGroupID || !haveGroupName) {
1067        ALOGE("Incomplete EXT-X-MEDIA element.");
1068        return ERROR_MALFORMED;
1069    }
1070
1071    uint32_t flags = 0;
1072    if (haveGroupAutoselect && groupAutoselect) {
1073        flags |= MediaGroup::FLAG_AUTOSELECT;
1074    }
1075    if (haveGroupDefault && groupDefault) {
1076        flags |= MediaGroup::FLAG_DEFAULT;
1077    }
1078    if (haveGroupForced) {
1079        if (groupType != MediaGroup::TYPE_SUBS) {
1080            ALOGE("The FORCED attribute MUST not be present on anything "
1081                  "but SUBS media.");
1082
1083            return ERROR_MALFORMED;
1084        }
1085
1086        if (groupForced) {
1087            flags |= MediaGroup::FLAG_FORCED;
1088        }
1089    }
1090    if (haveGroupLanguage) {
1091        flags |= MediaGroup::FLAG_HAS_LANGUAGE;
1092    }
1093    if (haveGroupURI) {
1094        flags |= MediaGroup::FLAG_HAS_URI;
1095    }
1096
1097    ssize_t groupIndex = mMediaGroups.indexOfKey(groupID);
1098    sp<MediaGroup> group;
1099
1100    if (groupIndex < 0) {
1101        group = new MediaGroup(groupType);
1102        mMediaGroups.add(groupID, group);
1103    } else {
1104        group = mMediaGroups.valueAt(groupIndex);
1105
1106        if (group->type() != groupType) {
1107            ALOGE("Attempt to put media item under group of different type "
1108                  "(groupType = %d, item type = %d",
1109                  group->type(),
1110                  groupType);
1111
1112            return ERROR_MALFORMED;
1113        }
1114    }
1115
1116    return group->addMedia(
1117            groupName.c_str(),
1118            haveGroupURI ? groupURI.c_str() : NULL,
1119            haveGroupLanguage ? groupLanguage.c_str() : NULL,
1120            flags);
1121}
1122
1123// static
1124status_t M3UParser::parseDiscontinuitySequence(const AString &line, size_t *seq) {
1125    ssize_t colonPos = line.find(":");
1126
1127    if (colonPos < 0) {
1128        return ERROR_MALFORMED;
1129    }
1130
1131    int32_t x;
1132    status_t err = ParseInt32(line.c_str() + colonPos + 1, &x);
1133    if (err != OK) {
1134        return err;
1135    }
1136
1137    if (x < 0) {
1138        return ERROR_MALFORMED;
1139    }
1140
1141    if (seq) {
1142        *seq = x;
1143    }
1144    return OK;
1145}
1146
1147// static
1148status_t M3UParser::ParseInt32(const char *s, int32_t *x) {
1149    char *end;
1150    long lval = strtol(s, &end, 10);
1151
1152    if (end == s || (*end != '\0' && *end != ',')) {
1153        return ERROR_MALFORMED;
1154    }
1155
1156    *x = (int32_t)lval;
1157
1158    return OK;
1159}
1160
1161// static
1162status_t M3UParser::ParseDouble(const char *s, double *x) {
1163    char *end;
1164    double dval = strtod(s, &end);
1165
1166    if (end == s || (*end != '\0' && *end != ',')) {
1167        return ERROR_MALFORMED;
1168    }
1169
1170    *x = dval;
1171
1172    return OK;
1173}
1174
1175// static
1176bool M3UParser::isQuotedString(const AString &str) {
1177    if (str.size() < 2
1178            || str.c_str()[0] != '"'
1179            || str.c_str()[str.size() - 1] != '"') {
1180        return false;
1181    }
1182    return true;
1183}
1184
1185// static
1186AString M3UParser::unquoteString(const AString &str) {
1187     if (!isQuotedString(str)) {
1188         return str;
1189     }
1190     return AString(str, 1, str.size() - 2);
1191}
1192
1193// static
1194bool M3UParser::codecIsType(const AString &codec, const char *type) {
1195    if (codec.size() < 4) {
1196        return false;
1197    }
1198    const char *c = codec.c_str();
1199    switch (FOURCC(c[0], c[1], c[2], c[3])) {
1200        // List extracted from http://www.mp4ra.org/codecs.html
1201        case 'ac-3':
1202        case 'alac':
1203        case 'dra1':
1204        case 'dtsc':
1205        case 'dtse':
1206        case 'dtsh':
1207        case 'dtsl':
1208        case 'ec-3':
1209        case 'enca':
1210        case 'g719':
1211        case 'g726':
1212        case 'm4ae':
1213        case 'mlpa':
1214        case 'mp4a':
1215        case 'raw ':
1216        case 'samr':
1217        case 'sawb':
1218        case 'sawp':
1219        case 'sevc':
1220        case 'sqcp':
1221        case 'ssmv':
1222        case 'twos':
1223        case 'agsm':
1224        case 'alaw':
1225        case 'dvi ':
1226        case 'fl32':
1227        case 'fl64':
1228        case 'ima4':
1229        case 'in24':
1230        case 'in32':
1231        case 'lpcm':
1232        case 'Qclp':
1233        case 'QDM2':
1234        case 'QDMC':
1235        case 'ulaw':
1236        case 'vdva':
1237            return !strcmp("audio", type);
1238
1239        case 'avc1':
1240        case 'avc2':
1241        case 'avcp':
1242        case 'drac':
1243        case 'encv':
1244        case 'mjp2':
1245        case 'mp4v':
1246        case 'mvc1':
1247        case 'mvc2':
1248        case 'resv':
1249        case 's263':
1250        case 'svc1':
1251        case 'vc-1':
1252        case 'CFHD':
1253        case 'civd':
1254        case 'DV10':
1255        case 'dvh5':
1256        case 'dvh6':
1257        case 'dvhp':
1258        case 'DVOO':
1259        case 'DVOR':
1260        case 'DVTV':
1261        case 'DVVT':
1262        case 'flic':
1263        case 'gif ':
1264        case 'h261':
1265        case 'h263':
1266        case 'HD10':
1267        case 'jpeg':
1268        case 'M105':
1269        case 'mjpa':
1270        case 'mjpb':
1271        case 'png ':
1272        case 'PNTG':
1273        case 'rle ':
1274        case 'rpza':
1275        case 'Shr0':
1276        case 'Shr1':
1277        case 'Shr2':
1278        case 'Shr3':
1279        case 'Shr4':
1280        case 'SVQ1':
1281        case 'SVQ3':
1282        case 'tga ':
1283        case 'tiff':
1284        case 'WRLE':
1285            return !strcmp("video", type);
1286
1287        default:
1288            return false;
1289    }
1290}
1291
1292}  // namespace android
1293