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