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