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/MediaErrors.h> 27 #include <media/mediaplayer.h> 28 29 namespace android { 30 31 struct M3UParser::MediaGroup : public RefBase { 32 enum Type { 33 TYPE_AUDIO, 34 TYPE_VIDEO, 35 TYPE_SUBS, 36 }; 37 38 enum FlagBits { 39 FLAG_AUTOSELECT = 1, 40 FLAG_DEFAULT = 2, 41 FLAG_FORCED = 4, 42 FLAG_HAS_LANGUAGE = 8, 43 FLAG_HAS_URI = 16, 44 }; 45 46 MediaGroup(Type type); 47 48 Type type() const; 49 50 status_t addMedia( 51 const char *name, 52 const char *uri, 53 const char *language, 54 uint32_t flags); 55 56 bool getActiveURI(AString *uri) const; 57 58 void pickRandomMediaItems(); 59 status_t selectTrack(size_t index, bool select); 60 void getTrackInfo(Parcel* reply) const; 61 size_t countTracks() const; 62 63 protected: 64 virtual ~MediaGroup(); 65 66 private: 67 struct Media { 68 AString mName; 69 AString mURI; 70 AString mLanguage; 71 uint32_t mFlags; 72 }; 73 74 Type mType; 75 Vector<Media> mMediaItems; 76 77 ssize_t mSelectedIndex; 78 79 DISALLOW_EVIL_CONSTRUCTORS(MediaGroup); 80 }; 81 82 M3UParser::MediaGroup::MediaGroup(Type type) 83 : mType(type), 84 mSelectedIndex(-1) { 85 } 86 87 M3UParser::MediaGroup::~MediaGroup() { 88 } 89 90 M3UParser::MediaGroup::Type M3UParser::MediaGroup::type() const { 91 return mType; 92 } 93 94 status_t M3UParser::MediaGroup::addMedia( 95 const char *name, 96 const char *uri, 97 const char *language, 98 uint32_t flags) { 99 mMediaItems.push(); 100 Media &item = mMediaItems.editItemAt(mMediaItems.size() - 1); 101 102 item.mName = name; 103 104 if (uri) { 105 item.mURI = uri; 106 } 107 108 if (language) { 109 item.mLanguage = language; 110 } 111 112 item.mFlags = flags; 113 114 return OK; 115 } 116 117 void M3UParser::MediaGroup::pickRandomMediaItems() { 118 #if 1 119 switch (mType) { 120 case TYPE_AUDIO: 121 { 122 char value[PROPERTY_VALUE_MAX]; 123 if (property_get("media.httplive.audio-index", value, NULL)) { 124 char *end; 125 mSelectedIndex = strtoul(value, &end, 10); 126 CHECK(end > value && *end == '\0'); 127 128 if (mSelectedIndex >= mMediaItems.size()) { 129 mSelectedIndex = mMediaItems.size() - 1; 130 } 131 } else { 132 mSelectedIndex = 0; 133 } 134 break; 135 } 136 137 case TYPE_VIDEO: 138 { 139 mSelectedIndex = 0; 140 break; 141 } 142 143 case TYPE_SUBS: 144 { 145 mSelectedIndex = -1; 146 break; 147 } 148 149 default: 150 TRESPASS(); 151 } 152 #else 153 mSelectedIndex = (rand() * mMediaItems.size()) / RAND_MAX; 154 #endif 155 } 156 157 status_t M3UParser::MediaGroup::selectTrack(size_t index, bool select) { 158 if (mType != TYPE_SUBS) { 159 ALOGE("only select subtitile tracks for now!"); 160 return INVALID_OPERATION; 161 } 162 163 if (select) { 164 if (index >= mMediaItems.size()) { 165 ALOGE("track %d does not exist", index); 166 return INVALID_OPERATION; 167 } 168 if (mSelectedIndex == index) { 169 ALOGE("track %d already selected", index); 170 return BAD_VALUE; 171 } 172 ALOGV("selected track %d", index); 173 mSelectedIndex = index; 174 } else { 175 if (mSelectedIndex != index) { 176 ALOGE("track %d is not selected", index); 177 return BAD_VALUE; 178 } 179 ALOGV("unselected track %d", index); 180 mSelectedIndex = -1; 181 } 182 183 return OK; 184 } 185 186 void M3UParser::MediaGroup::getTrackInfo(Parcel* reply) const { 187 for (size_t i = 0; i < mMediaItems.size(); ++i) { 188 reply->writeInt32(2); // 2 fields 189 190 if (mType == TYPE_AUDIO) { 191 reply->writeInt32(MEDIA_TRACK_TYPE_AUDIO); 192 } else if (mType == TYPE_VIDEO) { 193 reply->writeInt32(MEDIA_TRACK_TYPE_VIDEO); 194 } else if (mType == TYPE_SUBS) { 195 reply->writeInt32(MEDIA_TRACK_TYPE_SUBTITLE); 196 } else { 197 reply->writeInt32(MEDIA_TRACK_TYPE_UNKNOWN); 198 } 199 200 const Media &item = mMediaItems.itemAt(i); 201 const char *lang = item.mLanguage.empty() ? "und" : item.mLanguage.c_str(); 202 reply->writeString16(String16(lang)); 203 204 if (mType == TYPE_SUBS) { 205 // TO-DO: pass in a MediaFormat instead 206 reply->writeInt32(!!(item.mFlags & MediaGroup::FLAG_AUTOSELECT)); 207 reply->writeInt32(!!(item.mFlags & MediaGroup::FLAG_DEFAULT)); 208 reply->writeInt32(!!(item.mFlags & MediaGroup::FLAG_FORCED)); 209 } 210 } 211 } 212 213 size_t M3UParser::MediaGroup::countTracks() const { 214 return mMediaItems.size(); 215 } 216 217 bool M3UParser::MediaGroup::getActiveURI(AString *uri) const { 218 for (size_t i = 0; i < mMediaItems.size(); ++i) { 219 if (mSelectedIndex >= 0 && i == (size_t)mSelectedIndex) { 220 const Media &item = mMediaItems.itemAt(i); 221 222 *uri = item.mURI; 223 return true; 224 } 225 } 226 227 return false; 228 } 229 230 //////////////////////////////////////////////////////////////////////////////// 231 232 M3UParser::M3UParser( 233 const char *baseURI, const void *data, size_t size) 234 : mInitCheck(NO_INIT), 235 mBaseURI(baseURI), 236 mIsExtM3U(false), 237 mIsVariantPlaylist(false), 238 mIsComplete(false), 239 mIsEvent(false), 240 mSelectedIndex(-1) { 241 mInitCheck = parse(data, size); 242 } 243 244 M3UParser::~M3UParser() { 245 } 246 247 status_t M3UParser::initCheck() const { 248 return mInitCheck; 249 } 250 251 bool M3UParser::isExtM3U() const { 252 return mIsExtM3U; 253 } 254 255 bool M3UParser::isVariantPlaylist() const { 256 return mIsVariantPlaylist; 257 } 258 259 bool M3UParser::isComplete() const { 260 return mIsComplete; 261 } 262 263 bool M3UParser::isEvent() const { 264 return mIsEvent; 265 } 266 267 sp<AMessage> M3UParser::meta() { 268 return mMeta; 269 } 270 271 size_t M3UParser::size() { 272 return mItems.size(); 273 } 274 275 bool M3UParser::itemAt(size_t index, AString *uri, sp<AMessage> *meta) { 276 if (uri) { 277 uri->clear(); 278 } 279 280 if (meta) { 281 *meta = NULL; 282 } 283 284 if (index >= mItems.size()) { 285 return false; 286 } 287 288 if (uri) { 289 *uri = mItems.itemAt(index).mURI; 290 } 291 292 if (meta) { 293 *meta = mItems.itemAt(index).mMeta; 294 } 295 296 return true; 297 } 298 299 void M3UParser::pickRandomMediaItems() { 300 for (size_t i = 0; i < mMediaGroups.size(); ++i) { 301 mMediaGroups.valueAt(i)->pickRandomMediaItems(); 302 } 303 } 304 305 status_t M3UParser::selectTrack(size_t index, bool select) { 306 for (size_t i = 0, ii = index; i < mMediaGroups.size(); ++i) { 307 sp<MediaGroup> group = mMediaGroups.valueAt(i); 308 size_t tracks = group->countTracks(); 309 if (ii < tracks) { 310 status_t err = group->selectTrack(ii, select); 311 if (err == OK) { 312 mSelectedIndex = select ? index : -1; 313 } 314 return err; 315 } 316 ii -= tracks; 317 } 318 return INVALID_OPERATION; 319 } 320 321 status_t M3UParser::getTrackInfo(Parcel* reply) const { 322 size_t trackCount = 0; 323 for (size_t i = 0; i < mMediaGroups.size(); ++i) { 324 trackCount += mMediaGroups.valueAt(i)->countTracks(); 325 } 326 reply->writeInt32(trackCount); 327 328 for (size_t i = 0; i < mMediaGroups.size(); ++i) { 329 mMediaGroups.valueAt(i)->getTrackInfo(reply); 330 } 331 return OK; 332 } 333 334 ssize_t M3UParser::getSelectedIndex() const { 335 return mSelectedIndex; 336 } 337 338 bool M3UParser::getTypeURI(size_t index, const char *key, AString *uri) const { 339 if (!mIsVariantPlaylist) { 340 *uri = mBaseURI; 341 342 // Assume media without any more specific attribute contains 343 // audio and video, but no subtitles. 344 return !strcmp("audio", key) || !strcmp("video", key); 345 } 346 347 CHECK_LT(index, mItems.size()); 348 349 sp<AMessage> meta = mItems.itemAt(index).mMeta; 350 351 AString groupID; 352 if (!meta->findString(key, &groupID)) { 353 *uri = mItems.itemAt(index).mURI; 354 355 // Assume media without any more specific attribute contains 356 // audio and video, but no subtitles. 357 return !strcmp("audio", key) || !strcmp("video", key); 358 } 359 360 sp<MediaGroup> group = mMediaGroups.valueFor(groupID); 361 if (!group->getActiveURI(uri)) { 362 return false; 363 } 364 365 if ((*uri).empty()) { 366 *uri = mItems.itemAt(index).mURI; 367 } 368 369 return true; 370 } 371 372 bool M3UParser::getAudioURI(size_t index, AString *uri) const { 373 return getTypeURI(index, "audio", uri); 374 } 375 376 bool M3UParser::getVideoURI(size_t index, AString *uri) const { 377 return getTypeURI(index, "video", uri); 378 } 379 380 bool M3UParser::getSubtitleURI(size_t index, AString *uri) const { 381 return getTypeURI(index, "subtitles", uri); 382 } 383 384 static bool MakeURL(const char *baseURL, const char *url, AString *out) { 385 out->clear(); 386 387 if (strncasecmp("http://", baseURL, 7) 388 && strncasecmp("https://", baseURL, 8) 389 && strncasecmp("file://", baseURL, 7)) { 390 // Base URL must be absolute 391 return false; 392 } 393 394 if (!strncasecmp("http://", url, 7) || !strncasecmp("https://", url, 8)) { 395 // "url" is already an absolute URL, ignore base URL. 396 out->setTo(url); 397 398 ALOGV("base:'%s', url:'%s' => '%s'", baseURL, url, out->c_str()); 399 400 return true; 401 } 402 403 if (url[0] == '/') { 404 // URL is an absolute path. 405 406 char *protocolEnd = strstr(baseURL, "//") + 2; 407 char *pathStart = strchr(protocolEnd, '/'); 408 409 if (pathStart != NULL) { 410 out->setTo(baseURL, pathStart - baseURL); 411 } else { 412 out->setTo(baseURL); 413 } 414 415 out->append(url); 416 } else { 417 // URL is a relative path 418 419 size_t n = strlen(baseURL); 420 if (baseURL[n - 1] == '/') { 421 out->setTo(baseURL); 422 out->append(url); 423 } else { 424 const char *slashPos = strrchr(baseURL, '/'); 425 426 if (slashPos > &baseURL[6]) { 427 out->setTo(baseURL, slashPos - baseURL); 428 } else { 429 out->setTo(baseURL); 430 } 431 432 out->append("/"); 433 out->append(url); 434 } 435 } 436 437 ALOGV("base:'%s', url:'%s' => '%s'", baseURL, url, out->c_str()); 438 439 return true; 440 } 441 442 status_t M3UParser::parse(const void *_data, size_t size) { 443 int32_t lineNo = 0; 444 445 sp<AMessage> itemMeta; 446 447 const char *data = (const char *)_data; 448 size_t offset = 0; 449 uint64_t segmentRangeOffset = 0; 450 while (offset < size) { 451 size_t offsetLF = offset; 452 while (offsetLF < size && data[offsetLF] != '\n') { 453 ++offsetLF; 454 } 455 456 AString line; 457 if (offsetLF > offset && data[offsetLF - 1] == '\r') { 458 line.setTo(&data[offset], offsetLF - offset - 1); 459 } else { 460 line.setTo(&data[offset], offsetLF - offset); 461 } 462 463 // ALOGI("#%s#", line.c_str()); 464 465 if (line.empty()) { 466 offset = offsetLF + 1; 467 continue; 468 } 469 470 if (lineNo == 0 && line == "#EXTM3U") { 471 mIsExtM3U = true; 472 } 473 474 if (mIsExtM3U) { 475 status_t err = OK; 476 477 if (line.startsWith("#EXT-X-TARGETDURATION")) { 478 if (mIsVariantPlaylist) { 479 return ERROR_MALFORMED; 480 } 481 err = parseMetaData(line, &mMeta, "target-duration"); 482 } else if (line.startsWith("#EXT-X-MEDIA-SEQUENCE")) { 483 if (mIsVariantPlaylist) { 484 return ERROR_MALFORMED; 485 } 486 err = parseMetaData(line, &mMeta, "media-sequence"); 487 } else if (line.startsWith("#EXT-X-KEY")) { 488 if (mIsVariantPlaylist) { 489 return ERROR_MALFORMED; 490 } 491 err = parseCipherInfo(line, &itemMeta, mBaseURI); 492 } else if (line.startsWith("#EXT-X-ENDLIST")) { 493 mIsComplete = true; 494 } else if (line.startsWith("#EXT-X-PLAYLIST-TYPE:EVENT")) { 495 mIsEvent = true; 496 } else if (line.startsWith("#EXTINF")) { 497 if (mIsVariantPlaylist) { 498 return ERROR_MALFORMED; 499 } 500 err = parseMetaDataDuration(line, &itemMeta, "durationUs"); 501 } else if (line.startsWith("#EXT-X-DISCONTINUITY")) { 502 if (mIsVariantPlaylist) { 503 return ERROR_MALFORMED; 504 } 505 if (itemMeta == NULL) { 506 itemMeta = new AMessage; 507 } 508 itemMeta->setInt32("discontinuity", true); 509 } else if (line.startsWith("#EXT-X-STREAM-INF")) { 510 if (mMeta != NULL) { 511 return ERROR_MALFORMED; 512 } 513 mIsVariantPlaylist = true; 514 err = parseStreamInf(line, &itemMeta); 515 } else if (line.startsWith("#EXT-X-BYTERANGE")) { 516 if (mIsVariantPlaylist) { 517 return ERROR_MALFORMED; 518 } 519 520 uint64_t length, offset; 521 err = parseByteRange(line, segmentRangeOffset, &length, &offset); 522 523 if (err == OK) { 524 if (itemMeta == NULL) { 525 itemMeta = new AMessage; 526 } 527 528 itemMeta->setInt64("range-offset", offset); 529 itemMeta->setInt64("range-length", length); 530 531 segmentRangeOffset = offset + length; 532 } 533 } else if (line.startsWith("#EXT-X-MEDIA")) { 534 err = parseMedia(line); 535 } 536 537 if (err != OK) { 538 return err; 539 } 540 } 541 542 if (!line.startsWith("#")) { 543 if (!mIsVariantPlaylist) { 544 int64_t durationUs; 545 if (itemMeta == NULL 546 || !itemMeta->findInt64("durationUs", &durationUs)) { 547 return ERROR_MALFORMED; 548 } 549 } 550 551 mItems.push(); 552 Item *item = &mItems.editItemAt(mItems.size() - 1); 553 554 CHECK(MakeURL(mBaseURI.c_str(), line.c_str(), &item->mURI)); 555 556 item->mMeta = itemMeta; 557 558 itemMeta.clear(); 559 } 560 561 offset = offsetLF + 1; 562 ++lineNo; 563 } 564 565 return OK; 566 } 567 568 // static 569 status_t M3UParser::parseMetaData( 570 const AString &line, sp<AMessage> *meta, const char *key) { 571 ssize_t colonPos = line.find(":"); 572 573 if (colonPos < 0) { 574 return ERROR_MALFORMED; 575 } 576 577 int32_t x; 578 status_t err = ParseInt32(line.c_str() + colonPos + 1, &x); 579 580 if (err != OK) { 581 return err; 582 } 583 584 if (meta->get() == NULL) { 585 *meta = new AMessage; 586 } 587 (*meta)->setInt32(key, x); 588 589 return OK; 590 } 591 592 // static 593 status_t M3UParser::parseMetaDataDuration( 594 const AString &line, sp<AMessage> *meta, const char *key) { 595 ssize_t colonPos = line.find(":"); 596 597 if (colonPos < 0) { 598 return ERROR_MALFORMED; 599 } 600 601 double x; 602 status_t err = ParseDouble(line.c_str() + colonPos + 1, &x); 603 604 if (err != OK) { 605 return err; 606 } 607 608 if (meta->get() == NULL) { 609 *meta = new AMessage; 610 } 611 (*meta)->setInt64(key, (int64_t)x * 1E6); 612 613 return OK; 614 } 615 616 // Find the next occurence of the character "what" at or after "offset", 617 // but ignore occurences between quotation marks. 618 // Return the index of the occurrence or -1 if not found. 619 static ssize_t FindNextUnquoted( 620 const AString &line, char what, size_t offset) { 621 CHECK_NE((int)what, (int)'"'); 622 623 bool quoted = false; 624 while (offset < line.size()) { 625 char c = line.c_str()[offset]; 626 627 if (c == '"') { 628 quoted = !quoted; 629 } else if (c == what && !quoted) { 630 return offset; 631 } 632 633 ++offset; 634 } 635 636 return -1; 637 } 638 639 status_t M3UParser::parseStreamInf( 640 const AString &line, sp<AMessage> *meta) const { 641 ssize_t colonPos = line.find(":"); 642 643 if (colonPos < 0) { 644 return ERROR_MALFORMED; 645 } 646 647 size_t offset = colonPos + 1; 648 649 while (offset < line.size()) { 650 ssize_t end = FindNextUnquoted(line, ',', offset); 651 if (end < 0) { 652 end = line.size(); 653 } 654 655 AString attr(line, offset, end - offset); 656 attr.trim(); 657 658 offset = end + 1; 659 660 ssize_t equalPos = attr.find("="); 661 if (equalPos < 0) { 662 continue; 663 } 664 665 AString key(attr, 0, equalPos); 666 key.trim(); 667 668 AString val(attr, equalPos + 1, attr.size() - equalPos - 1); 669 val.trim(); 670 671 ALOGV("key=%s value=%s", key.c_str(), val.c_str()); 672 673 if (!strcasecmp("bandwidth", key.c_str())) { 674 const char *s = val.c_str(); 675 char *end; 676 unsigned long x = strtoul(s, &end, 10); 677 678 if (end == s || *end != '\0') { 679 // malformed 680 continue; 681 } 682 683 if (meta->get() == NULL) { 684 *meta = new AMessage; 685 } 686 (*meta)->setInt32("bandwidth", x); 687 } else if (!strcasecmp("audio", key.c_str()) 688 || !strcasecmp("video", key.c_str()) 689 || !strcasecmp("subtitles", key.c_str())) { 690 if (val.size() < 2 691 || val.c_str()[0] != '"' 692 || val.c_str()[val.size() - 1] != '"') { 693 ALOGE("Expected quoted string for %s attribute, " 694 "got '%s' instead.", 695 key.c_str(), val.c_str()); 696 697 return ERROR_MALFORMED; 698 } 699 700 AString groupID(val, 1, val.size() - 2); 701 ssize_t groupIndex = mMediaGroups.indexOfKey(groupID); 702 703 if (groupIndex < 0) { 704 ALOGE("Undefined media group '%s' referenced in stream info.", 705 groupID.c_str()); 706 707 return ERROR_MALFORMED; 708 } 709 710 key.tolower(); 711 (*meta)->setString(key.c_str(), groupID.c_str()); 712 } 713 } 714 715 return OK; 716 } 717 718 // static 719 status_t M3UParser::parseCipherInfo( 720 const AString &line, sp<AMessage> *meta, const AString &baseURI) { 721 ssize_t colonPos = line.find(":"); 722 723 if (colonPos < 0) { 724 return ERROR_MALFORMED; 725 } 726 727 size_t offset = colonPos + 1; 728 729 while (offset < line.size()) { 730 ssize_t end = FindNextUnquoted(line, ',', offset); 731 if (end < 0) { 732 end = line.size(); 733 } 734 735 AString attr(line, offset, end - offset); 736 attr.trim(); 737 738 offset = end + 1; 739 740 ssize_t equalPos = attr.find("="); 741 if (equalPos < 0) { 742 continue; 743 } 744 745 AString key(attr, 0, equalPos); 746 key.trim(); 747 748 AString val(attr, equalPos + 1, attr.size() - equalPos - 1); 749 val.trim(); 750 751 ALOGV("key=%s value=%s", key.c_str(), val.c_str()); 752 753 key.tolower(); 754 755 if (key == "method" || key == "uri" || key == "iv") { 756 if (meta->get() == NULL) { 757 *meta = new AMessage; 758 } 759 760 if (key == "uri") { 761 if (val.size() >= 2 762 && val.c_str()[0] == '"' 763 && val.c_str()[val.size() - 1] == '"') { 764 // Remove surrounding quotes. 765 AString tmp(val, 1, val.size() - 2); 766 val = tmp; 767 } 768 769 AString absURI; 770 if (MakeURL(baseURI.c_str(), val.c_str(), &absURI)) { 771 val = absURI; 772 } else { 773 ALOGE("failed to make absolute url for '%s'.", 774 val.c_str()); 775 } 776 } 777 778 key.insert(AString("cipher-"), 0); 779 780 (*meta)->setString(key.c_str(), val.c_str(), val.size()); 781 } 782 } 783 784 return OK; 785 } 786 787 // static 788 status_t M3UParser::parseByteRange( 789 const AString &line, uint64_t curOffset, 790 uint64_t *length, uint64_t *offset) { 791 ssize_t colonPos = line.find(":"); 792 793 if (colonPos < 0) { 794 return ERROR_MALFORMED; 795 } 796 797 ssize_t atPos = line.find("@", colonPos + 1); 798 799 AString lenStr; 800 if (atPos < 0) { 801 lenStr = AString(line, colonPos + 1, line.size() - colonPos - 1); 802 } else { 803 lenStr = AString(line, colonPos + 1, atPos - colonPos - 1); 804 } 805 806 lenStr.trim(); 807 808 const char *s = lenStr.c_str(); 809 char *end; 810 *length = strtoull(s, &end, 10); 811 812 if (s == end || *end != '\0') { 813 return ERROR_MALFORMED; 814 } 815 816 if (atPos >= 0) { 817 AString offStr = AString(line, atPos + 1, line.size() - atPos - 1); 818 offStr.trim(); 819 820 const char *s = offStr.c_str(); 821 *offset = strtoull(s, &end, 10); 822 823 if (s == end || *end != '\0') { 824 return ERROR_MALFORMED; 825 } 826 } else { 827 *offset = curOffset; 828 } 829 830 return OK; 831 } 832 833 status_t M3UParser::parseMedia(const AString &line) { 834 ssize_t colonPos = line.find(":"); 835 836 if (colonPos < 0) { 837 return ERROR_MALFORMED; 838 } 839 840 bool haveGroupType = false; 841 MediaGroup::Type groupType = MediaGroup::TYPE_AUDIO; 842 843 bool haveGroupID = false; 844 AString groupID; 845 846 bool haveGroupLanguage = false; 847 AString groupLanguage; 848 849 bool haveGroupName = false; 850 AString groupName; 851 852 bool haveGroupAutoselect = false; 853 bool groupAutoselect = false; 854 855 bool haveGroupDefault = false; 856 bool groupDefault = false; 857 858 bool haveGroupForced = false; 859 bool groupForced = false; 860 861 bool haveGroupURI = false; 862 AString groupURI; 863 864 size_t offset = colonPos + 1; 865 866 while (offset < line.size()) { 867 ssize_t end = FindNextUnquoted(line, ',', offset); 868 if (end < 0) { 869 end = line.size(); 870 } 871 872 AString attr(line, offset, end - offset); 873 attr.trim(); 874 875 offset = end + 1; 876 877 ssize_t equalPos = attr.find("="); 878 if (equalPos < 0) { 879 continue; 880 } 881 882 AString key(attr, 0, equalPos); 883 key.trim(); 884 885 AString val(attr, equalPos + 1, attr.size() - equalPos - 1); 886 val.trim(); 887 888 ALOGV("key=%s value=%s", key.c_str(), val.c_str()); 889 890 if (!strcasecmp("type", key.c_str())) { 891 if (!strcasecmp("subtitles", val.c_str())) { 892 groupType = MediaGroup::TYPE_SUBS; 893 } else if (!strcasecmp("audio", val.c_str())) { 894 groupType = MediaGroup::TYPE_AUDIO; 895 } else if (!strcasecmp("video", val.c_str())) { 896 groupType = MediaGroup::TYPE_VIDEO; 897 } else { 898 ALOGE("Invalid media group type '%s'", val.c_str()); 899 return ERROR_MALFORMED; 900 } 901 902 haveGroupType = true; 903 } else if (!strcasecmp("group-id", key.c_str())) { 904 if (val.size() < 2 905 || val.c_str()[0] != '"' 906 || val.c_str()[val.size() - 1] != '"') { 907 ALOGE("Expected quoted string for GROUP-ID, got '%s' instead.", 908 val.c_str()); 909 910 return ERROR_MALFORMED; 911 } 912 913 groupID.setTo(val, 1, val.size() - 2); 914 haveGroupID = true; 915 } else if (!strcasecmp("language", key.c_str())) { 916 if (val.size() < 2 917 || val.c_str()[0] != '"' 918 || val.c_str()[val.size() - 1] != '"') { 919 ALOGE("Expected quoted string for LANGUAGE, got '%s' instead.", 920 val.c_str()); 921 922 return ERROR_MALFORMED; 923 } 924 925 groupLanguage.setTo(val, 1, val.size() - 2); 926 haveGroupLanguage = true; 927 } else if (!strcasecmp("name", key.c_str())) { 928 if (val.size() < 2 929 || val.c_str()[0] != '"' 930 || val.c_str()[val.size() - 1] != '"') { 931 ALOGE("Expected quoted string for NAME, got '%s' instead.", 932 val.c_str()); 933 934 return ERROR_MALFORMED; 935 } 936 937 groupName.setTo(val, 1, val.size() - 2); 938 haveGroupName = true; 939 } else if (!strcasecmp("autoselect", key.c_str())) { 940 groupAutoselect = false; 941 if (!strcasecmp("YES", val.c_str())) { 942 groupAutoselect = true; 943 } else if (!strcasecmp("NO", val.c_str())) { 944 groupAutoselect = false; 945 } else { 946 ALOGE("Expected YES or NO for AUTOSELECT attribute, " 947 "got '%s' instead.", 948 val.c_str()); 949 950 return ERROR_MALFORMED; 951 } 952 953 haveGroupAutoselect = true; 954 } else if (!strcasecmp("default", key.c_str())) { 955 groupDefault = false; 956 if (!strcasecmp("YES", val.c_str())) { 957 groupDefault = true; 958 } else if (!strcasecmp("NO", val.c_str())) { 959 groupDefault = false; 960 } else { 961 ALOGE("Expected YES or NO for DEFAULT attribute, " 962 "got '%s' instead.", 963 val.c_str()); 964 965 return ERROR_MALFORMED; 966 } 967 968 haveGroupDefault = true; 969 } else if (!strcasecmp("forced", key.c_str())) { 970 groupForced = false; 971 if (!strcasecmp("YES", val.c_str())) { 972 groupForced = true; 973 } else if (!strcasecmp("NO", val.c_str())) { 974 groupForced = false; 975 } else { 976 ALOGE("Expected YES or NO for FORCED attribute, " 977 "got '%s' instead.", 978 val.c_str()); 979 980 return ERROR_MALFORMED; 981 } 982 983 haveGroupForced = true; 984 } else if (!strcasecmp("uri", key.c_str())) { 985 if (val.size() < 2 986 || val.c_str()[0] != '"' 987 || val.c_str()[val.size() - 1] != '"') { 988 ALOGE("Expected quoted string for URI, got '%s' instead.", 989 val.c_str()); 990 991 return ERROR_MALFORMED; 992 } 993 994 AString tmp(val, 1, val.size() - 2); 995 996 if (!MakeURL(mBaseURI.c_str(), tmp.c_str(), &groupURI)) { 997 ALOGI("Failed to make absolute URI from '%s'.", tmp.c_str()); 998 } 999 1000 haveGroupURI = true; 1001 } 1002 } 1003 1004 if (!haveGroupType || !haveGroupID || !haveGroupName) { 1005 ALOGE("Incomplete EXT-X-MEDIA element."); 1006 return ERROR_MALFORMED; 1007 } 1008 1009 uint32_t flags = 0; 1010 if (haveGroupAutoselect && groupAutoselect) { 1011 flags |= MediaGroup::FLAG_AUTOSELECT; 1012 } 1013 if (haveGroupDefault && groupDefault) { 1014 flags |= MediaGroup::FLAG_DEFAULT; 1015 } 1016 if (haveGroupForced) { 1017 if (groupType != MediaGroup::TYPE_SUBS) { 1018 ALOGE("The FORCED attribute MUST not be present on anything " 1019 "but SUBS media."); 1020 1021 return ERROR_MALFORMED; 1022 } 1023 1024 if (groupForced) { 1025 flags |= MediaGroup::FLAG_FORCED; 1026 } 1027 } 1028 if (haveGroupLanguage) { 1029 flags |= MediaGroup::FLAG_HAS_LANGUAGE; 1030 } 1031 if (haveGroupURI) { 1032 flags |= MediaGroup::FLAG_HAS_URI; 1033 } 1034 1035 ssize_t groupIndex = mMediaGroups.indexOfKey(groupID); 1036 sp<MediaGroup> group; 1037 1038 if (groupIndex < 0) { 1039 group = new MediaGroup(groupType); 1040 mMediaGroups.add(groupID, group); 1041 } else { 1042 group = mMediaGroups.valueAt(groupIndex); 1043 1044 if (group->type() != groupType) { 1045 ALOGE("Attempt to put media item under group of different type " 1046 "(groupType = %d, item type = %d", 1047 group->type(), 1048 groupType); 1049 1050 return ERROR_MALFORMED; 1051 } 1052 } 1053 1054 return group->addMedia( 1055 groupName.c_str(), 1056 haveGroupURI ? groupURI.c_str() : NULL, 1057 haveGroupLanguage ? groupLanguage.c_str() : NULL, 1058 flags); 1059 } 1060 1061 // static 1062 status_t M3UParser::ParseInt32(const char *s, int32_t *x) { 1063 char *end; 1064 long lval = strtol(s, &end, 10); 1065 1066 if (end == s || (*end != '\0' && *end != ',')) { 1067 return ERROR_MALFORMED; 1068 } 1069 1070 *x = (int32_t)lval; 1071 1072 return OK; 1073 } 1074 1075 // static 1076 status_t M3UParser::ParseDouble(const char *s, double *x) { 1077 char *end; 1078 double dval = strtod(s, &end); 1079 1080 if (end == s || (*end != '\0' && *end != ',')) { 1081 return ERROR_MALFORMED; 1082 } 1083 1084 *x = dval; 1085 1086 return OK; 1087 } 1088 1089 } // namespace android 1090