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