M3UParser.cpp revision b4a7a2df4c28c3f32b5d877b54831d2cc5d78f81
1ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa/*
2ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa * Copyright (C) 2010 The Android Open Source Project
3ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa *
4ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa * Licensed under the Apache License, Version 2.0 (the "License");
5ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa * you may not use this file except in compliance with the License.
6ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa * You may obtain a copy of the License at
7ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa *
8ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa *      http://www.apache.org/licenses/LICENSE-2.0
9ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa *
10ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa * Unless required by applicable law or agreed to in writing, software
11ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa * distributed under the License is distributed on an "AS IS" BASIS,
12ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa * See the License for the specific language governing permissions and
14ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa * limitations under the License.
15ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa */
16ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa
17ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa//#define LOG_NDEBUG 0
18ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa#define LOG_TAG "M3UParser"
19ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa#include <utils/Log.h>
20ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa
216568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa#include "M3UParser.h"
22ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa#include <binder/Parcel.h>
236568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa#include <cutils/properties.h>
24ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa#include <media/stagefright/foundation/ADebug.h>
25ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa#include <media/stagefright/foundation/AMessage.h>
26ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa#include <media/stagefright/MediaErrors.h>
27ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa#include <media/stagefright/Utils.h>
28ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa#include <media/mediaplayer.h>
29ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa
30ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaanamespace android {
31ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa
32ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaastruct M3UParser::MediaGroup : public RefBase {
33ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa    enum Type {
346568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa        TYPE_AUDIO,
356568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa        TYPE_VIDEO,
366568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa        TYPE_SUBS,
376568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa    };
384b3e3931270f8e406fc806bc7fa1c2788256687ddestradaa
39ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa    enum FlagBits {
40ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa        FLAG_AUTOSELECT         = 1,
41ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa        FLAG_DEFAULT            = 2,
42ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa        FLAG_FORCED             = 4,
43ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa        FLAG_HAS_LANGUAGE       = 8,
44ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa        FLAG_HAS_URI            = 16,
45ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa    };
46ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa
47ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa    MediaGroup(Type type);
48ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa
49ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa    Type type() const;
506568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa
516568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa    status_t addMedia(
526568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa            const char *name,
536568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa            const char *uri,
546568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa            const char *language,
556568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa            uint32_t flags);
566568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa
576568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa    bool getActiveURI(AString *uri) const;
586568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa
596568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa    void pickRandomMediaItems();
606568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa    status_t selectTrack(size_t index, bool select);
616568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa    void getTrackInfo(Parcel* reply) const;
626568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa    size_t countTracks() const;
636568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa
646568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaaprotected:
656568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa    virtual ~MediaGroup();
666568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa
676568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaaprivate:
686568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa    struct Media {
696568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa        AString mName;
706568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa        AString mURI;
716568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa        AString mLanguage;
726568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa        uint32_t mFlags;
736568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa    };
746568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa
756568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa    Type mType;
766568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa    Vector<Media> mMediaItems;
776568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa
786568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa    ssize_t mSelectedIndex;
796568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa
806568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa    DISALLOW_EVIL_CONSTRUCTORS(MediaGroup);
816568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa};
826568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa
836568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaaM3UParser::MediaGroup::MediaGroup(Type type)
846568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa    : mType(type),
856568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa      mSelectedIndex(-1) {
866568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa}
876568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa
886568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaaM3UParser::MediaGroup::~MediaGroup() {
896568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa}
906568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa
916568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaaM3UParser::MediaGroup::Type M3UParser::MediaGroup::type() const {
926568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa    return mType;
936568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa}
946568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa
956568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaastatus_t M3UParser::MediaGroup::addMedia(
966568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa        const char *name,
976568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa        const char *uri,
986568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa        const char *language,
996568d709e78d6ccaf256b7d0e4a19cdfb26deafbdestradaa        uint32_t flags) {
100ea8a8a6076f04360de2d25b3e5853cde8026cd5fdestradaa    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 >= (ssize_t)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 %zu does not exist", index);
167            return INVALID_OPERATION;
168        }
169        if (mSelectedIndex == (ssize_t)index) {
170            ALOGE("track %zu already selected", index);
171            return BAD_VALUE;
172        }
173        ALOGV("selected track %zu", index);
174        mSelectedIndex = index;
175    } else {
176        if (mSelectedIndex != (ssize_t)index) {
177            ALOGE("track %zu is not selected", index);
178            return BAD_VALUE;
179        }
180        ALOGV("unselected track %zu", 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                codec.trim();
369                // return true only if a codec of type `key` ("audio"/"video")
370                // is found.
371                if (codecIsType(codec, key)) {
372                    return true;
373                }
374                offset = commaPos + 1;
375            }
376            return false;
377        }
378    }
379
380    sp<MediaGroup> group = mMediaGroups.valueFor(groupID);
381    if (!group->getActiveURI(uri)) {
382        return false;
383    }
384
385    if ((*uri).empty()) {
386        *uri = mItems.itemAt(index).mURI;
387    }
388
389    return true;
390}
391
392static bool MakeURL(const char *baseURL, const char *url, AString *out) {
393    out->clear();
394
395    if (strncasecmp("http://", baseURL, 7)
396            && strncasecmp("https://", baseURL, 8)
397            && strncasecmp("file://", baseURL, 7)) {
398        // Base URL must be absolute
399        return false;
400    }
401
402    if (!strncasecmp("http://", url, 7) || !strncasecmp("https://", url, 8)) {
403        // "url" is already an absolute URL, ignore base URL.
404        out->setTo(url);
405
406        ALOGV("base:'%s', url:'%s' => '%s'", baseURL, url, out->c_str());
407
408        return true;
409    }
410
411    if (url[0] == '/') {
412        // URL is an absolute path.
413
414        char *protocolEnd = strstr(baseURL, "//") + 2;
415        char *pathStart = strchr(protocolEnd, '/');
416
417        if (pathStart != NULL) {
418            out->setTo(baseURL, pathStart - baseURL);
419        } else {
420            out->setTo(baseURL);
421        }
422
423        out->append(url);
424    } else {
425        // URL is a relative path
426
427        // Check for a possible query string
428        const char *qsPos = strchr(baseURL, '?');
429        size_t end;
430        if (qsPos != NULL) {
431            end = qsPos - baseURL;
432        } else {
433            end = strlen(baseURL);
434        }
435        // Check for the last slash before a potential query string
436        for (ssize_t pos = end - 1; pos >= 0; pos--) {
437            if (baseURL[pos] == '/') {
438                end = pos;
439                break;
440            }
441        }
442
443        // Check whether the found slash actually is part of the path
444        // and not part of the "http://".
445        if (end > 6) {
446            out->setTo(baseURL, end);
447        } else {
448            out->setTo(baseURL);
449        }
450
451        out->append("/");
452        out->append(url);
453    }
454
455    ALOGV("base:'%s', url:'%s' => '%s'", baseURL, url, out->c_str());
456
457    return true;
458}
459
460status_t M3UParser::parse(const void *_data, size_t size) {
461    int32_t lineNo = 0;
462
463    sp<AMessage> itemMeta;
464
465    const char *data = (const char *)_data;
466    size_t offset = 0;
467    uint64_t segmentRangeOffset = 0;
468    while (offset < size) {
469        size_t offsetLF = offset;
470        while (offsetLF < size && data[offsetLF] != '\n') {
471            ++offsetLF;
472        }
473
474        AString line;
475        if (offsetLF > offset && data[offsetLF - 1] == '\r') {
476            line.setTo(&data[offset], offsetLF - offset - 1);
477        } else {
478            line.setTo(&data[offset], offsetLF - offset);
479        }
480
481        // ALOGI("#%s#", line.c_str());
482
483        if (line.empty()) {
484            offset = offsetLF + 1;
485            continue;
486        }
487
488        if (lineNo == 0 && line == "#EXTM3U") {
489            mIsExtM3U = true;
490        }
491
492        if (mIsExtM3U) {
493            status_t err = OK;
494
495            if (line.startsWith("#EXT-X-TARGETDURATION")) {
496                if (mIsVariantPlaylist) {
497                    return ERROR_MALFORMED;
498                }
499                err = parseMetaData(line, &mMeta, "target-duration");
500            } else if (line.startsWith("#EXT-X-MEDIA-SEQUENCE")) {
501                if (mIsVariantPlaylist) {
502                    return ERROR_MALFORMED;
503                }
504                err = parseMetaData(line, &mMeta, "media-sequence");
505            } else if (line.startsWith("#EXT-X-KEY")) {
506                if (mIsVariantPlaylist) {
507                    return ERROR_MALFORMED;
508                }
509                err = parseCipherInfo(line, &itemMeta, mBaseURI);
510            } else if (line.startsWith("#EXT-X-ENDLIST")) {
511                mIsComplete = true;
512            } else if (line.startsWith("#EXT-X-PLAYLIST-TYPE:EVENT")) {
513                mIsEvent = true;
514            } else if (line.startsWith("#EXTINF")) {
515                if (mIsVariantPlaylist) {
516                    return ERROR_MALFORMED;
517                }
518                err = parseMetaDataDuration(line, &itemMeta, "durationUs");
519            } else if (line.startsWith("#EXT-X-DISCONTINUITY")) {
520                if (mIsVariantPlaylist) {
521                    return ERROR_MALFORMED;
522                }
523                if (itemMeta == NULL) {
524                    itemMeta = new AMessage;
525                }
526                itemMeta->setInt32("discontinuity", true);
527            } else if (line.startsWith("#EXT-X-STREAM-INF")) {
528                if (mMeta != NULL) {
529                    return ERROR_MALFORMED;
530                }
531                mIsVariantPlaylist = true;
532                err = parseStreamInf(line, &itemMeta);
533            } else if (line.startsWith("#EXT-X-BYTERANGE")) {
534                if (mIsVariantPlaylist) {
535                    return ERROR_MALFORMED;
536                }
537
538                uint64_t length, offset;
539                err = parseByteRange(line, segmentRangeOffset, &length, &offset);
540
541                if (err == OK) {
542                    if (itemMeta == NULL) {
543                        itemMeta = new AMessage;
544                    }
545
546                    itemMeta->setInt64("range-offset", offset);
547                    itemMeta->setInt64("range-length", length);
548
549                    segmentRangeOffset = offset + length;
550                }
551            } else if (line.startsWith("#EXT-X-MEDIA")) {
552                err = parseMedia(line);
553            }
554
555            if (err != OK) {
556                return err;
557            }
558        }
559
560        if (!line.startsWith("#")) {
561            if (!mIsVariantPlaylist) {
562                int64_t durationUs;
563                if (itemMeta == NULL
564                        || !itemMeta->findInt64("durationUs", &durationUs)) {
565                    return ERROR_MALFORMED;
566                }
567            }
568
569            mItems.push();
570            Item *item = &mItems.editItemAt(mItems.size() - 1);
571
572            CHECK(MakeURL(mBaseURI.c_str(), line.c_str(), &item->mURI));
573
574            item->mMeta = itemMeta;
575
576            itemMeta.clear();
577        }
578
579        offset = offsetLF + 1;
580        ++lineNo;
581    }
582
583    return OK;
584}
585
586// static
587status_t M3UParser::parseMetaData(
588        const AString &line, sp<AMessage> *meta, const char *key) {
589    ssize_t colonPos = line.find(":");
590
591    if (colonPos < 0) {
592        return ERROR_MALFORMED;
593    }
594
595    int32_t x;
596    status_t err = ParseInt32(line.c_str() + colonPos + 1, &x);
597
598    if (err != OK) {
599        return err;
600    }
601
602    if (meta->get() == NULL) {
603        *meta = new AMessage;
604    }
605    (*meta)->setInt32(key, x);
606
607    return OK;
608}
609
610// static
611status_t M3UParser::parseMetaDataDuration(
612        const AString &line, sp<AMessage> *meta, const char *key) {
613    ssize_t colonPos = line.find(":");
614
615    if (colonPos < 0) {
616        return ERROR_MALFORMED;
617    }
618
619    double x;
620    status_t err = ParseDouble(line.c_str() + colonPos + 1, &x);
621
622    if (err != OK) {
623        return err;
624    }
625
626    if (meta->get() == NULL) {
627        *meta = new AMessage;
628    }
629    (*meta)->setInt64(key, (int64_t)(x * 1E6));
630
631    return OK;
632}
633
634// Find the next occurence of the character "what" at or after "offset",
635// but ignore occurences between quotation marks.
636// Return the index of the occurrence or -1 if not found.
637static ssize_t FindNextUnquoted(
638        const AString &line, char what, size_t offset) {
639    CHECK_NE((int)what, (int)'"');
640
641    bool quoted = false;
642    while (offset < line.size()) {
643        char c = line.c_str()[offset];
644
645        if (c == '"') {
646            quoted = !quoted;
647        } else if (c == what && !quoted) {
648            return offset;
649        }
650
651        ++offset;
652    }
653
654    return -1;
655}
656
657status_t M3UParser::parseStreamInf(
658        const AString &line, sp<AMessage> *meta) const {
659    ssize_t colonPos = line.find(":");
660
661    if (colonPos < 0) {
662        return ERROR_MALFORMED;
663    }
664
665    size_t offset = colonPos + 1;
666
667    while (offset < line.size()) {
668        ssize_t end = FindNextUnquoted(line, ',', offset);
669        if (end < 0) {
670            end = line.size();
671        }
672
673        AString attr(line, offset, end - offset);
674        attr.trim();
675
676        offset = end + 1;
677
678        ssize_t equalPos = attr.find("=");
679        if (equalPos < 0) {
680            continue;
681        }
682
683        AString key(attr, 0, equalPos);
684        key.trim();
685
686        AString val(attr, equalPos + 1, attr.size() - equalPos - 1);
687        val.trim();
688
689        ALOGV("key=%s value=%s", key.c_str(), val.c_str());
690
691        if (!strcasecmp("bandwidth", key.c_str())) {
692            const char *s = val.c_str();
693            char *end;
694            unsigned long x = strtoul(s, &end, 10);
695
696            if (end == s || *end != '\0') {
697                // malformed
698                continue;
699            }
700
701            if (meta->get() == NULL) {
702                *meta = new AMessage;
703            }
704            (*meta)->setInt32("bandwidth", x);
705        } else if (!strcasecmp("codecs", key.c_str())) {
706            if (!isQuotedString(val)) {
707                ALOGE("Expected quoted string for %s attribute, "
708                      "got '%s' instead.",
709                      key.c_str(), val.c_str());;
710
711                return ERROR_MALFORMED;
712            }
713
714            key.tolower();
715            const AString &codecs = unquoteString(val);
716            (*meta)->setString(key.c_str(), codecs.c_str());
717        } else if (!strcasecmp("audio", key.c_str())
718                || !strcasecmp("video", key.c_str())
719                || !strcasecmp("subtitles", key.c_str())) {
720            if (!isQuotedString(val)) {
721                ALOGE("Expected quoted string for %s attribute, "
722                      "got '%s' instead.",
723                      key.c_str(), val.c_str());
724
725                return ERROR_MALFORMED;
726            }
727
728            const AString &groupID = unquoteString(val);
729            ssize_t groupIndex = mMediaGroups.indexOfKey(groupID);
730
731            if (groupIndex < 0) {
732                ALOGE("Undefined media group '%s' referenced in stream info.",
733                      groupID.c_str());
734
735                return ERROR_MALFORMED;
736            }
737
738            key.tolower();
739            (*meta)->setString(key.c_str(), groupID.c_str());
740        }
741    }
742
743    return OK;
744}
745
746// static
747status_t M3UParser::parseCipherInfo(
748        const AString &line, sp<AMessage> *meta, const AString &baseURI) {
749    ssize_t colonPos = line.find(":");
750
751    if (colonPos < 0) {
752        return ERROR_MALFORMED;
753    }
754
755    size_t offset = colonPos + 1;
756
757    while (offset < line.size()) {
758        ssize_t end = FindNextUnquoted(line, ',', offset);
759        if (end < 0) {
760            end = line.size();
761        }
762
763        AString attr(line, offset, end - offset);
764        attr.trim();
765
766        offset = end + 1;
767
768        ssize_t equalPos = attr.find("=");
769        if (equalPos < 0) {
770            continue;
771        }
772
773        AString key(attr, 0, equalPos);
774        key.trim();
775
776        AString val(attr, equalPos + 1, attr.size() - equalPos - 1);
777        val.trim();
778
779        ALOGV("key=%s value=%s", key.c_str(), val.c_str());
780
781        key.tolower();
782
783        if (key == "method" || key == "uri" || key == "iv") {
784            if (meta->get() == NULL) {
785                *meta = new AMessage;
786            }
787
788            if (key == "uri") {
789                if (val.size() >= 2
790                        && val.c_str()[0] == '"'
791                        && val.c_str()[val.size() - 1] == '"') {
792                    // Remove surrounding quotes.
793                    AString tmp(val, 1, val.size() - 2);
794                    val = tmp;
795                }
796
797                AString absURI;
798                if (MakeURL(baseURI.c_str(), val.c_str(), &absURI)) {
799                    val = absURI;
800                } else {
801                    ALOGE("failed to make absolute url for <URL suppressed>.");
802                }
803            }
804
805            key.insert(AString("cipher-"), 0);
806
807            (*meta)->setString(key.c_str(), val.c_str(), val.size());
808        }
809    }
810
811    return OK;
812}
813
814// static
815status_t M3UParser::parseByteRange(
816        const AString &line, uint64_t curOffset,
817        uint64_t *length, uint64_t *offset) {
818    ssize_t colonPos = line.find(":");
819
820    if (colonPos < 0) {
821        return ERROR_MALFORMED;
822    }
823
824    ssize_t atPos = line.find("@", colonPos + 1);
825
826    AString lenStr;
827    if (atPos < 0) {
828        lenStr = AString(line, colonPos + 1, line.size() - colonPos - 1);
829    } else {
830        lenStr = AString(line, colonPos + 1, atPos - colonPos - 1);
831    }
832
833    lenStr.trim();
834
835    const char *s = lenStr.c_str();
836    char *end;
837    *length = strtoull(s, &end, 10);
838
839    if (s == end || *end != '\0') {
840        return ERROR_MALFORMED;
841    }
842
843    if (atPos >= 0) {
844        AString offStr = AString(line, atPos + 1, line.size() - atPos - 1);
845        offStr.trim();
846
847        const char *s = offStr.c_str();
848        *offset = strtoull(s, &end, 10);
849
850        if (s == end || *end != '\0') {
851            return ERROR_MALFORMED;
852        }
853    } else {
854        *offset = curOffset;
855    }
856
857    return OK;
858}
859
860status_t M3UParser::parseMedia(const AString &line) {
861    ssize_t colonPos = line.find(":");
862
863    if (colonPos < 0) {
864        return ERROR_MALFORMED;
865    }
866
867    bool haveGroupType = false;
868    MediaGroup::Type groupType = MediaGroup::TYPE_AUDIO;
869
870    bool haveGroupID = false;
871    AString groupID;
872
873    bool haveGroupLanguage = false;
874    AString groupLanguage;
875
876    bool haveGroupName = false;
877    AString groupName;
878
879    bool haveGroupAutoselect = false;
880    bool groupAutoselect = false;
881
882    bool haveGroupDefault = false;
883    bool groupDefault = false;
884
885    bool haveGroupForced = false;
886    bool groupForced = false;
887
888    bool haveGroupURI = false;
889    AString groupURI;
890
891    size_t offset = colonPos + 1;
892
893    while (offset < line.size()) {
894        ssize_t end = FindNextUnquoted(line, ',', offset);
895        if (end < 0) {
896            end = line.size();
897        }
898
899        AString attr(line, offset, end - offset);
900        attr.trim();
901
902        offset = end + 1;
903
904        ssize_t equalPos = attr.find("=");
905        if (equalPos < 0) {
906            continue;
907        }
908
909        AString key(attr, 0, equalPos);
910        key.trim();
911
912        AString val(attr, equalPos + 1, attr.size() - equalPos - 1);
913        val.trim();
914
915        ALOGV("key=%s value=%s", key.c_str(), val.c_str());
916
917        if (!strcasecmp("type", key.c_str())) {
918            if (!strcasecmp("subtitles", val.c_str())) {
919                groupType = MediaGroup::TYPE_SUBS;
920            } else if (!strcasecmp("audio", val.c_str())) {
921                groupType = MediaGroup::TYPE_AUDIO;
922            } else if (!strcasecmp("video", val.c_str())) {
923                groupType = MediaGroup::TYPE_VIDEO;
924            } else {
925                ALOGE("Invalid media group type '%s'", val.c_str());
926                return ERROR_MALFORMED;
927            }
928
929            haveGroupType = true;
930        } else if (!strcasecmp("group-id", key.c_str())) {
931            if (val.size() < 2
932                    || val.c_str()[0] != '"'
933                    || val.c_str()[val.size() - 1] != '"') {
934                ALOGE("Expected quoted string for GROUP-ID, got '%s' instead.",
935                      val.c_str());
936
937                return ERROR_MALFORMED;
938            }
939
940            groupID.setTo(val, 1, val.size() - 2);
941            haveGroupID = true;
942        } else if (!strcasecmp("language", key.c_str())) {
943            if (val.size() < 2
944                    || val.c_str()[0] != '"'
945                    || val.c_str()[val.size() - 1] != '"') {
946                ALOGE("Expected quoted string for LANGUAGE, got '%s' instead.",
947                      val.c_str());
948
949                return ERROR_MALFORMED;
950            }
951
952            groupLanguage.setTo(val, 1, val.size() - 2);
953            haveGroupLanguage = true;
954        } else if (!strcasecmp("name", key.c_str())) {
955            if (val.size() < 2
956                    || val.c_str()[0] != '"'
957                    || val.c_str()[val.size() - 1] != '"') {
958                ALOGE("Expected quoted string for NAME, got '%s' instead.",
959                      val.c_str());
960
961                return ERROR_MALFORMED;
962            }
963
964            groupName.setTo(val, 1, val.size() - 2);
965            haveGroupName = true;
966        } else if (!strcasecmp("autoselect", key.c_str())) {
967            groupAutoselect = false;
968            if (!strcasecmp("YES", val.c_str())) {
969                groupAutoselect = true;
970            } else if (!strcasecmp("NO", val.c_str())) {
971                groupAutoselect = false;
972            } else {
973                ALOGE("Expected YES or NO for AUTOSELECT attribute, "
974                      "got '%s' instead.",
975                      val.c_str());
976
977                return ERROR_MALFORMED;
978            }
979
980            haveGroupAutoselect = true;
981        } else if (!strcasecmp("default", key.c_str())) {
982            groupDefault = false;
983            if (!strcasecmp("YES", val.c_str())) {
984                groupDefault = true;
985            } else if (!strcasecmp("NO", val.c_str())) {
986                groupDefault = false;
987            } else {
988                ALOGE("Expected YES or NO for DEFAULT attribute, "
989                      "got '%s' instead.",
990                      val.c_str());
991
992                return ERROR_MALFORMED;
993            }
994
995            haveGroupDefault = true;
996        } else if (!strcasecmp("forced", key.c_str())) {
997            groupForced = false;
998            if (!strcasecmp("YES", val.c_str())) {
999                groupForced = true;
1000            } else if (!strcasecmp("NO", val.c_str())) {
1001                groupForced = false;
1002            } else {
1003                ALOGE("Expected YES or NO for FORCED attribute, "
1004                      "got '%s' instead.",
1005                      val.c_str());
1006
1007                return ERROR_MALFORMED;
1008            }
1009
1010            haveGroupForced = true;
1011        } else if (!strcasecmp("uri", key.c_str())) {
1012            if (val.size() < 2
1013                    || val.c_str()[0] != '"'
1014                    || val.c_str()[val.size() - 1] != '"') {
1015                ALOGE("Expected quoted string for URI, got '%s' instead.",
1016                      val.c_str());
1017
1018                return ERROR_MALFORMED;
1019            }
1020
1021            AString tmp(val, 1, val.size() - 2);
1022
1023            if (!MakeURL(mBaseURI.c_str(), tmp.c_str(), &groupURI)) {
1024                ALOGI("Failed to make absolute URI from '%s'.", tmp.c_str());
1025            }
1026
1027            haveGroupURI = true;
1028        }
1029    }
1030
1031    if (!haveGroupType || !haveGroupID || !haveGroupName) {
1032        ALOGE("Incomplete EXT-X-MEDIA element.");
1033        return ERROR_MALFORMED;
1034    }
1035
1036    uint32_t flags = 0;
1037    if (haveGroupAutoselect && groupAutoselect) {
1038        flags |= MediaGroup::FLAG_AUTOSELECT;
1039    }
1040    if (haveGroupDefault && groupDefault) {
1041        flags |= MediaGroup::FLAG_DEFAULT;
1042    }
1043    if (haveGroupForced) {
1044        if (groupType != MediaGroup::TYPE_SUBS) {
1045            ALOGE("The FORCED attribute MUST not be present on anything "
1046                  "but SUBS media.");
1047
1048            return ERROR_MALFORMED;
1049        }
1050
1051        if (groupForced) {
1052            flags |= MediaGroup::FLAG_FORCED;
1053        }
1054    }
1055    if (haveGroupLanguage) {
1056        flags |= MediaGroup::FLAG_HAS_LANGUAGE;
1057    }
1058    if (haveGroupURI) {
1059        flags |= MediaGroup::FLAG_HAS_URI;
1060    }
1061
1062    ssize_t groupIndex = mMediaGroups.indexOfKey(groupID);
1063    sp<MediaGroup> group;
1064
1065    if (groupIndex < 0) {
1066        group = new MediaGroup(groupType);
1067        mMediaGroups.add(groupID, group);
1068    } else {
1069        group = mMediaGroups.valueAt(groupIndex);
1070
1071        if (group->type() != groupType) {
1072            ALOGE("Attempt to put media item under group of different type "
1073                  "(groupType = %d, item type = %d",
1074                  group->type(),
1075                  groupType);
1076
1077            return ERROR_MALFORMED;
1078        }
1079    }
1080
1081    return group->addMedia(
1082            groupName.c_str(),
1083            haveGroupURI ? groupURI.c_str() : NULL,
1084            haveGroupLanguage ? groupLanguage.c_str() : NULL,
1085            flags);
1086}
1087
1088// static
1089status_t M3UParser::ParseInt32(const char *s, int32_t *x) {
1090    char *end;
1091    long lval = strtol(s, &end, 10);
1092
1093    if (end == s || (*end != '\0' && *end != ',')) {
1094        return ERROR_MALFORMED;
1095    }
1096
1097    *x = (int32_t)lval;
1098
1099    return OK;
1100}
1101
1102// static
1103status_t M3UParser::ParseDouble(const char *s, double *x) {
1104    char *end;
1105    double dval = strtod(s, &end);
1106
1107    if (end == s || (*end != '\0' && *end != ',')) {
1108        return ERROR_MALFORMED;
1109    }
1110
1111    *x = dval;
1112
1113    return OK;
1114}
1115
1116// static
1117bool M3UParser::isQuotedString(const AString &str) {
1118    if (str.size() < 2
1119            || str.c_str()[0] != '"'
1120            || str.c_str()[str.size() - 1] != '"') {
1121        return false;
1122    }
1123    return true;
1124}
1125
1126// static
1127AString M3UParser::unquoteString(const AString &str) {
1128     if (!isQuotedString(str)) {
1129         return str;
1130     }
1131     return AString(str, 1, str.size() - 2);
1132}
1133
1134// static
1135bool M3UParser::codecIsType(const AString &codec, const char *type) {
1136    if (codec.size() < 4) {
1137        return false;
1138    }
1139    const char *c = codec.c_str();
1140    switch (FOURCC(c[0], c[1], c[2], c[3])) {
1141        // List extracted from http://www.mp4ra.org/codecs.html
1142        case 'ac-3':
1143        case 'alac':
1144        case 'dra1':
1145        case 'dtsc':
1146        case 'dtse':
1147        case 'dtsh':
1148        case 'dtsl':
1149        case 'ec-3':
1150        case 'enca':
1151        case 'g719':
1152        case 'g726':
1153        case 'm4ae':
1154        case 'mlpa':
1155        case 'mp4a':
1156        case 'raw ':
1157        case 'samr':
1158        case 'sawb':
1159        case 'sawp':
1160        case 'sevc':
1161        case 'sqcp':
1162        case 'ssmv':
1163        case 'twos':
1164        case 'agsm':
1165        case 'alaw':
1166        case 'dvi ':
1167        case 'fl32':
1168        case 'fl64':
1169        case 'ima4':
1170        case 'in24':
1171        case 'in32':
1172        case 'lpcm':
1173        case 'Qclp':
1174        case 'QDM2':
1175        case 'QDMC':
1176        case 'ulaw':
1177        case 'vdva':
1178            return !strcmp("audio", type);
1179
1180        case 'avc1':
1181        case 'avc2':
1182        case 'avcp':
1183        case 'drac':
1184        case 'encv':
1185        case 'mjp2':
1186        case 'mp4v':
1187        case 'mvc1':
1188        case 'mvc2':
1189        case 'resv':
1190        case 's263':
1191        case 'svc1':
1192        case 'vc-1':
1193        case 'CFHD':
1194        case 'civd':
1195        case 'DV10':
1196        case 'dvh5':
1197        case 'dvh6':
1198        case 'dvhp':
1199        case 'DVOO':
1200        case 'DVOR':
1201        case 'DVTV':
1202        case 'DVVT':
1203        case 'flic':
1204        case 'gif ':
1205        case 'h261':
1206        case 'h263':
1207        case 'HD10':
1208        case 'jpeg':
1209        case 'M105':
1210        case 'mjpa':
1211        case 'mjpb':
1212        case 'png ':
1213        case 'PNTG':
1214        case 'rle ':
1215        case 'rpza':
1216        case 'Shr0':
1217        case 'Shr1':
1218        case 'Shr2':
1219        case 'Shr3':
1220        case 'Shr4':
1221        case 'SVQ1':
1222        case 'SVQ3':
1223        case 'tga ':
1224        case 'tiff':
1225        case 'WRLE':
1226            return !strcmp("video", type);
1227
1228        default:
1229            return false;
1230    }
1231}
1232
1233}  // namespace android
1234