M3UParser.cpp revision 8ca002eedc747dd854b61cbe364b52c06869273f
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 29namespace android { 30 31struct 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 63protected: 64 virtual ~MediaGroup(); 65 66private: 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 82M3UParser::MediaGroup::MediaGroup(Type type) 83 : mType(type), 84 mSelectedIndex(-1) { 85} 86 87M3UParser::MediaGroup::~MediaGroup() { 88} 89 90M3UParser::MediaGroup::Type M3UParser::MediaGroup::type() const { 91 return mType; 92} 93 94status_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 117void 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 >= (ssize_t)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 157status_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 == (ssize_t)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 != (ssize_t)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 186void 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 213size_t M3UParser::MediaGroup::countTracks() const { 214 return mMediaItems.size(); 215} 216 217bool 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 232M3UParser::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 244M3UParser::~M3UParser() { 245} 246 247status_t M3UParser::initCheck() const { 248 return mInitCheck; 249} 250 251bool M3UParser::isExtM3U() const { 252 return mIsExtM3U; 253} 254 255bool M3UParser::isVariantPlaylist() const { 256 return mIsVariantPlaylist; 257} 258 259bool M3UParser::isComplete() const { 260 return mIsComplete; 261} 262 263bool M3UParser::isEvent() const { 264 return mIsEvent; 265} 266 267sp<AMessage> M3UParser::meta() { 268 return mMeta; 269} 270 271size_t M3UParser::size() { 272 return mItems.size(); 273} 274 275bool 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 299void M3UParser::pickRandomMediaItems() { 300 for (size_t i = 0; i < mMediaGroups.size(); ++i) { 301 mMediaGroups.valueAt(i)->pickRandomMediaItems(); 302 } 303} 304 305status_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 321status_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 334ssize_t M3UParser::getSelectedIndex() const { 335 return mSelectedIndex; 336} 337 338bool 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 372static bool MakeURL(const char *baseURL, const char *url, AString *out) { 373 out->clear(); 374 375 if (strncasecmp("http://", baseURL, 7) 376 && strncasecmp("https://", baseURL, 8) 377 && strncasecmp("file://", baseURL, 7)) { 378 // Base URL must be absolute 379 return false; 380 } 381 382 if (!strncasecmp("http://", url, 7) || !strncasecmp("https://", url, 8)) { 383 // "url" is already an absolute URL, ignore base URL. 384 out->setTo(url); 385 386 ALOGV("base:'%s', url:'%s' => '%s'", baseURL, url, out->c_str()); 387 388 return true; 389 } 390 391 if (url[0] == '/') { 392 // URL is an absolute path. 393 394 char *protocolEnd = strstr(baseURL, "//") + 2; 395 char *pathStart = strchr(protocolEnd, '/'); 396 397 if (pathStart != NULL) { 398 out->setTo(baseURL, pathStart - baseURL); 399 } else { 400 out->setTo(baseURL); 401 } 402 403 out->append(url); 404 } else { 405 // URL is a relative path 406 407 // Check for a possible query string 408 const char *qsPos = strchr(baseURL, '?'); 409 size_t end; 410 if (qsPos != NULL) { 411 end = qsPos - baseURL; 412 } else { 413 end = strlen(baseURL); 414 } 415 // Check for the last slash before a potential query string 416 for (ssize_t pos = end - 1; pos >= 0; pos--) { 417 if (baseURL[pos] == '/') { 418 end = pos; 419 break; 420 } 421 } 422 423 // Check whether the found slash actually is part of the path 424 // and not part of the "http://". 425 if (end > 6) { 426 out->setTo(baseURL, end); 427 } else { 428 out->setTo(baseURL); 429 } 430 431 out->append("/"); 432 out->append(url); 433 } 434 435 ALOGV("base:'%s', url:'%s' => '%s'", baseURL, url, out->c_str()); 436 437 return true; 438} 439 440status_t M3UParser::parse(const void *_data, size_t size) { 441 int32_t lineNo = 0; 442 443 sp<AMessage> itemMeta; 444 445 const char *data = (const char *)_data; 446 size_t offset = 0; 447 uint64_t segmentRangeOffset = 0; 448 while (offset < size) { 449 size_t offsetLF = offset; 450 while (offsetLF < size && data[offsetLF] != '\n') { 451 ++offsetLF; 452 } 453 454 AString line; 455 if (offsetLF > offset && data[offsetLF - 1] == '\r') { 456 line.setTo(&data[offset], offsetLF - offset - 1); 457 } else { 458 line.setTo(&data[offset], offsetLF - offset); 459 } 460 461 // ALOGI("#%s#", line.c_str()); 462 463 if (line.empty()) { 464 offset = offsetLF + 1; 465 continue; 466 } 467 468 if (lineNo == 0 && line == "#EXTM3U") { 469 mIsExtM3U = true; 470 } 471 472 if (mIsExtM3U) { 473 status_t err = OK; 474 475 if (line.startsWith("#EXT-X-TARGETDURATION")) { 476 if (mIsVariantPlaylist) { 477 return ERROR_MALFORMED; 478 } 479 err = parseMetaData(line, &mMeta, "target-duration"); 480 } else if (line.startsWith("#EXT-X-MEDIA-SEQUENCE")) { 481 if (mIsVariantPlaylist) { 482 return ERROR_MALFORMED; 483 } 484 err = parseMetaData(line, &mMeta, "media-sequence"); 485 } else if (line.startsWith("#EXT-X-KEY")) { 486 if (mIsVariantPlaylist) { 487 return ERROR_MALFORMED; 488 } 489 err = parseCipherInfo(line, &itemMeta, mBaseURI); 490 } else if (line.startsWith("#EXT-X-ENDLIST")) { 491 mIsComplete = true; 492 } else if (line.startsWith("#EXT-X-PLAYLIST-TYPE:EVENT")) { 493 mIsEvent = true; 494 } else if (line.startsWith("#EXTINF")) { 495 if (mIsVariantPlaylist) { 496 return ERROR_MALFORMED; 497 } 498 err = parseMetaDataDuration(line, &itemMeta, "durationUs"); 499 } else if (line.startsWith("#EXT-X-DISCONTINUITY")) { 500 if (mIsVariantPlaylist) { 501 return ERROR_MALFORMED; 502 } 503 if (itemMeta == NULL) { 504 itemMeta = new AMessage; 505 } 506 itemMeta->setInt32("discontinuity", true); 507 } else if (line.startsWith("#EXT-X-STREAM-INF")) { 508 if (mMeta != NULL) { 509 return ERROR_MALFORMED; 510 } 511 mIsVariantPlaylist = true; 512 err = parseStreamInf(line, &itemMeta); 513 } else if (line.startsWith("#EXT-X-BYTERANGE")) { 514 if (mIsVariantPlaylist) { 515 return ERROR_MALFORMED; 516 } 517 518 uint64_t length, offset; 519 err = parseByteRange(line, segmentRangeOffset, &length, &offset); 520 521 if (err == OK) { 522 if (itemMeta == NULL) { 523 itemMeta = new AMessage; 524 } 525 526 itemMeta->setInt64("range-offset", offset); 527 itemMeta->setInt64("range-length", length); 528 529 segmentRangeOffset = offset + length; 530 } 531 } else if (line.startsWith("#EXT-X-MEDIA")) { 532 err = parseMedia(line); 533 } 534 535 if (err != OK) { 536 return err; 537 } 538 } 539 540 if (!line.startsWith("#")) { 541 if (!mIsVariantPlaylist) { 542 int64_t durationUs; 543 if (itemMeta == NULL 544 || !itemMeta->findInt64("durationUs", &durationUs)) { 545 return ERROR_MALFORMED; 546 } 547 } 548 549 mItems.push(); 550 Item *item = &mItems.editItemAt(mItems.size() - 1); 551 552 CHECK(MakeURL(mBaseURI.c_str(), line.c_str(), &item->mURI)); 553 554 item->mMeta = itemMeta; 555 556 itemMeta.clear(); 557 } 558 559 offset = offsetLF + 1; 560 ++lineNo; 561 } 562 563 return OK; 564} 565 566// static 567status_t M3UParser::parseMetaData( 568 const AString &line, sp<AMessage> *meta, const char *key) { 569 ssize_t colonPos = line.find(":"); 570 571 if (colonPos < 0) { 572 return ERROR_MALFORMED; 573 } 574 575 int32_t x; 576 status_t err = ParseInt32(line.c_str() + colonPos + 1, &x); 577 578 if (err != OK) { 579 return err; 580 } 581 582 if (meta->get() == NULL) { 583 *meta = new AMessage; 584 } 585 (*meta)->setInt32(key, x); 586 587 return OK; 588} 589 590// static 591status_t M3UParser::parseMetaDataDuration( 592 const AString &line, sp<AMessage> *meta, const char *key) { 593 ssize_t colonPos = line.find(":"); 594 595 if (colonPos < 0) { 596 return ERROR_MALFORMED; 597 } 598 599 double x; 600 status_t err = ParseDouble(line.c_str() + colonPos + 1, &x); 601 602 if (err != OK) { 603 return err; 604 } 605 606 if (meta->get() == NULL) { 607 *meta = new AMessage; 608 } 609 (*meta)->setInt64(key, (int64_t)(x * 1E6)); 610 611 return OK; 612} 613 614// Find the next occurence of the character "what" at or after "offset", 615// but ignore occurences between quotation marks. 616// Return the index of the occurrence or -1 if not found. 617static ssize_t FindNextUnquoted( 618 const AString &line, char what, size_t offset) { 619 CHECK_NE((int)what, (int)'"'); 620 621 bool quoted = false; 622 while (offset < line.size()) { 623 char c = line.c_str()[offset]; 624 625 if (c == '"') { 626 quoted = !quoted; 627 } else if (c == what && !quoted) { 628 return offset; 629 } 630 631 ++offset; 632 } 633 634 return -1; 635} 636 637status_t M3UParser::parseStreamInf( 638 const AString &line, sp<AMessage> *meta) const { 639 ssize_t colonPos = line.find(":"); 640 641 if (colonPos < 0) { 642 return ERROR_MALFORMED; 643 } 644 645 size_t offset = colonPos + 1; 646 647 while (offset < line.size()) { 648 ssize_t end = FindNextUnquoted(line, ',', offset); 649 if (end < 0) { 650 end = line.size(); 651 } 652 653 AString attr(line, offset, end - offset); 654 attr.trim(); 655 656 offset = end + 1; 657 658 ssize_t equalPos = attr.find("="); 659 if (equalPos < 0) { 660 continue; 661 } 662 663 AString key(attr, 0, equalPos); 664 key.trim(); 665 666 AString val(attr, equalPos + 1, attr.size() - equalPos - 1); 667 val.trim(); 668 669 ALOGV("key=%s value=%s", key.c_str(), val.c_str()); 670 671 if (!strcasecmp("bandwidth", key.c_str())) { 672 const char *s = val.c_str(); 673 char *end; 674 unsigned long x = strtoul(s, &end, 10); 675 676 if (end == s || *end != '\0') { 677 // malformed 678 continue; 679 } 680 681 if (meta->get() == NULL) { 682 *meta = new AMessage; 683 } 684 (*meta)->setInt32("bandwidth", x); 685 } else if (!strcasecmp("audio", key.c_str()) 686 || !strcasecmp("video", key.c_str()) 687 || !strcasecmp("subtitles", key.c_str())) { 688 if (val.size() < 2 689 || val.c_str()[0] != '"' 690 || val.c_str()[val.size() - 1] != '"') { 691 ALOGE("Expected quoted string for %s attribute, " 692 "got '%s' instead.", 693 key.c_str(), val.c_str()); 694 695 return ERROR_MALFORMED; 696 } 697 698 AString groupID(val, 1, val.size() - 2); 699 ssize_t groupIndex = mMediaGroups.indexOfKey(groupID); 700 701 if (groupIndex < 0) { 702 ALOGE("Undefined media group '%s' referenced in stream info.", 703 groupID.c_str()); 704 705 return ERROR_MALFORMED; 706 } 707 708 key.tolower(); 709 (*meta)->setString(key.c_str(), groupID.c_str()); 710 } 711 } 712 713 return OK; 714} 715 716// static 717status_t M3UParser::parseCipherInfo( 718 const AString &line, sp<AMessage> *meta, const AString &baseURI) { 719 ssize_t colonPos = line.find(":"); 720 721 if (colonPos < 0) { 722 return ERROR_MALFORMED; 723 } 724 725 size_t offset = colonPos + 1; 726 727 while (offset < line.size()) { 728 ssize_t end = FindNextUnquoted(line, ',', offset); 729 if (end < 0) { 730 end = line.size(); 731 } 732 733 AString attr(line, offset, end - offset); 734 attr.trim(); 735 736 offset = end + 1; 737 738 ssize_t equalPos = attr.find("="); 739 if (equalPos < 0) { 740 continue; 741 } 742 743 AString key(attr, 0, equalPos); 744 key.trim(); 745 746 AString val(attr, equalPos + 1, attr.size() - equalPos - 1); 747 val.trim(); 748 749 ALOGV("key=%s value=%s", key.c_str(), val.c_str()); 750 751 key.tolower(); 752 753 if (key == "method" || key == "uri" || key == "iv") { 754 if (meta->get() == NULL) { 755 *meta = new AMessage; 756 } 757 758 if (key == "uri") { 759 if (val.size() >= 2 760 && val.c_str()[0] == '"' 761 && val.c_str()[val.size() - 1] == '"') { 762 // Remove surrounding quotes. 763 AString tmp(val, 1, val.size() - 2); 764 val = tmp; 765 } 766 767 AString absURI; 768 if (MakeURL(baseURI.c_str(), val.c_str(), &absURI)) { 769 val = absURI; 770 } else { 771 ALOGE("failed to make absolute url for '%s'.", 772 val.c_str()); 773 } 774 } 775 776 key.insert(AString("cipher-"), 0); 777 778 (*meta)->setString(key.c_str(), val.c_str(), val.size()); 779 } 780 } 781 782 return OK; 783} 784 785// static 786status_t M3UParser::parseByteRange( 787 const AString &line, uint64_t curOffset, 788 uint64_t *length, uint64_t *offset) { 789 ssize_t colonPos = line.find(":"); 790 791 if (colonPos < 0) { 792 return ERROR_MALFORMED; 793 } 794 795 ssize_t atPos = line.find("@", colonPos + 1); 796 797 AString lenStr; 798 if (atPos < 0) { 799 lenStr = AString(line, colonPos + 1, line.size() - colonPos - 1); 800 } else { 801 lenStr = AString(line, colonPos + 1, atPos - colonPos - 1); 802 } 803 804 lenStr.trim(); 805 806 const char *s = lenStr.c_str(); 807 char *end; 808 *length = strtoull(s, &end, 10); 809 810 if (s == end || *end != '\0') { 811 return ERROR_MALFORMED; 812 } 813 814 if (atPos >= 0) { 815 AString offStr = AString(line, atPos + 1, line.size() - atPos - 1); 816 offStr.trim(); 817 818 const char *s = offStr.c_str(); 819 *offset = strtoull(s, &end, 10); 820 821 if (s == end || *end != '\0') { 822 return ERROR_MALFORMED; 823 } 824 } else { 825 *offset = curOffset; 826 } 827 828 return OK; 829} 830 831status_t M3UParser::parseMedia(const AString &line) { 832 ssize_t colonPos = line.find(":"); 833 834 if (colonPos < 0) { 835 return ERROR_MALFORMED; 836 } 837 838 bool haveGroupType = false; 839 MediaGroup::Type groupType = MediaGroup::TYPE_AUDIO; 840 841 bool haveGroupID = false; 842 AString groupID; 843 844 bool haveGroupLanguage = false; 845 AString groupLanguage; 846 847 bool haveGroupName = false; 848 AString groupName; 849 850 bool haveGroupAutoselect = false; 851 bool groupAutoselect = false; 852 853 bool haveGroupDefault = false; 854 bool groupDefault = false; 855 856 bool haveGroupForced = false; 857 bool groupForced = false; 858 859 bool haveGroupURI = false; 860 AString groupURI; 861 862 size_t offset = colonPos + 1; 863 864 while (offset < line.size()) { 865 ssize_t end = FindNextUnquoted(line, ',', offset); 866 if (end < 0) { 867 end = line.size(); 868 } 869 870 AString attr(line, offset, end - offset); 871 attr.trim(); 872 873 offset = end + 1; 874 875 ssize_t equalPos = attr.find("="); 876 if (equalPos < 0) { 877 continue; 878 } 879 880 AString key(attr, 0, equalPos); 881 key.trim(); 882 883 AString val(attr, equalPos + 1, attr.size() - equalPos - 1); 884 val.trim(); 885 886 ALOGV("key=%s value=%s", key.c_str(), val.c_str()); 887 888 if (!strcasecmp("type", key.c_str())) { 889 if (!strcasecmp("subtitles", val.c_str())) { 890 groupType = MediaGroup::TYPE_SUBS; 891 } else if (!strcasecmp("audio", val.c_str())) { 892 groupType = MediaGroup::TYPE_AUDIO; 893 } else if (!strcasecmp("video", val.c_str())) { 894 groupType = MediaGroup::TYPE_VIDEO; 895 } else { 896 ALOGE("Invalid media group type '%s'", val.c_str()); 897 return ERROR_MALFORMED; 898 } 899 900 haveGroupType = true; 901 } else if (!strcasecmp("group-id", key.c_str())) { 902 if (val.size() < 2 903 || val.c_str()[0] != '"' 904 || val.c_str()[val.size() - 1] != '"') { 905 ALOGE("Expected quoted string for GROUP-ID, got '%s' instead.", 906 val.c_str()); 907 908 return ERROR_MALFORMED; 909 } 910 911 groupID.setTo(val, 1, val.size() - 2); 912 haveGroupID = true; 913 } else if (!strcasecmp("language", key.c_str())) { 914 if (val.size() < 2 915 || val.c_str()[0] != '"' 916 || val.c_str()[val.size() - 1] != '"') { 917 ALOGE("Expected quoted string for LANGUAGE, got '%s' instead.", 918 val.c_str()); 919 920 return ERROR_MALFORMED; 921 } 922 923 groupLanguage.setTo(val, 1, val.size() - 2); 924 haveGroupLanguage = true; 925 } else if (!strcasecmp("name", key.c_str())) { 926 if (val.size() < 2 927 || val.c_str()[0] != '"' 928 || val.c_str()[val.size() - 1] != '"') { 929 ALOGE("Expected quoted string for NAME, got '%s' instead.", 930 val.c_str()); 931 932 return ERROR_MALFORMED; 933 } 934 935 groupName.setTo(val, 1, val.size() - 2); 936 haveGroupName = true; 937 } else if (!strcasecmp("autoselect", key.c_str())) { 938 groupAutoselect = false; 939 if (!strcasecmp("YES", val.c_str())) { 940 groupAutoselect = true; 941 } else if (!strcasecmp("NO", val.c_str())) { 942 groupAutoselect = false; 943 } else { 944 ALOGE("Expected YES or NO for AUTOSELECT attribute, " 945 "got '%s' instead.", 946 val.c_str()); 947 948 return ERROR_MALFORMED; 949 } 950 951 haveGroupAutoselect = true; 952 } else if (!strcasecmp("default", key.c_str())) { 953 groupDefault = false; 954 if (!strcasecmp("YES", val.c_str())) { 955 groupDefault = true; 956 } else if (!strcasecmp("NO", val.c_str())) { 957 groupDefault = false; 958 } else { 959 ALOGE("Expected YES or NO for DEFAULT attribute, " 960 "got '%s' instead.", 961 val.c_str()); 962 963 return ERROR_MALFORMED; 964 } 965 966 haveGroupDefault = true; 967 } else if (!strcasecmp("forced", key.c_str())) { 968 groupForced = false; 969 if (!strcasecmp("YES", val.c_str())) { 970 groupForced = true; 971 } else if (!strcasecmp("NO", val.c_str())) { 972 groupForced = false; 973 } else { 974 ALOGE("Expected YES or NO for FORCED attribute, " 975 "got '%s' instead.", 976 val.c_str()); 977 978 return ERROR_MALFORMED; 979 } 980 981 haveGroupForced = true; 982 } else if (!strcasecmp("uri", key.c_str())) { 983 if (val.size() < 2 984 || val.c_str()[0] != '"' 985 || val.c_str()[val.size() - 1] != '"') { 986 ALOGE("Expected quoted string for URI, got '%s' instead.", 987 val.c_str()); 988 989 return ERROR_MALFORMED; 990 } 991 992 AString tmp(val, 1, val.size() - 2); 993 994 if (!MakeURL(mBaseURI.c_str(), tmp.c_str(), &groupURI)) { 995 ALOGI("Failed to make absolute URI from '%s'.", tmp.c_str()); 996 } 997 998 haveGroupURI = true; 999 } 1000 } 1001 1002 if (!haveGroupType || !haveGroupID || !haveGroupName) { 1003 ALOGE("Incomplete EXT-X-MEDIA element."); 1004 return ERROR_MALFORMED; 1005 } 1006 1007 uint32_t flags = 0; 1008 if (haveGroupAutoselect && groupAutoselect) { 1009 flags |= MediaGroup::FLAG_AUTOSELECT; 1010 } 1011 if (haveGroupDefault && groupDefault) { 1012 flags |= MediaGroup::FLAG_DEFAULT; 1013 } 1014 if (haveGroupForced) { 1015 if (groupType != MediaGroup::TYPE_SUBS) { 1016 ALOGE("The FORCED attribute MUST not be present on anything " 1017 "but SUBS media."); 1018 1019 return ERROR_MALFORMED; 1020 } 1021 1022 if (groupForced) { 1023 flags |= MediaGroup::FLAG_FORCED; 1024 } 1025 } 1026 if (haveGroupLanguage) { 1027 flags |= MediaGroup::FLAG_HAS_LANGUAGE; 1028 } 1029 if (haveGroupURI) { 1030 flags |= MediaGroup::FLAG_HAS_URI; 1031 } 1032 1033 ssize_t groupIndex = mMediaGroups.indexOfKey(groupID); 1034 sp<MediaGroup> group; 1035 1036 if (groupIndex < 0) { 1037 group = new MediaGroup(groupType); 1038 mMediaGroups.add(groupID, group); 1039 } else { 1040 group = mMediaGroups.valueAt(groupIndex); 1041 1042 if (group->type() != groupType) { 1043 ALOGE("Attempt to put media item under group of different type " 1044 "(groupType = %d, item type = %d", 1045 group->type(), 1046 groupType); 1047 1048 return ERROR_MALFORMED; 1049 } 1050 } 1051 1052 return group->addMedia( 1053 groupName.c_str(), 1054 haveGroupURI ? groupURI.c_str() : NULL, 1055 haveGroupLanguage ? groupLanguage.c_str() : NULL, 1056 flags); 1057} 1058 1059// static 1060status_t M3UParser::ParseInt32(const char *s, int32_t *x) { 1061 char *end; 1062 long lval = strtol(s, &end, 10); 1063 1064 if (end == s || (*end != '\0' && *end != ',')) { 1065 return ERROR_MALFORMED; 1066 } 1067 1068 *x = (int32_t)lval; 1069 1070 return OK; 1071} 1072 1073// static 1074status_t M3UParser::ParseDouble(const char *s, double *x) { 1075 char *end; 1076 double dval = strtod(s, &end); 1077 1078 if (end == s || (*end != '\0' && *end != ',')) { 1079 return ERROR_MALFORMED; 1080 } 1081 1082 *x = dval; 1083 1084 return OK; 1085} 1086 1087} // namespace android 1088