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);
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)938 status_t M3UParser::parseCipherInfo(
939 const AString &line, sp<AMessage> *meta) {
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 // To save space, we only store the partial Uri here
989 // The full Uri will be constructed from this plus
990 // the base Uri as needed by PlaylistFetcher
991 }
992
993 key.insert(AString("cipher-"), 0);
994
995 (*meta)->setString(key.c_str(), val.c_str(), val.size());
996 }
997 }
998
999 return OK;
1000 }
1001
getFullCipherUri(const AString & partial)1002 AString M3UParser::getFullCipherUri(const AString &partial) {
1003 AString full;
1004 if (MakeURL(mBaseURI.c_str(), partial.c_str(), &full)) {
1005 return full;
1006 }
1007 return AString("");
1008 }
1009
1010 // static
parseByteRange(const AString & line,uint64_t curOffset,uint64_t * length,uint64_t * offset)1011 status_t M3UParser::parseByteRange(
1012 const AString &line, uint64_t curOffset,
1013 uint64_t *length, uint64_t *offset) {
1014 ssize_t colonPos = line.find(":");
1015
1016 if (colonPos < 0) {
1017 return ERROR_MALFORMED;
1018 }
1019
1020 ssize_t atPos = line.find("@", colonPos + 1);
1021
1022 AString lenStr;
1023 if (atPos < 0) {
1024 lenStr = AString(line, colonPos + 1, line.size() - colonPos - 1);
1025 } else {
1026 lenStr = AString(line, colonPos + 1, atPos - colonPos - 1);
1027 }
1028
1029 lenStr.trim();
1030
1031 const char *s = lenStr.c_str();
1032 char *end;
1033 *length = strtoull(s, &end, 10);
1034
1035 if (s == end || *end != '\0') {
1036 return ERROR_MALFORMED;
1037 }
1038
1039 if (atPos >= 0) {
1040 AString offStr = AString(line, atPos + 1, line.size() - atPos - 1);
1041 offStr.trim();
1042
1043 const char *s = offStr.c_str();
1044 *offset = strtoull(s, &end, 10);
1045
1046 if (s == end || *end != '\0') {
1047 return ERROR_MALFORMED;
1048 }
1049 } else {
1050 *offset = curOffset;
1051 }
1052
1053 return OK;
1054 }
1055
parseMedia(const AString & line)1056 status_t M3UParser::parseMedia(const AString &line) {
1057 ssize_t colonPos = line.find(":");
1058
1059 if (colonPos < 0) {
1060 return ERROR_MALFORMED;
1061 }
1062
1063 bool haveGroupType = false;
1064 MediaGroup::Type groupType = MediaGroup::TYPE_AUDIO;
1065
1066 bool haveGroupID = false;
1067 AString groupID;
1068
1069 bool haveGroupLanguage = false;
1070 AString groupLanguage;
1071
1072 bool haveGroupName = false;
1073 AString groupName;
1074
1075 bool haveGroupAutoselect = false;
1076 bool groupAutoselect = false;
1077
1078 bool haveGroupDefault = false;
1079 bool groupDefault = false;
1080
1081 bool haveGroupForced = false;
1082 bool groupForced = false;
1083
1084 bool haveGroupURI = false;
1085 AString groupURI;
1086
1087 size_t offset = colonPos + 1;
1088
1089 while (offset < line.size()) {
1090 ssize_t end = FindNextUnquoted(line, ',', offset);
1091 if (end < 0) {
1092 end = line.size();
1093 }
1094
1095 AString attr(line, offset, end - offset);
1096 attr.trim();
1097
1098 offset = end + 1;
1099
1100 ssize_t equalPos = attr.find("=");
1101 if (equalPos < 0) {
1102 continue;
1103 }
1104
1105 AString key(attr, 0, equalPos);
1106 key.trim();
1107
1108 AString val(attr, equalPos + 1, attr.size() - equalPos - 1);
1109 val.trim();
1110
1111 ALOGV("key=%s value=%s", key.c_str(), val.c_str());
1112
1113 if (!strcasecmp("type", key.c_str())) {
1114 if (!strcasecmp("subtitles", val.c_str())) {
1115 groupType = MediaGroup::TYPE_SUBS;
1116 } else if (!strcasecmp("audio", val.c_str())) {
1117 groupType = MediaGroup::TYPE_AUDIO;
1118 } else if (!strcasecmp("video", val.c_str())) {
1119 groupType = MediaGroup::TYPE_VIDEO;
1120 } else if (!strcasecmp("closed-captions", val.c_str())){
1121 groupType = MediaGroup::TYPE_CC;
1122 } else {
1123 ALOGE("Invalid media group type '%s'", val.c_str());
1124 return ERROR_MALFORMED;
1125 }
1126
1127 haveGroupType = true;
1128 } else if (!strcasecmp("group-id", key.c_str())) {
1129 if (val.size() < 2
1130 || val.c_str()[0] != '"'
1131 || val.c_str()[val.size() - 1] != '"') {
1132 ALOGE("Expected quoted string for GROUP-ID, got '%s' instead.",
1133 val.c_str());
1134
1135 return ERROR_MALFORMED;
1136 }
1137
1138 groupID.setTo(val, 1, val.size() - 2);
1139 haveGroupID = true;
1140 } else if (!strcasecmp("language", key.c_str())) {
1141 if (val.size() < 2
1142 || val.c_str()[0] != '"'
1143 || val.c_str()[val.size() - 1] != '"') {
1144 ALOGE("Expected quoted string for LANGUAGE, got '%s' instead.",
1145 val.c_str());
1146
1147 return ERROR_MALFORMED;
1148 }
1149
1150 groupLanguage.setTo(val, 1, val.size() - 2);
1151 haveGroupLanguage = true;
1152 } else if (!strcasecmp("name", key.c_str())) {
1153 if (val.size() < 2
1154 || val.c_str()[0] != '"'
1155 || val.c_str()[val.size() - 1] != '"') {
1156 ALOGE("Expected quoted string for NAME, got '%s' instead.",
1157 val.c_str());
1158
1159 return ERROR_MALFORMED;
1160 }
1161
1162 groupName.setTo(val, 1, val.size() - 2);
1163 haveGroupName = true;
1164 } else if (!strcasecmp("autoselect", key.c_str())) {
1165 groupAutoselect = false;
1166 if (!strcasecmp("YES", val.c_str())) {
1167 groupAutoselect = true;
1168 } else if (!strcasecmp("NO", val.c_str())) {
1169 groupAutoselect = false;
1170 } else {
1171 ALOGE("Expected YES or NO for AUTOSELECT attribute, "
1172 "got '%s' instead.",
1173 val.c_str());
1174
1175 return ERROR_MALFORMED;
1176 }
1177
1178 haveGroupAutoselect = true;
1179 } else if (!strcasecmp("default", key.c_str())) {
1180 groupDefault = false;
1181 if (!strcasecmp("YES", val.c_str())) {
1182 groupDefault = true;
1183 } else if (!strcasecmp("NO", val.c_str())) {
1184 groupDefault = false;
1185 } else {
1186 ALOGE("Expected YES or NO for DEFAULT attribute, "
1187 "got '%s' instead.",
1188 val.c_str());
1189
1190 return ERROR_MALFORMED;
1191 }
1192
1193 haveGroupDefault = true;
1194 } else if (!strcasecmp("forced", key.c_str())) {
1195 groupForced = false;
1196 if (!strcasecmp("YES", val.c_str())) {
1197 groupForced = true;
1198 } else if (!strcasecmp("NO", val.c_str())) {
1199 groupForced = false;
1200 } else {
1201 ALOGE("Expected YES or NO for FORCED attribute, "
1202 "got '%s' instead.",
1203 val.c_str());
1204
1205 return ERROR_MALFORMED;
1206 }
1207
1208 haveGroupForced = true;
1209 } else if (!strcasecmp("uri", key.c_str())) {
1210 if (val.size() < 2
1211 || val.c_str()[0] != '"'
1212 || val.c_str()[val.size() - 1] != '"') {
1213 ALOGE("Expected quoted string for URI.");
1214
1215 return ERROR_MALFORMED;
1216 }
1217
1218 AString tmp(val, 1, val.size() - 2);
1219
1220 groupURI = tmp;
1221
1222 haveGroupURI = true;
1223 }
1224 }
1225
1226 if (!haveGroupType || !haveGroupID || !haveGroupName) {
1227 ALOGE("Incomplete EXT-X-MEDIA element.");
1228 return ERROR_MALFORMED;
1229 }
1230
1231 if (groupType == MediaGroup::TYPE_CC) {
1232 // TODO: ignore this for now.
1233 // CC track will be detected by CCDecoder. But we still need to
1234 // pass the CC track flags (lang, auto) to the app in the future.
1235 return OK;
1236 }
1237
1238 uint32_t flags = 0;
1239 if (haveGroupAutoselect && groupAutoselect) {
1240 flags |= MediaGroup::FLAG_AUTOSELECT;
1241 }
1242 if (haveGroupDefault && groupDefault) {
1243 flags |= MediaGroup::FLAG_DEFAULT;
1244 }
1245 if (haveGroupForced) {
1246 if (groupType != MediaGroup::TYPE_SUBS) {
1247 ALOGE("The FORCED attribute MUST not be present on anything "
1248 "but SUBS media.");
1249
1250 return ERROR_MALFORMED;
1251 }
1252
1253 if (groupForced) {
1254 flags |= MediaGroup::FLAG_FORCED;
1255 }
1256 }
1257 if (haveGroupLanguage) {
1258 flags |= MediaGroup::FLAG_HAS_LANGUAGE;
1259 }
1260 if (haveGroupURI) {
1261 flags |= MediaGroup::FLAG_HAS_URI;
1262 }
1263
1264 ssize_t groupIndex = mMediaGroups.indexOfKey(groupID);
1265 sp<MediaGroup> group;
1266
1267 if (groupIndex < 0) {
1268 group = new MediaGroup(groupType);
1269 mMediaGroups.add(groupID, group);
1270 } else {
1271 group = mMediaGroups.valueAt(groupIndex);
1272
1273 if (group->type() != groupType) {
1274 ALOGE("Attempt to put media item under group of different type "
1275 "(groupType = %d, item type = %d",
1276 group->type(),
1277 groupType);
1278
1279 return ERROR_MALFORMED;
1280 }
1281 }
1282
1283 return group->addMedia(
1284 groupName.c_str(),
1285 haveGroupURI ? groupURI.c_str() : NULL,
1286 haveGroupLanguage ? groupLanguage.c_str() : NULL,
1287 flags);
1288 }
1289
1290 // static
parseDiscontinuitySequence(const AString & line,size_t * seq)1291 status_t M3UParser::parseDiscontinuitySequence(const AString &line, size_t *seq) {
1292 ssize_t colonPos = line.find(":");
1293
1294 if (colonPos < 0) {
1295 return ERROR_MALFORMED;
1296 }
1297
1298 int32_t x;
1299 status_t err = ParseInt32(line.c_str() + colonPos + 1, &x);
1300 if (err != OK) {
1301 return err;
1302 }
1303
1304 if (x < 0) {
1305 return ERROR_MALFORMED;
1306 }
1307
1308 if (seq) {
1309 *seq = x;
1310 }
1311 return OK;
1312 }
1313
1314 // static
ParseInt32(const char * s,int32_t * x)1315 status_t M3UParser::ParseInt32(const char *s, int32_t *x) {
1316 char *end;
1317 long lval = strtol(s, &end, 10);
1318
1319 if (end == s || (*end != '\0' && *end != ',')) {
1320 return ERROR_MALFORMED;
1321 }
1322
1323 *x = (int32_t)lval;
1324
1325 return OK;
1326 }
1327
1328 // static
ParseDouble(const char * s,double * x)1329 status_t M3UParser::ParseDouble(const char *s, double *x) {
1330 char *end;
1331 double dval = strtod(s, &end);
1332
1333 if (end == s || (*end != '\0' && *end != ',')) {
1334 return ERROR_MALFORMED;
1335 }
1336
1337 *x = dval;
1338
1339 return OK;
1340 }
1341
1342 // static
isQuotedString(const AString & str)1343 bool M3UParser::isQuotedString(const AString &str) {
1344 if (str.size() < 2
1345 || str.c_str()[0] != '"'
1346 || str.c_str()[str.size() - 1] != '"') {
1347 return false;
1348 }
1349 return true;
1350 }
1351
1352 // static
unquoteString(const AString & str)1353 AString M3UParser::unquoteString(const AString &str) {
1354 if (!isQuotedString(str)) {
1355 return str;
1356 }
1357 return AString(str, 1, str.size() - 2);
1358 }
1359
1360 // static
codecIsType(const AString & codec,const char * type)1361 bool M3UParser::codecIsType(const AString &codec, const char *type) {
1362 if (codec.size() < 4) {
1363 return false;
1364 }
1365 const char *c = codec.c_str();
1366 switch (FOURCC(c[0], c[1], c[2], c[3])) {
1367 // List extracted from http://www.mp4ra.org/codecs.html
1368 case 'ac-3':
1369 case 'alac':
1370 case 'dra1':
1371 case 'dtsc':
1372 case 'dtse':
1373 case 'dtsh':
1374 case 'dtsl':
1375 case 'ec-3':
1376 case 'enca':
1377 case 'g719':
1378 case 'g726':
1379 case 'm4ae':
1380 case 'mlpa':
1381 case 'mp4a':
1382 case 'raw ':
1383 case 'samr':
1384 case 'sawb':
1385 case 'sawp':
1386 case 'sevc':
1387 case 'sqcp':
1388 case 'ssmv':
1389 case 'twos':
1390 case 'agsm':
1391 case 'alaw':
1392 case 'dvi ':
1393 case 'fl32':
1394 case 'fl64':
1395 case 'ima4':
1396 case 'in24':
1397 case 'in32':
1398 case 'lpcm':
1399 case 'Qclp':
1400 case 'QDM2':
1401 case 'QDMC':
1402 case 'ulaw':
1403 case 'vdva':
1404 case 'ac-4':
1405 case 'Opus':
1406 case 'a3ds':
1407 case 'dts+':
1408 case 'dts-':
1409 case 'dtsx':
1410 case 'dtsy':
1411 case 'ec+3':
1412 case 'mha1':
1413 case 'mha2':
1414 case 'mhm1':
1415 case 'mhm2':
1416 case 'sevs':
1417 return !strcmp("audio", type);
1418
1419 case 'avc1':
1420 case 'avc2':
1421 case 'avcp':
1422 case 'drac':
1423 case 'encv':
1424 case 'mjp2':
1425 case 'mp4v':
1426 case 'mvc1':
1427 case 'mvc2':
1428 case 'resv':
1429 case 's263':
1430 case 'svc1':
1431 case 'vc-1':
1432 case 'CFHD':
1433 case 'civd':
1434 case 'DV10':
1435 case 'dvh5':
1436 case 'dvh6':
1437 case 'dvhp':
1438 case 'DVOO':
1439 case 'DVOR':
1440 case 'DVTV':
1441 case 'DVVT':
1442 case 'flic':
1443 case 'gif ':
1444 case 'h261':
1445 case 'h263':
1446 case 'HD10':
1447 case 'jpeg':
1448 case 'M105':
1449 case 'mjpa':
1450 case 'mjpb':
1451 case 'png ':
1452 case 'PNTG':
1453 case 'rle ':
1454 case 'rpza':
1455 case 'Shr0':
1456 case 'Shr1':
1457 case 'Shr2':
1458 case 'Shr3':
1459 case 'Shr4':
1460 case 'SVQ1':
1461 case 'SVQ3':
1462 case 'tga ':
1463 case 'tiff':
1464 case 'WRLE':
1465 case 'a3d1':
1466 case 'a3d2':
1467 case 'a3d3':
1468 case 'a3d4':
1469 case 'avc3':
1470 case 'avc4':
1471 case 'dva1':
1472 case 'dvav':
1473 case 'dvh1':
1474 case 'dvhe':
1475 case 'hev1':
1476 case 'hev2':
1477 case 'hvc1':
1478 case 'hvc2':
1479 case 'hvt1':
1480 case 'lhe1':
1481 case 'lht1':
1482 case 'lhv1':
1483 case 'mjpg':
1484 case 'mvc3':
1485 case 'mvc4':
1486 case 'mvd1':
1487 case 'mvd2':
1488 case 'mvd3':
1489 case 'mvd4':
1490 case 'rv60':
1491 case 'svc2':
1492 case 'vp08':
1493 case 'vp09':
1494 return !strcmp("video", type);
1495
1496 default:
1497 return false;
1498 }
1499 }
1500
1501 } // namespace android
1502