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