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