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