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