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