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