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