M3UParser.cpp revision 9f434cfd021f60e26baf589dc34bf3839b832d4d
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 >= 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 == 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 != 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 codec.trim(); 369 // return true only if a codec of type `key` ("audio"/"video") 370 // is found. 371 if (codecIsType(codec, key)) { 372 return true; 373 } 374 offset = commaPos + 1; 375 } 376 return false; 377 } 378 } 379 380 sp<MediaGroup> group = mMediaGroups.valueFor(groupID); 381 if (!group->getActiveURI(uri)) { 382 return false; 383 } 384 385 if ((*uri).empty()) { 386 *uri = mItems.itemAt(index).mURI; 387 } 388 389 return true; 390} 391 392static bool MakeURL(const char *baseURL, const char *url, AString *out) { 393 out->clear(); 394 395 if (strncasecmp("http://", baseURL, 7) 396 && strncasecmp("https://", baseURL, 8) 397 && strncasecmp("file://", baseURL, 7)) { 398 // Base URL must be absolute 399 return false; 400 } 401 402 if (!strncasecmp("http://", url, 7) || !strncasecmp("https://", url, 8)) { 403 // "url" is already an absolute URL, ignore base URL. 404 out->setTo(url); 405 406 ALOGV("base:'%s', url:'%s' => '%s'", baseURL, url, out->c_str()); 407 408 return true; 409 } 410 411 if (url[0] == '/') { 412 // URL is an absolute path. 413 414 char *protocolEnd = strstr(baseURL, "//") + 2; 415 char *pathStart = strchr(protocolEnd, '/'); 416 417 if (pathStart != NULL) { 418 out->setTo(baseURL, pathStart - baseURL); 419 } else { 420 out->setTo(baseURL); 421 } 422 423 out->append(url); 424 } else { 425 // URL is a relative path 426 427 // Check for a possible query string 428 const char *qsPos = strchr(baseURL, '?'); 429 size_t end; 430 if (qsPos != NULL) { 431 end = qsPos - baseURL; 432 } else { 433 end = strlen(baseURL); 434 } 435 // Check for the last slash before a potential query string 436 for (ssize_t pos = end - 1; pos >= 0; pos--) { 437 if (baseURL[pos] == '/') { 438 end = pos; 439 break; 440 } 441 } 442 443 // Check whether the found slash actually is part of the path 444 // and not part of the "http://". 445 if (end > 6) { 446 out->setTo(baseURL, end); 447 } else { 448 out->setTo(baseURL); 449 } 450 451 out->append("/"); 452 out->append(url); 453 } 454 455 ALOGV("base:'%s', url:'%s' => '%s'", baseURL, url, out->c_str()); 456 457 return true; 458} 459 460status_t M3UParser::parse(const void *_data, size_t size) { 461 int32_t lineNo = 0; 462 463 sp<AMessage> itemMeta; 464 465 const char *data = (const char *)_data; 466 size_t offset = 0; 467 uint64_t segmentRangeOffset = 0; 468 while (offset < size) { 469 size_t offsetLF = offset; 470 while (offsetLF < size && data[offsetLF] != '\n') { 471 ++offsetLF; 472 } 473 474 AString line; 475 if (offsetLF > offset && data[offsetLF - 1] == '\r') { 476 line.setTo(&data[offset], offsetLF - offset - 1); 477 } else { 478 line.setTo(&data[offset], offsetLF - offset); 479 } 480 481 // ALOGI("#%s#", line.c_str()); 482 483 if (line.empty()) { 484 offset = offsetLF + 1; 485 continue; 486 } 487 488 if (lineNo == 0 && line == "#EXTM3U") { 489 mIsExtM3U = true; 490 } 491 492 if (mIsExtM3U) { 493 status_t err = OK; 494 495 if (line.startsWith("#EXT-X-TARGETDURATION")) { 496 if (mIsVariantPlaylist) { 497 return ERROR_MALFORMED; 498 } 499 err = parseMetaData(line, &mMeta, "target-duration"); 500 } else if (line.startsWith("#EXT-X-MEDIA-SEQUENCE")) { 501 if (mIsVariantPlaylist) { 502 return ERROR_MALFORMED; 503 } 504 err = parseMetaData(line, &mMeta, "media-sequence"); 505 } else if (line.startsWith("#EXT-X-KEY")) { 506 if (mIsVariantPlaylist) { 507 return ERROR_MALFORMED; 508 } 509 err = parseCipherInfo(line, &itemMeta, mBaseURI); 510 } else if (line.startsWith("#EXT-X-ENDLIST")) { 511 mIsComplete = true; 512 } else if (line.startsWith("#EXT-X-PLAYLIST-TYPE:EVENT")) { 513 mIsEvent = true; 514 } else if (line.startsWith("#EXTINF")) { 515 if (mIsVariantPlaylist) { 516 return ERROR_MALFORMED; 517 } 518 err = parseMetaDataDuration(line, &itemMeta, "durationUs"); 519 } else if (line.startsWith("#EXT-X-DISCONTINUITY")) { 520 if (mIsVariantPlaylist) { 521 return ERROR_MALFORMED; 522 } 523 if (itemMeta == NULL) { 524 itemMeta = new AMessage; 525 } 526 itemMeta->setInt32("discontinuity", true); 527 } else if (line.startsWith("#EXT-X-STREAM-INF")) { 528 if (mMeta != NULL) { 529 return ERROR_MALFORMED; 530 } 531 mIsVariantPlaylist = true; 532 err = parseStreamInf(line, &itemMeta); 533 } else if (line.startsWith("#EXT-X-BYTERANGE")) { 534 if (mIsVariantPlaylist) { 535 return ERROR_MALFORMED; 536 } 537 538 uint64_t length, offset; 539 err = parseByteRange(line, segmentRangeOffset, &length, &offset); 540 541 if (err == OK) { 542 if (itemMeta == NULL) { 543 itemMeta = new AMessage; 544 } 545 546 itemMeta->setInt64("range-offset", offset); 547 itemMeta->setInt64("range-length", length); 548 549 segmentRangeOffset = offset + length; 550 } 551 } else if (line.startsWith("#EXT-X-MEDIA")) { 552 err = parseMedia(line); 553 } 554 555 if (err != OK) { 556 return err; 557 } 558 } 559 560 if (!line.startsWith("#")) { 561 if (!mIsVariantPlaylist) { 562 int64_t durationUs; 563 if (itemMeta == NULL 564 || !itemMeta->findInt64("durationUs", &durationUs)) { 565 return ERROR_MALFORMED; 566 } 567 } 568 569 mItems.push(); 570 Item *item = &mItems.editItemAt(mItems.size() - 1); 571 572 CHECK(MakeURL(mBaseURI.c_str(), line.c_str(), &item->mURI)); 573 574 item->mMeta = itemMeta; 575 576 itemMeta.clear(); 577 } 578 579 offset = offsetLF + 1; 580 ++lineNo; 581 } 582 583 return OK; 584} 585 586// static 587status_t M3UParser::parseMetaData( 588 const AString &line, sp<AMessage> *meta, const char *key) { 589 ssize_t colonPos = line.find(":"); 590 591 if (colonPos < 0) { 592 return ERROR_MALFORMED; 593 } 594 595 int32_t x; 596 status_t err = ParseInt32(line.c_str() + colonPos + 1, &x); 597 598 if (err != OK) { 599 return err; 600 } 601 602 if (meta->get() == NULL) { 603 *meta = new AMessage; 604 } 605 (*meta)->setInt32(key, x); 606 607 return OK; 608} 609 610// static 611status_t M3UParser::parseMetaDataDuration( 612 const AString &line, sp<AMessage> *meta, const char *key) { 613 ssize_t colonPos = line.find(":"); 614 615 if (colonPos < 0) { 616 return ERROR_MALFORMED; 617 } 618 619 double x; 620 status_t err = ParseDouble(line.c_str() + colonPos + 1, &x); 621 622 if (err != OK) { 623 return err; 624 } 625 626 if (meta->get() == NULL) { 627 *meta = new AMessage; 628 } 629 (*meta)->setInt64(key, (int64_t)x * 1E6); 630 631 return OK; 632} 633 634// Find the next occurence of the character "what" at or after "offset", 635// but ignore occurences between quotation marks. 636// Return the index of the occurrence or -1 if not found. 637static ssize_t FindNextUnquoted( 638 const AString &line, char what, size_t offset) { 639 CHECK_NE((int)what, (int)'"'); 640 641 bool quoted = false; 642 while (offset < line.size()) { 643 char c = line.c_str()[offset]; 644 645 if (c == '"') { 646 quoted = !quoted; 647 } else if (c == what && !quoted) { 648 return offset; 649 } 650 651 ++offset; 652 } 653 654 return -1; 655} 656 657status_t M3UParser::parseStreamInf( 658 const AString &line, sp<AMessage> *meta) const { 659 ssize_t colonPos = line.find(":"); 660 661 if (colonPos < 0) { 662 return ERROR_MALFORMED; 663 } 664 665 size_t offset = colonPos + 1; 666 667 while (offset < line.size()) { 668 ssize_t end = FindNextUnquoted(line, ',', offset); 669 if (end < 0) { 670 end = line.size(); 671 } 672 673 AString attr(line, offset, end - offset); 674 attr.trim(); 675 676 offset = end + 1; 677 678 ssize_t equalPos = attr.find("="); 679 if (equalPos < 0) { 680 continue; 681 } 682 683 AString key(attr, 0, equalPos); 684 key.trim(); 685 686 AString val(attr, equalPos + 1, attr.size() - equalPos - 1); 687 val.trim(); 688 689 ALOGV("key=%s value=%s", key.c_str(), val.c_str()); 690 691 if (!strcasecmp("bandwidth", key.c_str())) { 692 const char *s = val.c_str(); 693 char *end; 694 unsigned long x = strtoul(s, &end, 10); 695 696 if (end == s || *end != '\0') { 697 // malformed 698 continue; 699 } 700 701 if (meta->get() == NULL) { 702 *meta = new AMessage; 703 } 704 (*meta)->setInt32("bandwidth", x); 705 } else if (!strcasecmp("codecs", key.c_str())) { 706 if (!isQuotedString(val)) { 707 ALOGE("Expected quoted string for %s attribute, " 708 "got '%s' instead.", 709 key.c_str(), val.c_str());; 710 711 return ERROR_MALFORMED; 712 } 713 714 key.tolower(); 715 const AString &codecs = unquoteString(val); 716 (*meta)->setString(key.c_str(), codecs.c_str()); 717 } else if (!strcasecmp("audio", key.c_str()) 718 || !strcasecmp("video", key.c_str()) 719 || !strcasecmp("subtitles", key.c_str())) { 720 if (!isQuotedString(val)) { 721 ALOGE("Expected quoted string for %s attribute, " 722 "got '%s' instead.", 723 key.c_str(), val.c_str()); 724 725 return ERROR_MALFORMED; 726 } 727 728 const AString &groupID = unquoteString(val); 729 ssize_t groupIndex = mMediaGroups.indexOfKey(groupID); 730 731 if (groupIndex < 0) { 732 ALOGE("Undefined media group '%s' referenced in stream info.", 733 groupID.c_str()); 734 735 return ERROR_MALFORMED; 736 } 737 738 key.tolower(); 739 (*meta)->setString(key.c_str(), groupID.c_str()); 740 } 741 } 742 743 return OK; 744} 745 746// static 747status_t M3UParser::parseCipherInfo( 748 const AString &line, sp<AMessage> *meta, const AString &baseURI) { 749 ssize_t colonPos = line.find(":"); 750 751 if (colonPos < 0) { 752 return ERROR_MALFORMED; 753 } 754 755 size_t offset = colonPos + 1; 756 757 while (offset < line.size()) { 758 ssize_t end = FindNextUnquoted(line, ',', offset); 759 if (end < 0) { 760 end = line.size(); 761 } 762 763 AString attr(line, offset, end - offset); 764 attr.trim(); 765 766 offset = end + 1; 767 768 ssize_t equalPos = attr.find("="); 769 if (equalPos < 0) { 770 continue; 771 } 772 773 AString key(attr, 0, equalPos); 774 key.trim(); 775 776 AString val(attr, equalPos + 1, attr.size() - equalPos - 1); 777 val.trim(); 778 779 ALOGV("key=%s value=%s", key.c_str(), val.c_str()); 780 781 key.tolower(); 782 783 if (key == "method" || key == "uri" || key == "iv") { 784 if (meta->get() == NULL) { 785 *meta = new AMessage; 786 } 787 788 if (key == "uri") { 789 if (val.size() >= 2 790 && val.c_str()[0] == '"' 791 && val.c_str()[val.size() - 1] == '"') { 792 // Remove surrounding quotes. 793 AString tmp(val, 1, val.size() - 2); 794 val = tmp; 795 } 796 797 AString absURI; 798 if (MakeURL(baseURI.c_str(), val.c_str(), &absURI)) { 799 val = absURI; 800 } else { 801 ALOGE("failed to make absolute url for '%s'.", 802 val.c_str()); 803 } 804 } 805 806 key.insert(AString("cipher-"), 0); 807 808 (*meta)->setString(key.c_str(), val.c_str(), val.size()); 809 } 810 } 811 812 return OK; 813} 814 815// static 816status_t M3UParser::parseByteRange( 817 const AString &line, uint64_t curOffset, 818 uint64_t *length, uint64_t *offset) { 819 ssize_t colonPos = line.find(":"); 820 821 if (colonPos < 0) { 822 return ERROR_MALFORMED; 823 } 824 825 ssize_t atPos = line.find("@", colonPos + 1); 826 827 AString lenStr; 828 if (atPos < 0) { 829 lenStr = AString(line, colonPos + 1, line.size() - colonPos - 1); 830 } else { 831 lenStr = AString(line, colonPos + 1, atPos - colonPos - 1); 832 } 833 834 lenStr.trim(); 835 836 const char *s = lenStr.c_str(); 837 char *end; 838 *length = strtoull(s, &end, 10); 839 840 if (s == end || *end != '\0') { 841 return ERROR_MALFORMED; 842 } 843 844 if (atPos >= 0) { 845 AString offStr = AString(line, atPos + 1, line.size() - atPos - 1); 846 offStr.trim(); 847 848 const char *s = offStr.c_str(); 849 *offset = strtoull(s, &end, 10); 850 851 if (s == end || *end != '\0') { 852 return ERROR_MALFORMED; 853 } 854 } else { 855 *offset = curOffset; 856 } 857 858 return OK; 859} 860 861status_t M3UParser::parseMedia(const AString &line) { 862 ssize_t colonPos = line.find(":"); 863 864 if (colonPos < 0) { 865 return ERROR_MALFORMED; 866 } 867 868 bool haveGroupType = false; 869 MediaGroup::Type groupType = MediaGroup::TYPE_AUDIO; 870 871 bool haveGroupID = false; 872 AString groupID; 873 874 bool haveGroupLanguage = false; 875 AString groupLanguage; 876 877 bool haveGroupName = false; 878 AString groupName; 879 880 bool haveGroupAutoselect = false; 881 bool groupAutoselect = false; 882 883 bool haveGroupDefault = false; 884 bool groupDefault = false; 885 886 bool haveGroupForced = false; 887 bool groupForced = false; 888 889 bool haveGroupURI = false; 890 AString groupURI; 891 892 size_t offset = colonPos + 1; 893 894 while (offset < line.size()) { 895 ssize_t end = FindNextUnquoted(line, ',', offset); 896 if (end < 0) { 897 end = line.size(); 898 } 899 900 AString attr(line, offset, end - offset); 901 attr.trim(); 902 903 offset = end + 1; 904 905 ssize_t equalPos = attr.find("="); 906 if (equalPos < 0) { 907 continue; 908 } 909 910 AString key(attr, 0, equalPos); 911 key.trim(); 912 913 AString val(attr, equalPos + 1, attr.size() - equalPos - 1); 914 val.trim(); 915 916 ALOGV("key=%s value=%s", key.c_str(), val.c_str()); 917 918 if (!strcasecmp("type", key.c_str())) { 919 if (!strcasecmp("subtitles", val.c_str())) { 920 groupType = MediaGroup::TYPE_SUBS; 921 } else if (!strcasecmp("audio", val.c_str())) { 922 groupType = MediaGroup::TYPE_AUDIO; 923 } else if (!strcasecmp("video", val.c_str())) { 924 groupType = MediaGroup::TYPE_VIDEO; 925 } else { 926 ALOGE("Invalid media group type '%s'", val.c_str()); 927 return ERROR_MALFORMED; 928 } 929 930 haveGroupType = true; 931 } else if (!strcasecmp("group-id", key.c_str())) { 932 if (val.size() < 2 933 || val.c_str()[0] != '"' 934 || val.c_str()[val.size() - 1] != '"') { 935 ALOGE("Expected quoted string for GROUP-ID, got '%s' instead.", 936 val.c_str()); 937 938 return ERROR_MALFORMED; 939 } 940 941 groupID.setTo(val, 1, val.size() - 2); 942 haveGroupID = true; 943 } else if (!strcasecmp("language", key.c_str())) { 944 if (val.size() < 2 945 || val.c_str()[0] != '"' 946 || val.c_str()[val.size() - 1] != '"') { 947 ALOGE("Expected quoted string for LANGUAGE, got '%s' instead.", 948 val.c_str()); 949 950 return ERROR_MALFORMED; 951 } 952 953 groupLanguage.setTo(val, 1, val.size() - 2); 954 haveGroupLanguage = true; 955 } else if (!strcasecmp("name", key.c_str())) { 956 if (val.size() < 2 957 || val.c_str()[0] != '"' 958 || val.c_str()[val.size() - 1] != '"') { 959 ALOGE("Expected quoted string for NAME, got '%s' instead.", 960 val.c_str()); 961 962 return ERROR_MALFORMED; 963 } 964 965 groupName.setTo(val, 1, val.size() - 2); 966 haveGroupName = true; 967 } else if (!strcasecmp("autoselect", key.c_str())) { 968 groupAutoselect = false; 969 if (!strcasecmp("YES", val.c_str())) { 970 groupAutoselect = true; 971 } else if (!strcasecmp("NO", val.c_str())) { 972 groupAutoselect = false; 973 } else { 974 ALOGE("Expected YES or NO for AUTOSELECT attribute, " 975 "got '%s' instead.", 976 val.c_str()); 977 978 return ERROR_MALFORMED; 979 } 980 981 haveGroupAutoselect = true; 982 } else if (!strcasecmp("default", key.c_str())) { 983 groupDefault = false; 984 if (!strcasecmp("YES", val.c_str())) { 985 groupDefault = true; 986 } else if (!strcasecmp("NO", val.c_str())) { 987 groupDefault = false; 988 } else { 989 ALOGE("Expected YES or NO for DEFAULT attribute, " 990 "got '%s' instead.", 991 val.c_str()); 992 993 return ERROR_MALFORMED; 994 } 995 996 haveGroupDefault = true; 997 } else if (!strcasecmp("forced", key.c_str())) { 998 groupForced = false; 999 if (!strcasecmp("YES", val.c_str())) { 1000 groupForced = true; 1001 } else if (!strcasecmp("NO", val.c_str())) { 1002 groupForced = false; 1003 } else { 1004 ALOGE("Expected YES or NO for FORCED attribute, " 1005 "got '%s' instead.", 1006 val.c_str()); 1007 1008 return ERROR_MALFORMED; 1009 } 1010 1011 haveGroupForced = true; 1012 } else if (!strcasecmp("uri", 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 URI, got '%s' instead.", 1017 val.c_str()); 1018 1019 return ERROR_MALFORMED; 1020 } 1021 1022 AString tmp(val, 1, val.size() - 2); 1023 1024 if (!MakeURL(mBaseURI.c_str(), tmp.c_str(), &groupURI)) { 1025 ALOGI("Failed to make absolute URI from '%s'.", tmp.c_str()); 1026 } 1027 1028 haveGroupURI = true; 1029 } 1030 } 1031 1032 if (!haveGroupType || !haveGroupID || !haveGroupName) { 1033 ALOGE("Incomplete EXT-X-MEDIA element."); 1034 return ERROR_MALFORMED; 1035 } 1036 1037 uint32_t flags = 0; 1038 if (haveGroupAutoselect && groupAutoselect) { 1039 flags |= MediaGroup::FLAG_AUTOSELECT; 1040 } 1041 if (haveGroupDefault && groupDefault) { 1042 flags |= MediaGroup::FLAG_DEFAULT; 1043 } 1044 if (haveGroupForced) { 1045 if (groupType != MediaGroup::TYPE_SUBS) { 1046 ALOGE("The FORCED attribute MUST not be present on anything " 1047 "but SUBS media."); 1048 1049 return ERROR_MALFORMED; 1050 } 1051 1052 if (groupForced) { 1053 flags |= MediaGroup::FLAG_FORCED; 1054 } 1055 } 1056 if (haveGroupLanguage) { 1057 flags |= MediaGroup::FLAG_HAS_LANGUAGE; 1058 } 1059 if (haveGroupURI) { 1060 flags |= MediaGroup::FLAG_HAS_URI; 1061 } 1062 1063 ssize_t groupIndex = mMediaGroups.indexOfKey(groupID); 1064 sp<MediaGroup> group; 1065 1066 if (groupIndex < 0) { 1067 group = new MediaGroup(groupType); 1068 mMediaGroups.add(groupID, group); 1069 } else { 1070 group = mMediaGroups.valueAt(groupIndex); 1071 1072 if (group->type() != groupType) { 1073 ALOGE("Attempt to put media item under group of different type " 1074 "(groupType = %d, item type = %d", 1075 group->type(), 1076 groupType); 1077 1078 return ERROR_MALFORMED; 1079 } 1080 } 1081 1082 return group->addMedia( 1083 groupName.c_str(), 1084 haveGroupURI ? groupURI.c_str() : NULL, 1085 haveGroupLanguage ? groupLanguage.c_str() : NULL, 1086 flags); 1087} 1088 1089// static 1090status_t M3UParser::ParseInt32(const char *s, int32_t *x) { 1091 char *end; 1092 long lval = strtol(s, &end, 10); 1093 1094 if (end == s || (*end != '\0' && *end != ',')) { 1095 return ERROR_MALFORMED; 1096 } 1097 1098 *x = (int32_t)lval; 1099 1100 return OK; 1101} 1102 1103// static 1104status_t M3UParser::ParseDouble(const char *s, double *x) { 1105 char *end; 1106 double dval = strtod(s, &end); 1107 1108 if (end == s || (*end != '\0' && *end != ',')) { 1109 return ERROR_MALFORMED; 1110 } 1111 1112 *x = dval; 1113 1114 return OK; 1115} 1116 1117// static 1118bool M3UParser::isQuotedString(const AString &str) { 1119 if (str.size() < 2 1120 || str.c_str()[0] != '"' 1121 || str.c_str()[str.size() - 1] != '"') { 1122 return false; 1123 } 1124 return true; 1125} 1126 1127// static 1128AString M3UParser::unquoteString(const AString &str) { 1129 if (!isQuotedString(str)) { 1130 return str; 1131 } 1132 return AString(str, 1, str.size() - 2); 1133} 1134 1135// static 1136bool M3UParser::codecIsType(const AString &codec, const char *type) { 1137 if (codec.size() < 4) { 1138 return false; 1139 } 1140 const char *c = codec.c_str(); 1141 switch (FOURCC(c[0], c[1], c[2], c[3])) { 1142 // List extracted from http://www.mp4ra.org/codecs.html 1143 case 'ac-3': 1144 case 'alac': 1145 case 'dra1': 1146 case 'dtsc': 1147 case 'dtse': 1148 case 'dtsh': 1149 case 'dtsl': 1150 case 'ec-3': 1151 case 'enca': 1152 case 'g719': 1153 case 'g726': 1154 case 'm4ae': 1155 case 'mlpa': 1156 case 'mp4a': 1157 case 'raw ': 1158 case 'samr': 1159 case 'sawb': 1160 case 'sawp': 1161 case 'sevc': 1162 case 'sqcp': 1163 case 'ssmv': 1164 case 'twos': 1165 case 'agsm': 1166 case 'alaw': 1167 case 'dvi ': 1168 case 'fl32': 1169 case 'fl64': 1170 case 'ima4': 1171 case 'in24': 1172 case 'in32': 1173 case 'lpcm': 1174 case 'Qclp': 1175 case 'QDM2': 1176 case 'QDMC': 1177 case 'ulaw': 1178 case 'vdva': 1179 return !strcmp("audio", type); 1180 1181 case 'avc1': 1182 case 'avc2': 1183 case 'avcp': 1184 case 'drac': 1185 case 'encv': 1186 case 'mjp2': 1187 case 'mp4v': 1188 case 'mvc1': 1189 case 'mvc2': 1190 case 'resv': 1191 case 's263': 1192 case 'svc1': 1193 case 'vc-1': 1194 case 'CFHD': 1195 case 'civd': 1196 case 'DV10': 1197 case 'dvh5': 1198 case 'dvh6': 1199 case 'dvhp': 1200 case 'DVOO': 1201 case 'DVOR': 1202 case 'DVTV': 1203 case 'DVVT': 1204 case 'flic': 1205 case 'gif ': 1206 case 'h261': 1207 case 'h263': 1208 case 'HD10': 1209 case 'jpeg': 1210 case 'M105': 1211 case 'mjpa': 1212 case 'mjpb': 1213 case 'png ': 1214 case 'PNTG': 1215 case 'rle ': 1216 case 'rpza': 1217 case 'Shr0': 1218 case 'Shr1': 1219 case 'Shr2': 1220 case 'Shr3': 1221 case 'Shr4': 1222 case 'SVQ1': 1223 case 'SVQ3': 1224 case 'tga ': 1225 case 'tiff': 1226 case 'WRLE': 1227 return !strcmp("video", type); 1228 1229 default: 1230 return false; 1231 } 1232} 1233 1234} // namespace android 1235