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