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-SEQUENCE")) { 607 if (mIsVariantPlaylist) { 608 return ERROR_MALFORMED; 609 } 610 size_t seq; 611 err = parseDiscontinuitySequence(line, &seq); 612 if (err == OK) { 613 mDiscontinuitySeq = seq; 614 ALOGI("mDiscontinuitySeq %zu", mDiscontinuitySeq); 615 } else { 616 ALOGI("Failed to parseDiscontinuitySequence %d", err); 617 } 618 } else if (line.startsWith("#EXT-X-DISCONTINUITY")) { 619 if (mIsVariantPlaylist) { 620 return ERROR_MALFORMED; 621 } 622 if (itemMeta == NULL) { 623 itemMeta = new AMessage; 624 } 625 itemMeta->setInt32("discontinuity", true); 626 ++mDiscontinuityCount; 627 } else if (line.startsWith("#EXT-X-STREAM-INF")) { 628 if (mMeta != NULL) { 629 return ERROR_MALFORMED; 630 } 631 mIsVariantPlaylist = true; 632 err = parseStreamInf(line, &itemMeta); 633 } else if (line.startsWith("#EXT-X-BYTERANGE")) { 634 if (mIsVariantPlaylist) { 635 return ERROR_MALFORMED; 636 } 637 638 uint64_t length, offset; 639 err = parseByteRange(line, segmentRangeOffset, &length, &offset); 640 641 if (err == OK) { 642 if (itemMeta == NULL) { 643 itemMeta = new AMessage; 644 } 645 646 itemMeta->setInt64("range-offset", offset); 647 itemMeta->setInt64("range-length", length); 648 649 segmentRangeOffset = offset + length; 650 } 651 } else if (line.startsWith("#EXT-X-MEDIA")) { 652 err = parseMedia(line); 653 } 654 655 if (err != OK) { 656 return err; 657 } 658 } 659 660 if (!line.startsWith("#")) { 661 if (!mIsVariantPlaylist) { 662 int64_t durationUs; 663 if (itemMeta == NULL 664 || !itemMeta->findInt64("durationUs", &durationUs)) { 665 return ERROR_MALFORMED; 666 } 667 itemMeta->setInt32("discontinuity-sequence", 668 mDiscontinuitySeq + mDiscontinuityCount); 669 } 670 671 mItems.push(); 672 Item *item = &mItems.editItemAt(mItems.size() - 1); 673 674 CHECK(MakeURL(mBaseURI.c_str(), line.c_str(), &item->mURI)); 675 676 item->mMeta = itemMeta; 677 678 itemMeta.clear(); 679 } 680 681 offset = offsetLF + 1; 682 ++lineNo; 683 } 684 685 // error checking of all fields that's required to appear once 686 // (currently only checking "target-duration"), and 687 // initialization of playlist properties (eg. mTargetDurationUs) 688 if (!mIsVariantPlaylist) { 689 int32_t targetDurationSecs; 690 if (mMeta == NULL || !mMeta->findInt32( 691 "target-duration", &targetDurationSecs)) { 692 ALOGE("Media playlist missing #EXT-X-TARGETDURATION"); 693 return ERROR_MALFORMED; 694 } 695 mTargetDurationUs = targetDurationSecs * 1000000ll; 696 697 mFirstSeqNumber = 0; 698 if (mMeta != NULL) { 699 mMeta->findInt32("media-sequence", &mFirstSeqNumber); 700 } 701 mLastSeqNumber = mFirstSeqNumber + mItems.size() - 1; 702 } 703 704 for (size_t i = 0; i < mItems.size(); ++i) { 705 sp<AMessage> meta = mItems.itemAt(i).mMeta; 706 const char *keys[] = {"audio", "video", "subtitles"}; 707 for (size_t j = 0; j < sizeof(keys) / sizeof(const char *); ++j) { 708 AString groupID; 709 if (meta->findString(keys[j], &groupID)) { 710 ssize_t groupIndex = mMediaGroups.indexOfKey(groupID); 711 if (groupIndex < 0) { 712 ALOGE("Undefined media group '%s' referenced in stream info.", 713 groupID.c_str()); 714 return ERROR_MALFORMED; 715 } 716 } 717 } 718 } 719 720 return OK; 721 } 722 723 // static 724 status_t M3UParser::parseMetaData( 725 const AString &line, sp<AMessage> *meta, const char *key) { 726 ssize_t colonPos = line.find(":"); 727 728 if (colonPos < 0) { 729 return ERROR_MALFORMED; 730 } 731 732 int32_t x; 733 status_t err = ParseInt32(line.c_str() + colonPos + 1, &x); 734 735 if (err != OK) { 736 return err; 737 } 738 739 if (meta->get() == NULL) { 740 *meta = new AMessage; 741 } 742 (*meta)->setInt32(key, x); 743 744 return OK; 745 } 746 747 // static 748 status_t M3UParser::parseMetaDataDuration( 749 const AString &line, sp<AMessage> *meta, const char *key) { 750 ssize_t colonPos = line.find(":"); 751 752 if (colonPos < 0) { 753 return ERROR_MALFORMED; 754 } 755 756 double x; 757 status_t err = ParseDouble(line.c_str() + colonPos + 1, &x); 758 759 if (err != OK) { 760 return err; 761 } 762 763 if (meta->get() == NULL) { 764 *meta = new AMessage; 765 } 766 (*meta)->setInt64(key, (int64_t)(x * 1E6)); 767 768 return OK; 769 } 770 771 // Find the next occurence of the character "what" at or after "offset", 772 // but ignore occurences between quotation marks. 773 // Return the index of the occurrence or -1 if not found. 774 static ssize_t FindNextUnquoted( 775 const AString &line, char what, size_t offset) { 776 CHECK_NE((int)what, (int)'"'); 777 778 bool quoted = false; 779 while (offset < line.size()) { 780 char c = line.c_str()[offset]; 781 782 if (c == '"') { 783 quoted = !quoted; 784 } else if (c == what && !quoted) { 785 return offset; 786 } 787 788 ++offset; 789 } 790 791 return -1; 792 } 793 794 status_t M3UParser::parseStreamInf( 795 const AString &line, sp<AMessage> *meta) const { 796 ssize_t colonPos = line.find(":"); 797 798 if (colonPos < 0) { 799 return ERROR_MALFORMED; 800 } 801 802 size_t offset = colonPos + 1; 803 804 while (offset < line.size()) { 805 ssize_t end = FindNextUnquoted(line, ',', offset); 806 if (end < 0) { 807 end = line.size(); 808 } 809 810 AString attr(line, offset, end - offset); 811 attr.trim(); 812 813 offset = end + 1; 814 815 ssize_t equalPos = attr.find("="); 816 if (equalPos < 0) { 817 continue; 818 } 819 820 AString key(attr, 0, equalPos); 821 key.trim(); 822 823 AString val(attr, equalPos + 1, attr.size() - equalPos - 1); 824 val.trim(); 825 826 ALOGV("key=%s value=%s", key.c_str(), val.c_str()); 827 828 if (!strcasecmp("bandwidth", key.c_str())) { 829 const char *s = val.c_str(); 830 char *end; 831 unsigned long x = strtoul(s, &end, 10); 832 833 if (end == s || *end != '\0') { 834 // malformed 835 continue; 836 } 837 838 if (meta->get() == NULL) { 839 *meta = new AMessage; 840 } 841 (*meta)->setInt32("bandwidth", x); 842 } else if (!strcasecmp("codecs", key.c_str())) { 843 if (!isQuotedString(val)) { 844 ALOGE("Expected quoted string for %s attribute, " 845 "got '%s' instead.", 846 key.c_str(), val.c_str());; 847 848 return ERROR_MALFORMED; 849 } 850 851 key.tolower(); 852 const AString &codecs = unquoteString(val); 853 if (meta->get() == NULL) { 854 *meta = new AMessage; 855 } 856 (*meta)->setString(key.c_str(), codecs.c_str()); 857 } else if (!strcasecmp("resolution", key.c_str())) { 858 const char *s = val.c_str(); 859 char *end; 860 unsigned long width = strtoul(s, &end, 10); 861 862 if (end == s || *end != 'x') { 863 // malformed 864 continue; 865 } 866 867 s = end + 1; 868 unsigned long height = strtoul(s, &end, 10); 869 870 if (end == s || *end != '\0') { 871 // malformed 872 continue; 873 } 874 875 if (meta->get() == NULL) { 876 *meta = new AMessage; 877 } 878 (*meta)->setInt32("width", width); 879 (*meta)->setInt32("height", height); 880 } else if (!strcasecmp("audio", key.c_str()) 881 || !strcasecmp("video", key.c_str()) 882 || !strcasecmp("subtitles", key.c_str())) { 883 if (!isQuotedString(val)) { 884 ALOGE("Expected quoted string for %s attribute, " 885 "got '%s' instead.", 886 key.c_str(), val.c_str()); 887 888 return ERROR_MALFORMED; 889 } 890 891 const AString &groupID = unquoteString(val); 892 key.tolower(); 893 if (meta->get() == NULL) { 894 *meta = new AMessage; 895 } 896 (*meta)->setString(key.c_str(), groupID.c_str()); 897 } 898 } 899 900 return OK; 901 } 902 903 // static 904 status_t M3UParser::parseCipherInfo( 905 const AString &line, sp<AMessage> *meta, const AString &baseURI) { 906 ssize_t colonPos = line.find(":"); 907 908 if (colonPos < 0) { 909 return ERROR_MALFORMED; 910 } 911 912 size_t offset = colonPos + 1; 913 914 while (offset < line.size()) { 915 ssize_t end = FindNextUnquoted(line, ',', offset); 916 if (end < 0) { 917 end = line.size(); 918 } 919 920 AString attr(line, offset, end - offset); 921 attr.trim(); 922 923 offset = end + 1; 924 925 ssize_t equalPos = attr.find("="); 926 if (equalPos < 0) { 927 continue; 928 } 929 930 AString key(attr, 0, equalPos); 931 key.trim(); 932 933 AString val(attr, equalPos + 1, attr.size() - equalPos - 1); 934 val.trim(); 935 936 ALOGV("key=%s value=%s", key.c_str(), val.c_str()); 937 938 key.tolower(); 939 940 if (key == "method" || key == "uri" || key == "iv") { 941 if (meta->get() == NULL) { 942 *meta = new AMessage; 943 } 944 945 if (key == "uri") { 946 if (val.size() >= 2 947 && val.c_str()[0] == '"' 948 && val.c_str()[val.size() - 1] == '"') { 949 // Remove surrounding quotes. 950 AString tmp(val, 1, val.size() - 2); 951 val = tmp; 952 } 953 954 AString absURI; 955 if (MakeURL(baseURI.c_str(), val.c_str(), &absURI)) { 956 val = absURI; 957 } else { 958 ALOGE("failed to make absolute url for %s.", 959 uriDebugString(baseURI).c_str()); 960 } 961 } 962 963 key.insert(AString("cipher-"), 0); 964 965 (*meta)->setString(key.c_str(), val.c_str(), val.size()); 966 } 967 } 968 969 return OK; 970 } 971 972 // static 973 status_t M3UParser::parseByteRange( 974 const AString &line, uint64_t curOffset, 975 uint64_t *length, uint64_t *offset) { 976 ssize_t colonPos = line.find(":"); 977 978 if (colonPos < 0) { 979 return ERROR_MALFORMED; 980 } 981 982 ssize_t atPos = line.find("@", colonPos + 1); 983 984 AString lenStr; 985 if (atPos < 0) { 986 lenStr = AString(line, colonPos + 1, line.size() - colonPos - 1); 987 } else { 988 lenStr = AString(line, colonPos + 1, atPos - colonPos - 1); 989 } 990 991 lenStr.trim(); 992 993 const char *s = lenStr.c_str(); 994 char *end; 995 *length = strtoull(s, &end, 10); 996 997 if (s == end || *end != '\0') { 998 return ERROR_MALFORMED; 999 } 1000 1001 if (atPos >= 0) { 1002 AString offStr = AString(line, atPos + 1, line.size() - atPos - 1); 1003 offStr.trim(); 1004 1005 const char *s = offStr.c_str(); 1006 *offset = strtoull(s, &end, 10); 1007 1008 if (s == end || *end != '\0') { 1009 return ERROR_MALFORMED; 1010 } 1011 } else { 1012 *offset = curOffset; 1013 } 1014 1015 return OK; 1016 } 1017 1018 status_t M3UParser::parseMedia(const AString &line) { 1019 ssize_t colonPos = line.find(":"); 1020 1021 if (colonPos < 0) { 1022 return ERROR_MALFORMED; 1023 } 1024 1025 bool haveGroupType = false; 1026 MediaGroup::Type groupType = MediaGroup::TYPE_AUDIO; 1027 1028 bool haveGroupID = false; 1029 AString groupID; 1030 1031 bool haveGroupLanguage = false; 1032 AString groupLanguage; 1033 1034 bool haveGroupName = false; 1035 AString groupName; 1036 1037 bool haveGroupAutoselect = false; 1038 bool groupAutoselect = false; 1039 1040 bool haveGroupDefault = false; 1041 bool groupDefault = false; 1042 1043 bool haveGroupForced = false; 1044 bool groupForced = false; 1045 1046 bool haveGroupURI = false; 1047 AString groupURI; 1048 1049 size_t offset = colonPos + 1; 1050 1051 while (offset < line.size()) { 1052 ssize_t end = FindNextUnquoted(line, ',', offset); 1053 if (end < 0) { 1054 end = line.size(); 1055 } 1056 1057 AString attr(line, offset, end - offset); 1058 attr.trim(); 1059 1060 offset = end + 1; 1061 1062 ssize_t equalPos = attr.find("="); 1063 if (equalPos < 0) { 1064 continue; 1065 } 1066 1067 AString key(attr, 0, equalPos); 1068 key.trim(); 1069 1070 AString val(attr, equalPos + 1, attr.size() - equalPos - 1); 1071 val.trim(); 1072 1073 ALOGV("key=%s value=%s", key.c_str(), val.c_str()); 1074 1075 if (!strcasecmp("type", key.c_str())) { 1076 if (!strcasecmp("subtitles", val.c_str())) { 1077 groupType = MediaGroup::TYPE_SUBS; 1078 } else if (!strcasecmp("audio", val.c_str())) { 1079 groupType = MediaGroup::TYPE_AUDIO; 1080 } else if (!strcasecmp("video", val.c_str())) { 1081 groupType = MediaGroup::TYPE_VIDEO; 1082 } else if (!strcasecmp("closed-captions", val.c_str())){ 1083 groupType = MediaGroup::TYPE_CC; 1084 } else { 1085 ALOGE("Invalid media group type '%s'", val.c_str()); 1086 return ERROR_MALFORMED; 1087 } 1088 1089 haveGroupType = true; 1090 } else if (!strcasecmp("group-id", key.c_str())) { 1091 if (val.size() < 2 1092 || val.c_str()[0] != '"' 1093 || val.c_str()[val.size() - 1] != '"') { 1094 ALOGE("Expected quoted string for GROUP-ID, got '%s' instead.", 1095 val.c_str()); 1096 1097 return ERROR_MALFORMED; 1098 } 1099 1100 groupID.setTo(val, 1, val.size() - 2); 1101 haveGroupID = true; 1102 } else if (!strcasecmp("language", key.c_str())) { 1103 if (val.size() < 2 1104 || val.c_str()[0] != '"' 1105 || val.c_str()[val.size() - 1] != '"') { 1106 ALOGE("Expected quoted string for LANGUAGE, got '%s' instead.", 1107 val.c_str()); 1108 1109 return ERROR_MALFORMED; 1110 } 1111 1112 groupLanguage.setTo(val, 1, val.size() - 2); 1113 haveGroupLanguage = true; 1114 } else if (!strcasecmp("name", key.c_str())) { 1115 if (val.size() < 2 1116 || val.c_str()[0] != '"' 1117 || val.c_str()[val.size() - 1] != '"') { 1118 ALOGE("Expected quoted string for NAME, got '%s' instead.", 1119 val.c_str()); 1120 1121 return ERROR_MALFORMED; 1122 } 1123 1124 groupName.setTo(val, 1, val.size() - 2); 1125 haveGroupName = true; 1126 } else if (!strcasecmp("autoselect", key.c_str())) { 1127 groupAutoselect = false; 1128 if (!strcasecmp("YES", val.c_str())) { 1129 groupAutoselect = true; 1130 } else if (!strcasecmp("NO", val.c_str())) { 1131 groupAutoselect = false; 1132 } else { 1133 ALOGE("Expected YES or NO for AUTOSELECT attribute, " 1134 "got '%s' instead.", 1135 val.c_str()); 1136 1137 return ERROR_MALFORMED; 1138 } 1139 1140 haveGroupAutoselect = true; 1141 } else if (!strcasecmp("default", key.c_str())) { 1142 groupDefault = false; 1143 if (!strcasecmp("YES", val.c_str())) { 1144 groupDefault = true; 1145 } else if (!strcasecmp("NO", val.c_str())) { 1146 groupDefault = false; 1147 } else { 1148 ALOGE("Expected YES or NO for DEFAULT attribute, " 1149 "got '%s' instead.", 1150 val.c_str()); 1151 1152 return ERROR_MALFORMED; 1153 } 1154 1155 haveGroupDefault = true; 1156 } else if (!strcasecmp("forced", key.c_str())) { 1157 groupForced = false; 1158 if (!strcasecmp("YES", val.c_str())) { 1159 groupForced = true; 1160 } else if (!strcasecmp("NO", val.c_str())) { 1161 groupForced = false; 1162 } else { 1163 ALOGE("Expected YES or NO for FORCED attribute, " 1164 "got '%s' instead.", 1165 val.c_str()); 1166 1167 return ERROR_MALFORMED; 1168 } 1169 1170 haveGroupForced = true; 1171 } else if (!strcasecmp("uri", key.c_str())) { 1172 if (val.size() < 2 1173 || val.c_str()[0] != '"' 1174 || val.c_str()[val.size() - 1] != '"') { 1175 ALOGE("Expected quoted string for URI, got '%s' instead.", 1176 val.c_str()); 1177 1178 return ERROR_MALFORMED; 1179 } 1180 1181 AString tmp(val, 1, val.size() - 2); 1182 1183 if (!MakeURL(mBaseURI.c_str(), tmp.c_str(), &groupURI)) { 1184 ALOGI("Failed to make absolute URI from '%s'.", tmp.c_str()); 1185 } 1186 1187 haveGroupURI = true; 1188 } 1189 } 1190 1191 if (!haveGroupType || !haveGroupID || !haveGroupName) { 1192 ALOGE("Incomplete EXT-X-MEDIA element."); 1193 return ERROR_MALFORMED; 1194 } 1195 1196 if (groupType == MediaGroup::TYPE_CC) { 1197 // TODO: ignore this for now. 1198 // CC track will be detected by CCDecoder. But we still need to 1199 // pass the CC track flags (lang, auto) to the app in the future. 1200 return OK; 1201 } 1202 1203 uint32_t flags = 0; 1204 if (haveGroupAutoselect && groupAutoselect) { 1205 flags |= MediaGroup::FLAG_AUTOSELECT; 1206 } 1207 if (haveGroupDefault && groupDefault) { 1208 flags |= MediaGroup::FLAG_DEFAULT; 1209 } 1210 if (haveGroupForced) { 1211 if (groupType != MediaGroup::TYPE_SUBS) { 1212 ALOGE("The FORCED attribute MUST not be present on anything " 1213 "but SUBS media."); 1214 1215 return ERROR_MALFORMED; 1216 } 1217 1218 if (groupForced) { 1219 flags |= MediaGroup::FLAG_FORCED; 1220 } 1221 } 1222 if (haveGroupLanguage) { 1223 flags |= MediaGroup::FLAG_HAS_LANGUAGE; 1224 } 1225 if (haveGroupURI) { 1226 flags |= MediaGroup::FLAG_HAS_URI; 1227 } 1228 1229 ssize_t groupIndex = mMediaGroups.indexOfKey(groupID); 1230 sp<MediaGroup> group; 1231 1232 if (groupIndex < 0) { 1233 group = new MediaGroup(groupType); 1234 mMediaGroups.add(groupID, group); 1235 } else { 1236 group = mMediaGroups.valueAt(groupIndex); 1237 1238 if (group->type() != groupType) { 1239 ALOGE("Attempt to put media item under group of different type " 1240 "(groupType = %d, item type = %d", 1241 group->type(), 1242 groupType); 1243 1244 return ERROR_MALFORMED; 1245 } 1246 } 1247 1248 return group->addMedia( 1249 groupName.c_str(), 1250 haveGroupURI ? groupURI.c_str() : NULL, 1251 haveGroupLanguage ? groupLanguage.c_str() : NULL, 1252 flags); 1253 } 1254 1255 // static 1256 status_t M3UParser::parseDiscontinuitySequence(const AString &line, size_t *seq) { 1257 ssize_t colonPos = line.find(":"); 1258 1259 if (colonPos < 0) { 1260 return ERROR_MALFORMED; 1261 } 1262 1263 int32_t x; 1264 status_t err = ParseInt32(line.c_str() + colonPos + 1, &x); 1265 if (err != OK) { 1266 return err; 1267 } 1268 1269 if (x < 0) { 1270 return ERROR_MALFORMED; 1271 } 1272 1273 if (seq) { 1274 *seq = x; 1275 } 1276 return OK; 1277 } 1278 1279 // static 1280 status_t M3UParser::ParseInt32(const char *s, int32_t *x) { 1281 char *end; 1282 long lval = strtol(s, &end, 10); 1283 1284 if (end == s || (*end != '\0' && *end != ',')) { 1285 return ERROR_MALFORMED; 1286 } 1287 1288 *x = (int32_t)lval; 1289 1290 return OK; 1291 } 1292 1293 // static 1294 status_t M3UParser::ParseDouble(const char *s, double *x) { 1295 char *end; 1296 double dval = strtod(s, &end); 1297 1298 if (end == s || (*end != '\0' && *end != ',')) { 1299 return ERROR_MALFORMED; 1300 } 1301 1302 *x = dval; 1303 1304 return OK; 1305 } 1306 1307 // static 1308 bool M3UParser::isQuotedString(const AString &str) { 1309 if (str.size() < 2 1310 || str.c_str()[0] != '"' 1311 || str.c_str()[str.size() - 1] != '"') { 1312 return false; 1313 } 1314 return true; 1315 } 1316 1317 // static 1318 AString M3UParser::unquoteString(const AString &str) { 1319 if (!isQuotedString(str)) { 1320 return str; 1321 } 1322 return AString(str, 1, str.size() - 2); 1323 } 1324 1325 // static 1326 bool M3UParser::codecIsType(const AString &codec, const char *type) { 1327 if (codec.size() < 4) { 1328 return false; 1329 } 1330 const char *c = codec.c_str(); 1331 switch (FOURCC(c[0], c[1], c[2], c[3])) { 1332 // List extracted from http://www.mp4ra.org/codecs.html 1333 case 'ac-3': 1334 case 'alac': 1335 case 'dra1': 1336 case 'dtsc': 1337 case 'dtse': 1338 case 'dtsh': 1339 case 'dtsl': 1340 case 'ec-3': 1341 case 'enca': 1342 case 'g719': 1343 case 'g726': 1344 case 'm4ae': 1345 case 'mlpa': 1346 case 'mp4a': 1347 case 'raw ': 1348 case 'samr': 1349 case 'sawb': 1350 case 'sawp': 1351 case 'sevc': 1352 case 'sqcp': 1353 case 'ssmv': 1354 case 'twos': 1355 case 'agsm': 1356 case 'alaw': 1357 case 'dvi ': 1358 case 'fl32': 1359 case 'fl64': 1360 case 'ima4': 1361 case 'in24': 1362 case 'in32': 1363 case 'lpcm': 1364 case 'Qclp': 1365 case 'QDM2': 1366 case 'QDMC': 1367 case 'ulaw': 1368 case 'vdva': 1369 return !strcmp("audio", type); 1370 1371 case 'avc1': 1372 case 'avc2': 1373 case 'avcp': 1374 case 'drac': 1375 case 'encv': 1376 case 'mjp2': 1377 case 'mp4v': 1378 case 'mvc1': 1379 case 'mvc2': 1380 case 'resv': 1381 case 's263': 1382 case 'svc1': 1383 case 'vc-1': 1384 case 'CFHD': 1385 case 'civd': 1386 case 'DV10': 1387 case 'dvh5': 1388 case 'dvh6': 1389 case 'dvhp': 1390 case 'DVOO': 1391 case 'DVOR': 1392 case 'DVTV': 1393 case 'DVVT': 1394 case 'flic': 1395 case 'gif ': 1396 case 'h261': 1397 case 'h263': 1398 case 'HD10': 1399 case 'jpeg': 1400 case 'M105': 1401 case 'mjpa': 1402 case 'mjpb': 1403 case 'png ': 1404 case 'PNTG': 1405 case 'rle ': 1406 case 'rpza': 1407 case 'Shr0': 1408 case 'Shr1': 1409 case 'Shr2': 1410 case 'Shr3': 1411 case 'Shr4': 1412 case 'SVQ1': 1413 case 'SVQ3': 1414 case 'tga ': 1415 case 'tiff': 1416 case 'WRLE': 1417 return !strcmp("video", type); 1418 1419 default: 1420 return false; 1421 } 1422 } 1423 1424 } // namespace android 1425