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