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