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