LiveSession.cpp revision 0f30bd90272c818aa37c0bb22d22eaa7d3689879
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 "LiveSession" 19#include <utils/Log.h> 20 21#include "include/LiveSession.h" 22 23#include "LiveDataSource.h" 24 25#include "include/M3UParser.h" 26#include "include/HTTPBase.h" 27 28#include <cutils/properties.h> 29#include <media/stagefright/foundation/hexdump.h> 30#include <media/stagefright/foundation/ABuffer.h> 31#include <media/stagefright/foundation/ADebug.h> 32#include <media/stagefright/foundation/AMessage.h> 33#include <media/stagefright/DataSource.h> 34#include <media/stagefright/FileSource.h> 35#include <media/stagefright/MediaErrors.h> 36 37#include <ctype.h> 38#include <openssl/aes.h> 39 40namespace android { 41 42const int64_t LiveSession::kMaxPlaylistAgeUs = 15000000ll; 43 44LiveSession::LiveSession(uint32_t flags) 45 : mFlags(flags), 46 mDataSource(new LiveDataSource), 47 mHTTPDataSource( 48 HTTPBase::Create( 49 (mFlags & kFlagIncognito) 50 ? HTTPBase::kFlagIncognito 51 : 0)), 52 mPrevBandwidthIndex(-1), 53 mLastPlaylistFetchTimeUs(-1), 54 mSeqNumber(-1), 55 mSeekTimeUs(-1), 56 mNumRetries(0), 57 mDurationUs(-1), 58 mSeekDone(false), 59 mDisconnectPending(false), 60 mMonitorQueueGeneration(0) { 61} 62 63LiveSession::~LiveSession() { 64} 65 66sp<DataSource> LiveSession::getDataSource() { 67 return mDataSource; 68} 69 70void LiveSession::connect( 71 const char *url, const KeyedVector<String8, String8> *headers) { 72 sp<AMessage> msg = new AMessage(kWhatConnect, id()); 73 msg->setString("url", url); 74 75 if (headers != NULL) { 76 msg->setPointer( 77 "headers", 78 new KeyedVector<String8, String8>(*headers)); 79 } 80 81 msg->post(); 82} 83 84void LiveSession::disconnect() { 85 Mutex::Autolock autoLock(mLock); 86 mDisconnectPending = true; 87 88 mHTTPDataSource->disconnect(); 89 90 (new AMessage(kWhatDisconnect, id()))->post(); 91} 92 93void LiveSession::seekTo(int64_t timeUs) { 94 Mutex::Autolock autoLock(mLock); 95 mSeekDone = false; 96 97 sp<AMessage> msg = new AMessage(kWhatSeek, id()); 98 msg->setInt64("timeUs", timeUs); 99 msg->post(); 100 101 while (!mSeekDone) { 102 mCondition.wait(mLock); 103 } 104} 105 106void LiveSession::onMessageReceived(const sp<AMessage> &msg) { 107 switch (msg->what()) { 108 case kWhatConnect: 109 onConnect(msg); 110 break; 111 112 case kWhatDisconnect: 113 onDisconnect(); 114 break; 115 116 case kWhatMonitorQueue: 117 { 118 int32_t generation; 119 CHECK(msg->findInt32("generation", &generation)); 120 121 if (generation != mMonitorQueueGeneration) { 122 // Stale event 123 break; 124 } 125 126 onMonitorQueue(); 127 break; 128 } 129 130 case kWhatSeek: 131 onSeek(msg); 132 break; 133 134 default: 135 TRESPASS(); 136 break; 137 } 138} 139 140// static 141int LiveSession::SortByBandwidth(const BandwidthItem *a, const BandwidthItem *b) { 142 if (a->mBandwidth < b->mBandwidth) { 143 return -1; 144 } else if (a->mBandwidth == b->mBandwidth) { 145 return 0; 146 } 147 148 return 1; 149} 150 151void LiveSession::onConnect(const sp<AMessage> &msg) { 152 AString url; 153 CHECK(msg->findString("url", &url)); 154 155 KeyedVector<String8, String8> *headers = NULL; 156 if (!msg->findPointer("headers", (void **)&headers)) { 157 mExtraHeaders.clear(); 158 } else { 159 mExtraHeaders = *headers; 160 161 delete headers; 162 headers = NULL; 163 } 164 165 if (!(mFlags & kFlagIncognito)) { 166 LOGI("onConnect '%s'", url.c_str()); 167 } else { 168 LOGI("onConnect <URL suppressed>"); 169 } 170 171 mMasterURL = url; 172 173 sp<M3UParser> playlist = fetchPlaylist(url.c_str()); 174 175 if (playlist == NULL) { 176 LOGE("unable to fetch master playlist '%s'.", url.c_str()); 177 178 mDataSource->queueEOS(ERROR_IO); 179 return; 180 } 181 182 if (playlist->isVariantPlaylist()) { 183 for (size_t i = 0; i < playlist->size(); ++i) { 184 BandwidthItem item; 185 186 sp<AMessage> meta; 187 playlist->itemAt(i, &item.mURI, &meta); 188 189 unsigned long bandwidth; 190 CHECK(meta->findInt32("bandwidth", (int32_t *)&item.mBandwidth)); 191 192 mBandwidthItems.push(item); 193 } 194 195 CHECK_GT(mBandwidthItems.size(), 0u); 196 197 mBandwidthItems.sort(SortByBandwidth); 198 } 199 200 postMonitorQueue(); 201} 202 203void LiveSession::onDisconnect() { 204 LOGI("onDisconnect"); 205 206 mDataSource->queueEOS(ERROR_END_OF_STREAM); 207 208 Mutex::Autolock autoLock(mLock); 209 mDisconnectPending = false; 210} 211 212status_t LiveSession::fetchFile(const char *url, sp<ABuffer> *out) { 213 *out = NULL; 214 215 sp<DataSource> source; 216 217 if (!strncasecmp(url, "file://", 7)) { 218 source = new FileSource(url + 7); 219 } else if (strncasecmp(url, "http://", 7) 220 && strncasecmp(url, "https://", 8)) { 221 return ERROR_UNSUPPORTED; 222 } else { 223 { 224 Mutex::Autolock autoLock(mLock); 225 226 if (mDisconnectPending) { 227 return ERROR_IO; 228 } 229 } 230 231 status_t err = mHTTPDataSource->connect( 232 url, mExtraHeaders.isEmpty() ? NULL : &mExtraHeaders); 233 234 if (err != OK) { 235 return err; 236 } 237 238 source = mHTTPDataSource; 239 } 240 241 off64_t size; 242 status_t err = source->getSize(&size); 243 244 if (err != OK) { 245 size = 65536; 246 } 247 248 sp<ABuffer> buffer = new ABuffer(size); 249 buffer->setRange(0, 0); 250 251 for (;;) { 252 size_t bufferRemaining = buffer->capacity() - buffer->size(); 253 254 if (bufferRemaining == 0) { 255 bufferRemaining = 32768; 256 257 LOGV("increasing download buffer to %d bytes", 258 buffer->size() + bufferRemaining); 259 260 sp<ABuffer> copy = new ABuffer(buffer->size() + bufferRemaining); 261 memcpy(copy->data(), buffer->data(), buffer->size()); 262 copy->setRange(0, buffer->size()); 263 264 buffer = copy; 265 } 266 267 ssize_t n = source->readAt( 268 buffer->size(), buffer->data() + buffer->size(), 269 bufferRemaining); 270 271 if (n < 0) { 272 return n; 273 } 274 275 if (n == 0) { 276 break; 277 } 278 279 buffer->setRange(0, buffer->size() + (size_t)n); 280 } 281 282 *out = buffer; 283 284 return OK; 285} 286 287sp<M3UParser> LiveSession::fetchPlaylist(const char *url) { 288 sp<ABuffer> buffer; 289 status_t err = fetchFile(url, &buffer); 290 291 if (err != OK) { 292 return NULL; 293 } 294 295 sp<M3UParser> playlist = 296 new M3UParser(url, buffer->data(), buffer->size()); 297 298 if (playlist->initCheck() != OK) { 299 LOGE("failed to parse .m3u8 playlist"); 300 301 return NULL; 302 } 303 304 return playlist; 305} 306 307static double uniformRand() { 308 return (double)rand() / RAND_MAX; 309} 310 311size_t LiveSession::getBandwidthIndex() { 312 if (mBandwidthItems.size() == 0) { 313 return 0; 314 } 315 316#if 1 317 int32_t bandwidthBps; 318 if (mHTTPDataSource != NULL 319 && mHTTPDataSource->estimateBandwidth(&bandwidthBps)) { 320 LOGV("bandwidth estimated at %.2f kbps", bandwidthBps / 1024.0f); 321 } else { 322 LOGV("no bandwidth estimate."); 323 return 0; // Pick the lowest bandwidth stream by default. 324 } 325 326 char value[PROPERTY_VALUE_MAX]; 327 if (property_get("media.httplive.max-bw", value, NULL)) { 328 char *end; 329 long maxBw = strtoul(value, &end, 10); 330 if (end > value && *end == '\0') { 331 if (maxBw > 0 && bandwidthBps > maxBw) { 332 LOGV("bandwidth capped to %ld bps", maxBw); 333 bandwidthBps = maxBw; 334 } 335 } 336 } 337 338 // Consider only 80% of the available bandwidth usable. 339 bandwidthBps = (bandwidthBps * 8) / 10; 340 341 // Pick the highest bandwidth stream below or equal to estimated bandwidth. 342 343 size_t index = mBandwidthItems.size() - 1; 344 while (index > 0 && mBandwidthItems.itemAt(index).mBandwidth 345 > (size_t)bandwidthBps) { 346 --index; 347 } 348#elif 0 349 // Change bandwidth at random() 350 size_t index = uniformRand() * mBandwidthItems.size(); 351#elif 0 352 // There's a 50% chance to stay on the current bandwidth and 353 // a 50% chance to switch to the next higher bandwidth (wrapping around 354 // to lowest) 355 const size_t kMinIndex = 0; 356 357 size_t index; 358 if (mPrevBandwidthIndex < 0) { 359 index = kMinIndex; 360 } else if (uniformRand() < 0.5) { 361 index = (size_t)mPrevBandwidthIndex; 362 } else { 363 index = mPrevBandwidthIndex + 1; 364 if (index == mBandwidthItems.size()) { 365 index = kMinIndex; 366 } 367 } 368#elif 0 369 // Pick the highest bandwidth stream below or equal to 1.2 Mbit/sec 370 371 size_t index = mBandwidthItems.size() - 1; 372 while (index > 0 && mBandwidthItems.itemAt(index).mBandwidth > 1200000) { 373 --index; 374 } 375#else 376 size_t index = mBandwidthItems.size() - 1; // Highest bandwidth stream 377#endif 378 379 return index; 380} 381 382void LiveSession::onDownloadNext() { 383 size_t bandwidthIndex = getBandwidthIndex(); 384 385rinse_repeat: 386 int64_t nowUs = ALooper::GetNowUs(); 387 388 if (mLastPlaylistFetchTimeUs < 0 389 || (ssize_t)bandwidthIndex != mPrevBandwidthIndex 390 || (!mPlaylist->isComplete() 391 && mLastPlaylistFetchTimeUs + kMaxPlaylistAgeUs <= nowUs)) { 392 AString url; 393 if (mBandwidthItems.size() > 0) { 394 url = mBandwidthItems.editItemAt(bandwidthIndex).mURI; 395 } else { 396 url = mMasterURL; 397 } 398 399 bool firstTime = (mPlaylist == NULL); 400 401 mPlaylist = fetchPlaylist(url.c_str()); 402 if (mPlaylist == NULL) { 403 LOGE("failed to load playlist at url '%s'", url.c_str()); 404 mDataSource->queueEOS(ERROR_IO); 405 return; 406 } 407 408 if (firstTime) { 409 Mutex::Autolock autoLock(mLock); 410 411 if (!mPlaylist->isComplete()) { 412 mDurationUs = -1; 413 } else { 414 mDurationUs = 0; 415 for (size_t i = 0; i < mPlaylist->size(); ++i) { 416 sp<AMessage> itemMeta; 417 CHECK(mPlaylist->itemAt( 418 i, NULL /* uri */, &itemMeta)); 419 420 int64_t itemDurationUs; 421 CHECK(itemMeta->findInt64("durationUs", &itemDurationUs)); 422 423 mDurationUs += itemDurationUs; 424 } 425 } 426 } 427 428 mLastPlaylistFetchTimeUs = ALooper::GetNowUs(); 429 } 430 431 int32_t firstSeqNumberInPlaylist; 432 if (mPlaylist->meta() == NULL || !mPlaylist->meta()->findInt32( 433 "media-sequence", &firstSeqNumberInPlaylist)) { 434 firstSeqNumberInPlaylist = 0; 435 } 436 437 bool explicitDiscontinuity = false; 438 bool bandwidthChanged = false; 439 440 if (mSeekTimeUs >= 0) { 441 if (mPlaylist->isComplete()) { 442 size_t index = 0; 443 int64_t segmentStartUs = 0; 444 while (index < mPlaylist->size()) { 445 sp<AMessage> itemMeta; 446 CHECK(mPlaylist->itemAt( 447 index, NULL /* uri */, &itemMeta)); 448 449 int64_t itemDurationUs; 450 CHECK(itemMeta->findInt64("durationUs", &itemDurationUs)); 451 452 if (mSeekTimeUs < segmentStartUs + itemDurationUs) { 453 break; 454 } 455 456 segmentStartUs += itemDurationUs; 457 ++index; 458 } 459 460 if (index < mPlaylist->size()) { 461 int32_t newSeqNumber = firstSeqNumberInPlaylist + index; 462 463 if (newSeqNumber != mSeqNumber) { 464 LOGI("seeking to seq no %d", newSeqNumber); 465 466 mSeqNumber = newSeqNumber; 467 468 mDataSource->reset(); 469 470 // reseting the data source will have had the 471 // side effect of discarding any previously queued 472 // bandwidth change discontinuity. 473 // Therefore we'll need to treat these explicit 474 // discontinuities as involving a bandwidth change 475 // even if they aren't directly. 476 explicitDiscontinuity = true; 477 bandwidthChanged = true; 478 } 479 } 480 } 481 482 mSeekTimeUs = -1; 483 484 Mutex::Autolock autoLock(mLock); 485 mSeekDone = true; 486 mCondition.broadcast(); 487 } 488 489 if (mSeqNumber < 0) { 490 if (mPlaylist->isComplete()) { 491 mSeqNumber = firstSeqNumberInPlaylist; 492 } else { 493 mSeqNumber = firstSeqNumberInPlaylist + mPlaylist->size() / 2; 494 } 495 } 496 497 int32_t lastSeqNumberInPlaylist = 498 firstSeqNumberInPlaylist + (int32_t)mPlaylist->size() - 1; 499 500 if (mSeqNumber < firstSeqNumberInPlaylist 501 || mSeqNumber > lastSeqNumberInPlaylist) { 502 if (mPrevBandwidthIndex != (ssize_t)bandwidthIndex) { 503 // Go back to the previous bandwidth. 504 505 LOGI("new bandwidth does not have the sequence number " 506 "we're looking for, switching back to previous bandwidth"); 507 508 mLastPlaylistFetchTimeUs = -1; 509 bandwidthIndex = mPrevBandwidthIndex; 510 goto rinse_repeat; 511 } 512 513 if (!mPlaylist->isComplete() 514 && mSeqNumber > lastSeqNumberInPlaylist 515 && mNumRetries < kMaxNumRetries) { 516 ++mNumRetries; 517 518 mLastPlaylistFetchTimeUs = -1; 519 postMonitorQueue(3000000ll); 520 return; 521 } 522 523 LOGE("Cannot find sequence number %d in playlist " 524 "(contains %d - %d)", 525 mSeqNumber, firstSeqNumberInPlaylist, 526 firstSeqNumberInPlaylist + mPlaylist->size() - 1); 527 528 mDataSource->queueEOS(ERROR_END_OF_STREAM); 529 return; 530 } 531 532 mNumRetries = 0; 533 534 AString uri; 535 sp<AMessage> itemMeta; 536 CHECK(mPlaylist->itemAt( 537 mSeqNumber - firstSeqNumberInPlaylist, 538 &uri, 539 &itemMeta)); 540 541 int32_t val; 542 if (itemMeta->findInt32("discontinuity", &val) && val != 0) { 543 explicitDiscontinuity = true; 544 } 545 546 sp<ABuffer> buffer; 547 status_t err = fetchFile(uri.c_str(), &buffer); 548 if (err != OK) { 549 LOGE("failed to fetch .ts segment at url '%s'", uri.c_str()); 550 mDataSource->queueEOS(err); 551 return; 552 } 553 554 CHECK(buffer != NULL); 555 556 err = decryptBuffer(mSeqNumber - firstSeqNumberInPlaylist, buffer); 557 558 if (err != OK) { 559 LOGE("decryptBuffer failed w/ error %d", err); 560 561 mDataSource->queueEOS(err); 562 return; 563 } 564 565 if (buffer->size() == 0 || buffer->data()[0] != 0x47) { 566 // Not a transport stream??? 567 568 LOGE("This doesn't look like a transport stream..."); 569 570 mBandwidthItems.removeAt(bandwidthIndex); 571 572 if (mBandwidthItems.isEmpty()) { 573 mDataSource->queueEOS(ERROR_UNSUPPORTED); 574 return; 575 } 576 577 LOGI("Retrying with a different bandwidth stream."); 578 579 mLastPlaylistFetchTimeUs = -1; 580 bandwidthIndex = getBandwidthIndex(); 581 mPrevBandwidthIndex = bandwidthIndex; 582 mSeqNumber = -1; 583 584 goto rinse_repeat; 585 } 586 587 if ((size_t)mPrevBandwidthIndex != bandwidthIndex) { 588 bandwidthChanged = true; 589 } 590 591 if (mPrevBandwidthIndex < 0) { 592 // Don't signal a bandwidth change at the very beginning of 593 // playback. 594 bandwidthChanged = false; 595 } 596 597 if (explicitDiscontinuity || bandwidthChanged) { 598 // Signal discontinuity. 599 600 LOGI("queueing discontinuity (explicit=%d, bandwidthChanged=%d)", 601 explicitDiscontinuity, bandwidthChanged); 602 603 sp<ABuffer> tmp = new ABuffer(188); 604 memset(tmp->data(), 0, tmp->size()); 605 tmp->data()[1] = bandwidthChanged; 606 607 mDataSource->queueBuffer(tmp); 608 } 609 610 mDataSource->queueBuffer(buffer); 611 612 mPrevBandwidthIndex = bandwidthIndex; 613 ++mSeqNumber; 614 615 postMonitorQueue(); 616} 617 618void LiveSession::onMonitorQueue() { 619 if (mSeekTimeUs >= 0 620 || mDataSource->countQueuedBuffers() < kMaxNumQueuedFragments) { 621 onDownloadNext(); 622 } else { 623 postMonitorQueue(1000000ll); 624 } 625} 626 627status_t LiveSession::decryptBuffer( 628 size_t playlistIndex, const sp<ABuffer> &buffer) { 629 sp<AMessage> itemMeta; 630 bool found = false; 631 AString method; 632 633 for (ssize_t i = playlistIndex; i >= 0; --i) { 634 AString uri; 635 CHECK(mPlaylist->itemAt(i, &uri, &itemMeta)); 636 637 if (itemMeta->findString("cipher-method", &method)) { 638 found = true; 639 break; 640 } 641 } 642 643 if (!found) { 644 method = "NONE"; 645 } 646 647 if (method == "NONE") { 648 return OK; 649 } else if (!(method == "AES-128")) { 650 LOGE("Unsupported cipher method '%s'", method.c_str()); 651 return ERROR_UNSUPPORTED; 652 } 653 654 AString keyURI; 655 if (!itemMeta->findString("cipher-uri", &keyURI)) { 656 LOGE("Missing key uri"); 657 return ERROR_MALFORMED; 658 } 659 660 ssize_t index = mAESKeyForURI.indexOfKey(keyURI); 661 662 sp<ABuffer> key; 663 if (index >= 0) { 664 key = mAESKeyForURI.valueAt(index); 665 } else { 666 key = new ABuffer(16); 667 668 sp<HTTPBase> keySource = 669 HTTPBase::Create( 670 (mFlags & kFlagIncognito) 671 ? HTTPBase::kFlagIncognito 672 : 0); 673 674 status_t err = keySource->connect(keyURI.c_str()); 675 676 if (err == OK) { 677 size_t offset = 0; 678 while (offset < 16) { 679 ssize_t n = keySource->readAt( 680 offset, key->data() + offset, 16 - offset); 681 if (n <= 0) { 682 err = ERROR_IO; 683 break; 684 } 685 686 offset += n; 687 } 688 } 689 690 if (err != OK) { 691 LOGE("failed to fetch cipher key from '%s'.", keyURI.c_str()); 692 return ERROR_IO; 693 } 694 695 mAESKeyForURI.add(keyURI, key); 696 } 697 698 AES_KEY aes_key; 699 if (AES_set_decrypt_key(key->data(), 128, &aes_key) != 0) { 700 LOGE("failed to set AES decryption key."); 701 return UNKNOWN_ERROR; 702 } 703 704 unsigned char aes_ivec[16]; 705 706 AString iv; 707 if (itemMeta->findString("cipher-iv", &iv)) { 708 if ((!iv.startsWith("0x") && !iv.startsWith("0X")) 709 || iv.size() != 16 * 2 + 2) { 710 LOGE("malformed cipher IV '%s'.", iv.c_str()); 711 return ERROR_MALFORMED; 712 } 713 714 memset(aes_ivec, 0, sizeof(aes_ivec)); 715 for (size_t i = 0; i < 16; ++i) { 716 char c1 = tolower(iv.c_str()[2 + 2 * i]); 717 char c2 = tolower(iv.c_str()[3 + 2 * i]); 718 if (!isxdigit(c1) || !isxdigit(c2)) { 719 LOGE("malformed cipher IV '%s'.", iv.c_str()); 720 return ERROR_MALFORMED; 721 } 722 uint8_t nibble1 = isdigit(c1) ? c1 - '0' : c1 - 'a' + 10; 723 uint8_t nibble2 = isdigit(c2) ? c2 - '0' : c2 - 'a' + 10; 724 725 aes_ivec[i] = nibble1 << 4 | nibble2; 726 } 727 } else { 728 memset(aes_ivec, 0, sizeof(aes_ivec)); 729 aes_ivec[15] = mSeqNumber & 0xff; 730 aes_ivec[14] = (mSeqNumber >> 8) & 0xff; 731 aes_ivec[13] = (mSeqNumber >> 16) & 0xff; 732 aes_ivec[12] = (mSeqNumber >> 24) & 0xff; 733 } 734 735 AES_cbc_encrypt( 736 buffer->data(), buffer->data(), buffer->size(), 737 &aes_key, aes_ivec, AES_DECRYPT); 738 739 // hexdump(buffer->data(), buffer->size()); 740 741 size_t n = buffer->size(); 742 CHECK_GT(n, 0u); 743 744 size_t pad = buffer->data()[n - 1]; 745 746 CHECK_GT(pad, 0u); 747 CHECK_LE(pad, 16u); 748 CHECK_GE((size_t)n, pad); 749 for (size_t i = 0; i < pad; ++i) { 750 CHECK_EQ((unsigned)buffer->data()[n - 1 - i], pad); 751 } 752 753 n -= pad; 754 755 buffer->setRange(buffer->offset(), n); 756 757 return OK; 758} 759 760void LiveSession::postMonitorQueue(int64_t delayUs) { 761 sp<AMessage> msg = new AMessage(kWhatMonitorQueue, id()); 762 msg->setInt32("generation", ++mMonitorQueueGeneration); 763 msg->post(delayUs); 764} 765 766void LiveSession::onSeek(const sp<AMessage> &msg) { 767 int64_t timeUs; 768 CHECK(msg->findInt64("timeUs", &timeUs)); 769 770 mSeekTimeUs = timeUs; 771 postMonitorQueue(); 772} 773 774status_t LiveSession::getDuration(int64_t *durationUs) { 775 Mutex::Autolock autoLock(mLock); 776 *durationUs = mDurationUs; 777 778 return OK; 779} 780 781bool LiveSession::isSeekable() { 782 int64_t durationUs; 783 return getDuration(&durationUs) == OK && durationUs >= 0; 784} 785 786} // namespace android 787 788