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