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