diff options
Diffstat (limited to 'media/libstagefright/httplive/LiveSession.cpp')
-rw-r--r-- | media/libstagefright/httplive/LiveSession.cpp | 1249 |
1 files changed, 677 insertions, 572 deletions
diff --git a/media/libstagefright/httplive/LiveSession.cpp b/media/libstagefright/httplive/LiveSession.cpp index 505bdb3..bd12ddc 100644 --- a/media/libstagefright/httplive/LiveSession.cpp +++ b/media/libstagefright/httplive/LiveSession.cpp @@ -18,12 +18,13 @@ #define LOG_TAG "LiveSession" #include <utils/Log.h> -#include "include/LiveSession.h" +#include "LiveSession.h" -#include "LiveDataSource.h" +#include "M3UParser.h" +#include "PlaylistFetcher.h" -#include "include/M3UParser.h" #include "include/HTTPBase.h" +#include "mpeg2ts/AnotherPacketSource.h" #include <cutils/properties.h> #include <media/stagefright/foundation/hexdump.h> @@ -33,6 +34,8 @@ #include <media/stagefright/DataSource.h> #include <media/stagefright/FileSource.h> #include <media/stagefright/MediaErrors.h> +#include <media/stagefright/MetaData.h> +#include <media/stagefright/Utils.h> #include <ctype.h> #include <openssl/aes.h> @@ -47,37 +50,115 @@ LiveSession::LiveSession( mUIDValid(uidValid), mUID(uid), mInPreparationPhase(true), - mDataSource(new LiveDataSource), mHTTPDataSource( HTTPBase::Create( (mFlags & kFlagIncognito) ? HTTPBase::kFlagIncognito : 0)), mPrevBandwidthIndex(-1), - mLastPlaylistFetchTimeUs(-1), - mSeqNumber(-1), - mSeekTimeUs(-1), - mNumRetries(0), - mStartOfPlayback(true), - mDurationUs(-1), - mDurationFixed(false), - mSeekDone(false), - mDisconnectPending(false), - mMonitorQueueGeneration(0), - mRefreshState(INITIAL_MINIMUM_RELOAD_DELAY) { + mStreamMask(0), + mCheckBandwidthGeneration(0), + mLastDequeuedTimeUs(0ll), + mRealTimeBaseUs(0ll), + mReconfigurationInProgress(false), + mDisconnectReplyID(0) { if (mUIDValid) { mHTTPDataSource->setUID(mUID); } + + mPacketSources.add( + STREAMTYPE_AUDIO, new AnotherPacketSource(NULL /* meta */)); + + mPacketSources.add( + STREAMTYPE_VIDEO, new AnotherPacketSource(NULL /* meta */)); + + mPacketSources.add( + STREAMTYPE_SUBTITLES, new AnotherPacketSource(NULL /* meta */)); } LiveSession::~LiveSession() { } -sp<DataSource> LiveSession::getDataSource() { - return mDataSource; +status_t LiveSession::dequeueAccessUnit( + StreamType stream, sp<ABuffer> *accessUnit) { + if (!(mStreamMask & stream)) { + return UNKNOWN_ERROR; + } + + sp<AnotherPacketSource> packetSource = mPacketSources.valueFor(stream); + + status_t finalResult; + if (!packetSource->hasBufferAvailable(&finalResult)) { + return finalResult == OK ? -EAGAIN : finalResult; + } + + status_t err = packetSource->dequeueAccessUnit(accessUnit); + + const char *streamStr; + switch (stream) { + case STREAMTYPE_AUDIO: + streamStr = "audio"; + break; + case STREAMTYPE_VIDEO: + streamStr = "video"; + break; + case STREAMTYPE_SUBTITLES: + streamStr = "subs"; + break; + default: + TRESPASS(); + } + + if (err == INFO_DISCONTINUITY) { + int32_t type; + CHECK((*accessUnit)->meta()->findInt32("discontinuity", &type)); + + sp<AMessage> extra; + if (!(*accessUnit)->meta()->findMessage("extra", &extra)) { + extra.clear(); + } + + ALOGI("[%s] read discontinuity of type %d, extra = %s", + streamStr, + type, + extra == NULL ? "NULL" : extra->debugString().c_str()); + } else if (err == OK) { + if (stream == STREAMTYPE_AUDIO || stream == STREAMTYPE_VIDEO) { + int64_t timeUs; + CHECK((*accessUnit)->meta()->findInt64("timeUs", &timeUs)); + ALOGV("[%s] read buffer at time %lld us", streamStr, timeUs); + + mLastDequeuedTimeUs = timeUs; + mRealTimeBaseUs = ALooper::GetNowUs() - timeUs; + } else if (stream == STREAMTYPE_SUBTITLES) { + (*accessUnit)->meta()->setInt32( + "trackIndex", mPlaylist->getSelectedIndex()); + (*accessUnit)->meta()->setInt64("baseUs", mRealTimeBaseUs); + } + } else { + ALOGI("[%s] encountered error %d", streamStr, err); + } + + return err; +} + +status_t LiveSession::getStreamFormat(StreamType stream, sp<AMessage> *format) { + if (!(mStreamMask & stream)) { + return UNKNOWN_ERROR; + } + + sp<AnotherPacketSource> packetSource = mPacketSources.valueFor(stream); + + sp<MetaData> meta = packetSource->getFormat(); + + if (meta == NULL) { + return -EAGAIN; + } + + return convertMetaDataToMessage(meta, format); } -void LiveSession::connect( +void LiveSession::connectAsync( const char *url, const KeyedVector<String8, String8> *headers) { sp<AMessage> msg = new AMessage(kWhatConnect, id()); msg->setString("url", url); @@ -91,55 +172,190 @@ void LiveSession::connect( msg->post(); } -void LiveSession::disconnect() { - Mutex::Autolock autoLock(mLock); - mDisconnectPending = true; +status_t LiveSession::disconnect() { + sp<AMessage> msg = new AMessage(kWhatDisconnect, id()); - mHTTPDataSource->disconnect(); + sp<AMessage> response; + status_t err = msg->postAndAwaitResponse(&response); - (new AMessage(kWhatDisconnect, id()))->post(); + return err; } -void LiveSession::seekTo(int64_t timeUs) { - Mutex::Autolock autoLock(mLock); - mSeekDone = false; - +status_t LiveSession::seekTo(int64_t timeUs) { sp<AMessage> msg = new AMessage(kWhatSeek, id()); msg->setInt64("timeUs", timeUs); - msg->post(); - while (!mSeekDone) { - mCondition.wait(mLock); - } + sp<AMessage> response; + status_t err = msg->postAndAwaitResponse(&response); + + return err; } void LiveSession::onMessageReceived(const sp<AMessage> &msg) { switch (msg->what()) { case kWhatConnect: + { onConnect(msg); break; + } case kWhatDisconnect: - onDisconnect(); + { + CHECK(msg->senderAwaitsResponse(&mDisconnectReplyID)); + + if (mReconfigurationInProgress) { + break; + } + + finishDisconnect(); break; + } + + case kWhatSeek: + { + uint32_t replyID; + CHECK(msg->senderAwaitsResponse(&replyID)); + + status_t err = onSeek(msg); + + sp<AMessage> response = new AMessage; + response->setInt32("err", err); + + response->postReply(replyID); + break; + } + + case kWhatFetcherNotify: + { + int32_t what; + CHECK(msg->findInt32("what", &what)); + + switch (what) { + case PlaylistFetcher::kWhatStarted: + break; + case PlaylistFetcher::kWhatPaused: + case PlaylistFetcher::kWhatStopped: + { + if (what == PlaylistFetcher::kWhatStopped) { + AString uri; + CHECK(msg->findString("uri", &uri)); + mFetcherInfos.removeItem(uri); + } + + if (mContinuation != NULL) { + CHECK_GT(mContinuationCounter, 0); + if (--mContinuationCounter == 0) { + mContinuation->post(); + } + } + break; + } + + case PlaylistFetcher::kWhatDurationUpdate: + { + AString uri; + CHECK(msg->findString("uri", &uri)); + + int64_t durationUs; + CHECK(msg->findInt64("durationUs", &durationUs)); + + FetcherInfo *info = &mFetcherInfos.editValueFor(uri); + info->mDurationUs = durationUs; + break; + } + + case PlaylistFetcher::kWhatError: + { + status_t err; + CHECK(msg->findInt32("err", &err)); + + ALOGE("XXX Received error %d from PlaylistFetcher.", err); - case kWhatMonitorQueue: + if (mInPreparationPhase) { + postPrepared(err); + } + + mPacketSources.valueFor(STREAMTYPE_AUDIO)->signalEOS(err); + + mPacketSources.valueFor(STREAMTYPE_VIDEO)->signalEOS(err); + + mPacketSources.valueFor( + STREAMTYPE_SUBTITLES)->signalEOS(err); + + sp<AMessage> notify = mNotify->dup(); + notify->setInt32("what", kWhatError); + notify->setInt32("err", err); + notify->post(); + break; + } + + case PlaylistFetcher::kWhatTemporarilyDoneFetching: + { + AString uri; + CHECK(msg->findString("uri", &uri)); + + FetcherInfo *info = &mFetcherInfos.editValueFor(uri); + info->mIsPrepared = true; + + if (mInPreparationPhase) { + bool allFetchersPrepared = true; + for (size_t i = 0; i < mFetcherInfos.size(); ++i) { + if (!mFetcherInfos.valueAt(i).mIsPrepared) { + allFetchersPrepared = false; + break; + } + } + + if (allFetchersPrepared) { + postPrepared(OK); + } + } + break; + } + + default: + TRESPASS(); + } + + break; + } + + case kWhatCheckBandwidth: { int32_t generation; CHECK(msg->findInt32("generation", &generation)); - if (generation != mMonitorQueueGeneration) { - // Stale event + if (generation != mCheckBandwidthGeneration) { break; } - onMonitorQueue(); + onCheckBandwidth(); break; } - case kWhatSeek: - onSeek(msg); + case kWhatChangeConfiguration: + { + onChangeConfiguration(msg); + break; + } + + case kWhatChangeConfiguration2: + { + onChangeConfiguration2(msg); + break; + } + + case kWhatChangeConfiguration3: + { + onChangeConfiguration3(msg); break; + } + + case kWhatFinishDisconnect2: + { + onFinishDisconnect2(); + break; + } default: TRESPASS(); @@ -172,48 +388,128 @@ void LiveSession::onConnect(const sp<AMessage> &msg) { headers = NULL; } +#if 1 ALOGI("onConnect <URL suppressed>"); +#else + ALOGI("onConnect %s", url.c_str()); +#endif mMasterURL = url; bool dummy; - sp<M3UParser> playlist = fetchPlaylist(url.c_str(), &dummy); + mPlaylist = fetchPlaylist(url.c_str(), NULL /* curPlaylistHash */, &dummy); - if (playlist == NULL) { + if (mPlaylist == NULL) { ALOGE("unable to fetch master playlist '%s'.", url.c_str()); - signalEOS(ERROR_IO); + postPrepared(ERROR_IO); return; } - if (playlist->isVariantPlaylist()) { - for (size_t i = 0; i < playlist->size(); ++i) { + // We trust the content provider to make a reasonable choice of preferred + // initial bandwidth by listing it first in the variant playlist. + // At startup we really don't have a good estimate on the available + // network bandwidth since we haven't tranferred any data yet. Once + // we have we can make a better informed choice. + size_t initialBandwidth = 0; + size_t initialBandwidthIndex = 0; + + if (mPlaylist->isVariantPlaylist()) { + for (size_t i = 0; i < mPlaylist->size(); ++i) { BandwidthItem item; + item.mPlaylistIndex = i; + sp<AMessage> meta; - playlist->itemAt(i, &item.mURI, &meta); + AString uri; + mPlaylist->itemAt(i, &uri, &meta); unsigned long bandwidth; CHECK(meta->findInt32("bandwidth", (int32_t *)&item.mBandwidth)); + if (initialBandwidth == 0) { + initialBandwidth = item.mBandwidth; + } + mBandwidthItems.push(item); } CHECK_GT(mBandwidthItems.size(), 0u); mBandwidthItems.sort(SortByBandwidth); + + for (size_t i = 0; i < mBandwidthItems.size(); ++i) { + if (mBandwidthItems.itemAt(i).mBandwidth == initialBandwidth) { + initialBandwidthIndex = i; + break; + } + } + } else { + // dummy item. + BandwidthItem item; + item.mPlaylistIndex = 0; + item.mBandwidth = 0; + mBandwidthItems.push(item); + } + + changeConfiguration( + 0ll /* timeUs */, initialBandwidthIndex, true /* pickTrack */); +} + +void LiveSession::finishDisconnect() { + // No reconfiguration is currently pending, make sure none will trigger + // during disconnection either. + cancelCheckBandwidthEvent(); + + for (size_t i = 0; i < mFetcherInfos.size(); ++i) { + mFetcherInfos.valueAt(i).mFetcher->stopAsync(); } - postMonitorQueue(); + sp<AMessage> msg = new AMessage(kWhatFinishDisconnect2, id()); + + mContinuationCounter = mFetcherInfos.size(); + mContinuation = msg; + + if (mContinuationCounter == 0) { + msg->post(); + } +} + +void LiveSession::onFinishDisconnect2() { + mContinuation.clear(); + + mPacketSources.valueFor(STREAMTYPE_AUDIO)->signalEOS(ERROR_END_OF_STREAM); + mPacketSources.valueFor(STREAMTYPE_VIDEO)->signalEOS(ERROR_END_OF_STREAM); + + mPacketSources.valueFor( + STREAMTYPE_SUBTITLES)->signalEOS(ERROR_END_OF_STREAM); + + sp<AMessage> response = new AMessage; + response->setInt32("err", OK); + + response->postReply(mDisconnectReplyID); + mDisconnectReplyID = 0; } -void LiveSession::onDisconnect() { - ALOGI("onDisconnect"); +sp<PlaylistFetcher> LiveSession::addFetcher(const char *uri) { + ssize_t index = mFetcherInfos.indexOfKey(uri); + + if (index >= 0) { + return NULL; + } - signalEOS(ERROR_END_OF_STREAM); + sp<AMessage> notify = new AMessage(kWhatFetcherNotify, id()); + notify->setString("uri", uri); - Mutex::Autolock autoLock(mLock); - mDisconnectPending = false; + FetcherInfo info; + info.mFetcher = new PlaylistFetcher(notify, this, uri); + info.mDurationUs = -1ll; + info.mIsPrepared = false; + looper()->registerHandler(info.mFetcher); + + mFetcherInfos.add(uri, info); + + return info.mFetcher; } status_t LiveSession::fetchFile( @@ -229,14 +525,6 @@ status_t LiveSession::fetchFile( && strncasecmp(url, "https://", 8)) { return ERROR_UNSUPPORTED; } else { - { - Mutex::Autolock autoLock(mLock); - - if (mDisconnectPending) { - return ERROR_IO; - } - } - KeyedVector<String8, String8> headers = mExtraHeaders; if (range_offset > 0 || range_length >= 0) { headers.add( @@ -315,7 +603,8 @@ status_t LiveSession::fetchFile( return OK; } -sp<M3UParser> LiveSession::fetchPlaylist(const char *url, bool *unchanged) { +sp<M3UParser> LiveSession::fetchPlaylist( + const char *url, uint8_t *curPlaylistHash, bool *unchanged) { ALOGV("fetchPlaylist '%s'", url); *unchanged = false; @@ -339,13 +628,8 @@ sp<M3UParser> LiveSession::fetchPlaylist(const char *url, bool *unchanged) { MD5_Final(hash, &m); - if (mPlaylist != NULL && !memcmp(hash, mPlaylistHash, 16)) { + if (curPlaylistHash != NULL && !memcmp(hash, curPlaylistHash, 16)) { // playlist unchanged - - if (mRefreshState != THIRD_UNCHANGED_RELOAD_ATTEMPT) { - mRefreshState = (RefreshState)(mRefreshState + 1); - } - *unchanged = true; ALOGV("Playlist unchanged, refresh state is now %d", @@ -354,9 +638,9 @@ sp<M3UParser> LiveSession::fetchPlaylist(const char *url, bool *unchanged) { return NULL; } - memcpy(mPlaylistHash, hash, sizeof(hash)); - - mRefreshState = INITIAL_MINIMUM_RELOAD_DELAY; + if (curPlaylistHash != NULL) { + memcpy(curPlaylistHash, hash, sizeof(hash)); + } #endif sp<M3UParser> playlist = @@ -371,37 +655,6 @@ sp<M3UParser> LiveSession::fetchPlaylist(const char *url, bool *unchanged) { return playlist; } -int64_t LiveSession::getSegmentStartTimeUs(int32_t seqNumber) const { - CHECK(mPlaylist != NULL); - - int32_t firstSeqNumberInPlaylist; - if (mPlaylist->meta() == NULL || !mPlaylist->meta()->findInt32( - "media-sequence", &firstSeqNumberInPlaylist)) { - firstSeqNumberInPlaylist = 0; - } - - int32_t lastSeqNumberInPlaylist = - firstSeqNumberInPlaylist + (int32_t)mPlaylist->size() - 1; - - CHECK_GE(seqNumber, firstSeqNumberInPlaylist); - CHECK_LE(seqNumber, lastSeqNumberInPlaylist); - - int64_t segmentStartUs = 0ll; - for (int32_t index = 0; - index < seqNumber - firstSeqNumberInPlaylist; ++index) { - sp<AMessage> itemMeta; - CHECK(mPlaylist->itemAt( - index, NULL /* uri */, &itemMeta)); - - int64_t itemDurationUs; - CHECK(itemMeta->findInt64("durationUs", &itemDurationUs)); - - segmentStartUs += itemDurationUs; - } - - return segmentStartUs; -} - static double uniformRand() { return (double)rand() / RAND_MAX; } @@ -412,36 +665,50 @@ size_t LiveSession::getBandwidthIndex() { } #if 1 - int32_t bandwidthBps; - if (mHTTPDataSource != NULL - && mHTTPDataSource->estimateBandwidth(&bandwidthBps)) { - ALOGV("bandwidth estimated at %.2f kbps", bandwidthBps / 1024.0f); - } else { - ALOGV("no bandwidth estimate."); - return 0; // Pick the lowest bandwidth stream by default. - } - char value[PROPERTY_VALUE_MAX]; - if (property_get("media.httplive.max-bw", value, NULL)) { + ssize_t index = -1; + if (property_get("media.httplive.bw-index", value, NULL)) { char *end; - long maxBw = strtoul(value, &end, 10); - if (end > value && *end == '\0') { - if (maxBw > 0 && bandwidthBps > maxBw) { - ALOGV("bandwidth capped to %ld bps", maxBw); - bandwidthBps = maxBw; - } + index = strtol(value, &end, 10); + CHECK(end > value && *end == '\0'); + + if (index >= 0 && (size_t)index >= mBandwidthItems.size()) { + index = mBandwidthItems.size() - 1; } } - // Consider only 80% of the available bandwidth usable. - bandwidthBps = (bandwidthBps * 8) / 10; + if (index < 0) { + int32_t bandwidthBps; + if (mHTTPDataSource != NULL + && mHTTPDataSource->estimateBandwidth(&bandwidthBps)) { + ALOGV("bandwidth estimated at %.2f kbps", bandwidthBps / 1024.0f); + } else { + ALOGV("no bandwidth estimate."); + return 0; // Pick the lowest bandwidth stream by default. + } - // Pick the highest bandwidth stream below or equal to estimated bandwidth. + char value[PROPERTY_VALUE_MAX]; + if (property_get("media.httplive.max-bw", value, NULL)) { + char *end; + long maxBw = strtoul(value, &end, 10); + if (end > value && *end == '\0') { + if (maxBw > 0 && bandwidthBps > maxBw) { + ALOGV("bandwidth capped to %ld bps", maxBw); + bandwidthBps = maxBw; + } + } + } - size_t index = mBandwidthItems.size() - 1; - while (index > 0 && mBandwidthItems.itemAt(index).mBandwidth - > (size_t)bandwidthBps) { - --index; + // Consider only 80% of the available bandwidth usable. + bandwidthBps = (bandwidthBps * 8) / 10; + + // Pick the highest bandwidth stream below or equal to estimated bandwidth. + + index = mBandwidthItems.size() - 1; + while (index > 0 && mBandwidthItems.itemAt(index).mBandwidth + > (size_t)bandwidthBps) { + --index; + } } #elif 0 // Change bandwidth at random() @@ -452,6 +719,8 @@ size_t LiveSession::getBandwidthIndex() { // to lowest) const size_t kMinIndex = 0; + static ssize_t mPrevBandwidthIndex = -1; + size_t index; if (mPrevBandwidthIndex < 0) { index = kMinIndex; @@ -463,6 +732,7 @@ size_t LiveSession::getBandwidthIndex() { index = kMinIndex; } } + mPrevBandwidthIndex = index; #elif 0 // Pick the highest bandwidth stream below or equal to 1.2 Mbit/sec @@ -470,570 +740,405 @@ size_t LiveSession::getBandwidthIndex() { while (index > 0 && mBandwidthItems.itemAt(index).mBandwidth > 1200000) { --index; } +#elif 1 + char value[PROPERTY_VALUE_MAX]; + size_t index; + if (property_get("media.httplive.bw-index", value, NULL)) { + char *end; + index = strtoul(value, &end, 10); + CHECK(end > value && *end == '\0'); + + if (index >= mBandwidthItems.size()) { + index = mBandwidthItems.size() - 1; + } + } else { + index = 0; + } #else size_t index = mBandwidthItems.size() - 1; // Highest bandwidth stream #endif + CHECK_GE(index, 0); + return index; } -bool LiveSession::timeToRefreshPlaylist(int64_t nowUs) const { - if (mPlaylist == NULL) { - CHECK_EQ((int)mRefreshState, (int)INITIAL_MINIMUM_RELOAD_DELAY); - return true; - } - - int32_t targetDurationSecs; - CHECK(mPlaylist->meta()->findInt32("target-duration", &targetDurationSecs)); - - int64_t targetDurationUs = targetDurationSecs * 1000000ll; - - int64_t minPlaylistAgeUs; - - switch (mRefreshState) { - case INITIAL_MINIMUM_RELOAD_DELAY: - { - size_t n = mPlaylist->size(); - if (n > 0) { - sp<AMessage> itemMeta; - CHECK(mPlaylist->itemAt(n - 1, NULL /* uri */, &itemMeta)); - - int64_t itemDurationUs; - CHECK(itemMeta->findInt64("durationUs", &itemDurationUs)); - - minPlaylistAgeUs = itemDurationUs; - break; - } - - // fall through - } - - case FIRST_UNCHANGED_RELOAD_ATTEMPT: - { - minPlaylistAgeUs = targetDurationUs / 2; - break; - } - - case SECOND_UNCHANGED_RELOAD_ATTEMPT: - { - minPlaylistAgeUs = (targetDurationUs * 3) / 2; - break; - } - - case THIRD_UNCHANGED_RELOAD_ATTEMPT: - { - minPlaylistAgeUs = targetDurationUs * 3; - break; - } +status_t LiveSession::onSeek(const sp<AMessage> &msg) { + int64_t timeUs; + CHECK(msg->findInt64("timeUs", &timeUs)); - default: - TRESPASS(); - break; + if (!mReconfigurationInProgress) { + changeConfiguration(timeUs, getBandwidthIndex()); } - return mLastPlaylistFetchTimeUs + minPlaylistAgeUs <= nowUs; + return OK; } -void LiveSession::onDownloadNext() { - size_t bandwidthIndex = getBandwidthIndex(); - -rinse_repeat: - int64_t nowUs = ALooper::GetNowUs(); - - if (mLastPlaylistFetchTimeUs < 0 - || (ssize_t)bandwidthIndex != mPrevBandwidthIndex - || (!mPlaylist->isComplete() && timeToRefreshPlaylist(nowUs))) { - AString url; - if (mBandwidthItems.size() > 0) { - url = mBandwidthItems.editItemAt(bandwidthIndex).mURI; - } else { - url = mMasterURL; - } - - if ((ssize_t)bandwidthIndex != mPrevBandwidthIndex) { - // If we switch bandwidths, do not pay any heed to whether - // playlists changed since the last time... - mPlaylist.clear(); - } - - bool unchanged; - sp<M3UParser> playlist = fetchPlaylist(url.c_str(), &unchanged); - if (playlist == NULL) { - if (unchanged) { - // We succeeded in fetching the playlist, but it was - // unchanged from the last time we tried. - } else { - ALOGE("failed to load playlist at url '%s'", url.c_str()); - signalEOS(ERROR_IO); - - return; - } - } else { - mPlaylist = playlist; - } - - if (!mDurationFixed) { - Mutex::Autolock autoLock(mLock); - - if (!mPlaylist->isComplete() && !mPlaylist->isEvent()) { - mDurationUs = -1; - mDurationFixed = true; - } else { - mDurationUs = 0; - for (size_t i = 0; i < mPlaylist->size(); ++i) { - sp<AMessage> itemMeta; - CHECK(mPlaylist->itemAt( - i, NULL /* uri */, &itemMeta)); - - int64_t itemDurationUs; - CHECK(itemMeta->findInt64("durationUs", &itemDurationUs)); - - mDurationUs += itemDurationUs; - } +status_t LiveSession::getDuration(int64_t *durationUs) const { + int64_t maxDurationUs = 0ll; + for (size_t i = 0; i < mFetcherInfos.size(); ++i) { + int64_t fetcherDurationUs = mFetcherInfos.valueAt(i).mDurationUs; - mDurationFixed = mPlaylist->isComplete(); - } + if (fetcherDurationUs >= 0ll && fetcherDurationUs > maxDurationUs) { + maxDurationUs = fetcherDurationUs; } - - mLastPlaylistFetchTimeUs = ALooper::GetNowUs(); } - int32_t firstSeqNumberInPlaylist; - if (mPlaylist->meta() == NULL || !mPlaylist->meta()->findInt32( - "media-sequence", &firstSeqNumberInPlaylist)) { - firstSeqNumberInPlaylist = 0; - } + *durationUs = maxDurationUs; - bool seekDiscontinuity = false; - bool explicitDiscontinuity = false; - bool bandwidthChanged = false; + return OK; +} - if (mSeekTimeUs >= 0) { - if (mPlaylist->isComplete() || mPlaylist->isEvent()) { - size_t index = 0; - int64_t segmentStartUs = 0; - while (index < mPlaylist->size()) { - sp<AMessage> itemMeta; - CHECK(mPlaylist->itemAt( - index, NULL /* uri */, &itemMeta)); +bool LiveSession::isSeekable() const { + int64_t durationUs; + return getDuration(&durationUs) == OK && durationUs >= 0; +} - int64_t itemDurationUs; - CHECK(itemMeta->findInt64("durationUs", &itemDurationUs)); +bool LiveSession::hasDynamicDuration() const { + return false; +} - if (mSeekTimeUs < segmentStartUs + itemDurationUs) { - break; - } +status_t LiveSession::getTrackInfo(Parcel *reply) const { + return mPlaylist->getTrackInfo(reply); +} - segmentStartUs += itemDurationUs; - ++index; - } +status_t LiveSession::selectTrack(size_t index, bool select) { + status_t err = mPlaylist->selectTrack(index, select); + if (err == OK) { + (new AMessage(kWhatChangeConfiguration, id()))->post(); + } + return err; +} - if (index < mPlaylist->size()) { - int32_t newSeqNumber = firstSeqNumberInPlaylist + index; +void LiveSession::changeConfiguration( + int64_t timeUs, size_t bandwidthIndex, bool pickTrack) { + CHECK(!mReconfigurationInProgress); + mReconfigurationInProgress = true; - ALOGI("seeking to seq no %d", newSeqNumber); + mPrevBandwidthIndex = bandwidthIndex; - mSeqNumber = newSeqNumber; + ALOGV("changeConfiguration => timeUs:%lld us, bwIndex:%d, pickTrack:%d", + timeUs, bandwidthIndex, pickTrack); - mDataSource->reset(); + if (pickTrack) { + mPlaylist->pickRandomMediaItems(); + } - // reseting the data source will have had the - // side effect of discarding any previously queued - // bandwidth change discontinuity. - // Therefore we'll need to treat these seek - // discontinuities as involving a bandwidth change - // even if they aren't directly. - seekDiscontinuity = true; - bandwidthChanged = true; - } - } + CHECK_LT(bandwidthIndex, mBandwidthItems.size()); + const BandwidthItem &item = mBandwidthItems.itemAt(bandwidthIndex); - mSeekTimeUs = -1; + uint32_t streamMask = 0; - Mutex::Autolock autoLock(mLock); - mSeekDone = true; - mCondition.broadcast(); + AString audioURI; + if (mPlaylist->getAudioURI(item.mPlaylistIndex, &audioURI)) { + streamMask |= STREAMTYPE_AUDIO; } - const int32_t lastSeqNumberInPlaylist = - firstSeqNumberInPlaylist + (int32_t)mPlaylist->size() - 1; - - if (mSeqNumber < 0) { - if (mPlaylist->isComplete()) { - mSeqNumber = firstSeqNumberInPlaylist; - } else { - // If this is a live session, start 3 segments from the end. - mSeqNumber = lastSeqNumberInPlaylist - 3; - if (mSeqNumber < firstSeqNumberInPlaylist) { - mSeqNumber = firstSeqNumberInPlaylist; - } - } + AString videoURI; + if (mPlaylist->getVideoURI(item.mPlaylistIndex, &videoURI)) { + streamMask |= STREAMTYPE_VIDEO; } - if (mSeqNumber < firstSeqNumberInPlaylist - || mSeqNumber > lastSeqNumberInPlaylist) { - if (mPrevBandwidthIndex != (ssize_t)bandwidthIndex) { - // Go back to the previous bandwidth. - - ALOGI("new bandwidth does not have the sequence number " - "we're looking for, switching back to previous bandwidth"); + AString subtitleURI; + if (mPlaylist->getSubtitleURI(item.mPlaylistIndex, &subtitleURI)) { + streamMask |= STREAMTYPE_SUBTITLES; + } - mLastPlaylistFetchTimeUs = -1; - bandwidthIndex = mPrevBandwidthIndex; - goto rinse_repeat; - } + // Step 1, stop and discard fetchers that are no longer needed. + // Pause those that we'll reuse. + for (size_t i = 0; i < mFetcherInfos.size(); ++i) { + const AString &uri = mFetcherInfos.keyAt(i); - if (!mPlaylist->isComplete() && mNumRetries < kMaxNumRetries) { - ++mNumRetries; + bool discardFetcher = true; - if (mSeqNumber > lastSeqNumberInPlaylist) { - mLastPlaylistFetchTimeUs = -1; - postMonitorQueue(3000000ll); - return; + // If we're seeking all current fetchers are discarded. + if (timeUs < 0ll) { + if (((streamMask & STREAMTYPE_AUDIO) && uri == audioURI) + || ((streamMask & STREAMTYPE_VIDEO) && uri == videoURI) + || ((streamMask & STREAMTYPE_SUBTITLES) && uri == subtitleURI)) { + discardFetcher = false; } + } - // we've missed the boat, let's start from the lowest sequence - // number available and signal a discontinuity. - - ALOGI("We've missed the boat, restarting playback."); - mSeqNumber = lastSeqNumberInPlaylist; - explicitDiscontinuity = true; - - // fall through + if (discardFetcher) { + mFetcherInfos.valueAt(i).mFetcher->stopAsync(); } else { - ALOGE("Cannot find sequence number %d in playlist " - "(contains %d - %d)", - mSeqNumber, firstSeqNumberInPlaylist, - firstSeqNumberInPlaylist + mPlaylist->size() - 1); - - signalEOS(ERROR_END_OF_STREAM); - return; + mFetcherInfos.valueAt(i).mFetcher->pauseAsync(); } } - mNumRetries = 0; - - AString uri; - sp<AMessage> itemMeta; - CHECK(mPlaylist->itemAt( - mSeqNumber - firstSeqNumberInPlaylist, - &uri, - &itemMeta)); - - int32_t val; - if (itemMeta->findInt32("discontinuity", &val) && val != 0) { - explicitDiscontinuity = true; + sp<AMessage> msg = new AMessage(kWhatChangeConfiguration2, id()); + msg->setInt32("streamMask", streamMask); + msg->setInt64("timeUs", timeUs); + if (streamMask & STREAMTYPE_AUDIO) { + msg->setString("audioURI", audioURI.c_str()); } - - int64_t range_offset, range_length; - if (!itemMeta->findInt64("range-offset", &range_offset) - || !itemMeta->findInt64("range-length", &range_length)) { - range_offset = 0; - range_length = -1; + if (streamMask & STREAMTYPE_VIDEO) { + msg->setString("videoURI", videoURI.c_str()); } - - ALOGV("fetching segment %d from (%d .. %d)", - mSeqNumber, firstSeqNumberInPlaylist, lastSeqNumberInPlaylist); - - sp<ABuffer> buffer; - status_t err = fetchFile(uri.c_str(), &buffer, range_offset, range_length); - if (err != OK) { - ALOGE("failed to fetch .ts segment at url '%s'", uri.c_str()); - signalEOS(err); - return; + if (streamMask & STREAMTYPE_SUBTITLES) { + msg->setString("subtitleURI", subtitleURI.c_str()); } - CHECK(buffer != NULL); - - err = decryptBuffer(mSeqNumber - firstSeqNumberInPlaylist, buffer); + // Every time a fetcher acknowledges the stopAsync or pauseAsync request + // we'll decrement mContinuationCounter, once it reaches zero, i.e. all + // fetchers have completed their asynchronous operation, we'll post + // mContinuation, which then is handled below in onChangeConfiguration2. + mContinuationCounter = mFetcherInfos.size(); + mContinuation = msg; - if (err != OK) { - ALOGE("decryptBuffer failed w/ error %d", err); - - signalEOS(err); - return; + if (mContinuationCounter == 0) { + msg->post(); } +} - if (buffer->size() == 0 || buffer->data()[0] != 0x47) { - // Not a transport stream??? - - ALOGE("This doesn't look like a transport stream..."); - - mBandwidthItems.removeAt(bandwidthIndex); +void LiveSession::onChangeConfiguration(const sp<AMessage> &msg) { + if (!mReconfigurationInProgress) { + changeConfiguration(-1ll /* timeUs */, getBandwidthIndex()); + } else { + msg->post(1000000ll); // retry in 1 sec + } +} - if (mBandwidthItems.isEmpty()) { - signalEOS(ERROR_UNSUPPORTED); - return; - } +void LiveSession::onChangeConfiguration2(const sp<AMessage> &msg) { + mContinuation.clear(); - ALOGI("Retrying with a different bandwidth stream."); + // All fetchers are either suspended or have been removed now. - mLastPlaylistFetchTimeUs = -1; - bandwidthIndex = getBandwidthIndex(); - mPrevBandwidthIndex = bandwidthIndex; - mSeqNumber = -1; + uint32_t streamMask; + CHECK(msg->findInt32("streamMask", (int32_t *)&streamMask)); - goto rinse_repeat; + AString audioURI, videoURI, subtitleURI; + if (streamMask & STREAMTYPE_AUDIO) { + CHECK(msg->findString("audioURI", &audioURI)); + ALOGV("audioURI = '%s'", audioURI.c_str()); } - - if ((size_t)mPrevBandwidthIndex != bandwidthIndex) { - bandwidthChanged = true; + if (streamMask & STREAMTYPE_VIDEO) { + CHECK(msg->findString("videoURI", &videoURI)); + ALOGV("videoURI = '%s'", videoURI.c_str()); } - - if (mPrevBandwidthIndex < 0) { - // Don't signal a bandwidth change at the very beginning of - // playback. - bandwidthChanged = false; + if (streamMask & STREAMTYPE_SUBTITLES) { + CHECK(msg->findString("subtitleURI", &subtitleURI)); + ALOGV("subtitleURI = '%s'", subtitleURI.c_str()); } - if (mStartOfPlayback) { - seekDiscontinuity = true; - mStartOfPlayback = false; + // Determine which decoders to shutdown on the player side, + // a decoder has to be shutdown if either + // 1) its streamtype was active before but now longer isn't. + // or + // 2) its streamtype was already active and still is but the URI + // has changed. + uint32_t changedMask = 0; + if (((mStreamMask & streamMask & STREAMTYPE_AUDIO) + && !(audioURI == mAudioURI)) + || (mStreamMask & ~streamMask & STREAMTYPE_AUDIO)) { + changedMask |= STREAMTYPE_AUDIO; + } + if (((mStreamMask & streamMask & STREAMTYPE_VIDEO) + && !(videoURI == mVideoURI)) + || (mStreamMask & ~streamMask & STREAMTYPE_VIDEO)) { + changedMask |= STREAMTYPE_VIDEO; } - if (seekDiscontinuity || explicitDiscontinuity || bandwidthChanged) { - // Signal discontinuity. - - ALOGI("queueing discontinuity (seek=%d, explicit=%d, bandwidthChanged=%d)", - seekDiscontinuity, explicitDiscontinuity, bandwidthChanged); + if (changedMask == 0) { + // If nothing changed as far as the audio/video decoders + // are concerned we can proceed. + onChangeConfiguration3(msg); + return; + } - sp<ABuffer> tmp = new ABuffer(188); - memset(tmp->data(), 0, tmp->size()); + // Something changed, inform the player which will shutdown the + // corresponding decoders and will post the reply once that's done. + // Handling the reply will continue executing below in + // onChangeConfiguration3. + sp<AMessage> notify = mNotify->dup(); + notify->setInt32("what", kWhatStreamsChanged); + notify->setInt32("changedMask", changedMask); - // signal a 'hard' discontinuity for explicit or bandwidthChanged. - uint8_t type = (explicitDiscontinuity || bandwidthChanged) ? 1 : 0; + msg->setWhat(kWhatChangeConfiguration3); + msg->setTarget(id()); - if (mPlaylist->isComplete() || mPlaylist->isEvent()) { - // If this was a live event this made no sense since - // we don't have access to all the segment before the current - // one. - int64_t segmentStartTimeUs = getSegmentStartTimeUs(mSeqNumber); - memcpy(tmp->data() + 2, &segmentStartTimeUs, sizeof(segmentStartTimeUs)); + notify->setMessage("reply", msg); + notify->post(); +} - type |= 2; - } +void LiveSession::onChangeConfiguration3(const sp<AMessage> &msg) { + // All remaining fetchers are still suspended, the player has shutdown + // any decoders that needed it. - tmp->data()[1] = type; + uint32_t streamMask; + CHECK(msg->findInt32("streamMask", (int32_t *)&streamMask)); - mDataSource->queueBuffer(tmp); + AString audioURI, videoURI, subtitleURI; + if (streamMask & STREAMTYPE_AUDIO) { + CHECK(msg->findString("audioURI", &audioURI)); + } + if (streamMask & STREAMTYPE_VIDEO) { + CHECK(msg->findString("videoURI", &videoURI)); + } + if (streamMask & STREAMTYPE_SUBTITLES) { + CHECK(msg->findString("subtitleURI", &subtitleURI)); } - mDataSource->queueBuffer(buffer); + int64_t timeUs; + CHECK(msg->findInt64("timeUs", &timeUs)); - mPrevBandwidthIndex = bandwidthIndex; - ++mSeqNumber; + if (timeUs < 0ll) { + timeUs = mLastDequeuedTimeUs; + } + mRealTimeBaseUs = ALooper::GetNowUs() - timeUs; - postMonitorQueue(); -} + mStreamMask = streamMask; + mAudioURI = audioURI; + mVideoURI = videoURI; + mSubtitleURI = subtitleURI; -void LiveSession::signalEOS(status_t err) { - if (mInPreparationPhase && mNotify != NULL) { - sp<AMessage> notify = mNotify->dup(); + // Resume all existing fetchers and assign them packet sources. + for (size_t i = 0; i < mFetcherInfos.size(); ++i) { + const AString &uri = mFetcherInfos.keyAt(i); - notify->setInt32( - "what", - err == ERROR_END_OF_STREAM - ? kWhatPrepared : kWhatPreparationFailed); + uint32_t resumeMask = 0; - if (err != ERROR_END_OF_STREAM) { - notify->setInt32("err", err); + sp<AnotherPacketSource> audioSource; + if ((streamMask & STREAMTYPE_AUDIO) && uri == audioURI) { + audioSource = mPacketSources.valueFor(STREAMTYPE_AUDIO); + resumeMask |= STREAMTYPE_AUDIO; } - notify->post(); - - mInPreparationPhase = false; - } - - mDataSource->queueEOS(err); -} - -void LiveSession::onMonitorQueue() { - if (mSeekTimeUs >= 0 - || mDataSource->countQueuedBuffers() < kMaxNumQueuedFragments) { - onDownloadNext(); - } else { - if (mInPreparationPhase) { - if (mNotify != NULL) { - sp<AMessage> notify = mNotify->dup(); - notify->setInt32("what", kWhatPrepared); - notify->post(); - } - - mInPreparationPhase = false; + sp<AnotherPacketSource> videoSource; + if ((streamMask & STREAMTYPE_VIDEO) && uri == videoURI) { + videoSource = mPacketSources.valueFor(STREAMTYPE_VIDEO); + resumeMask |= STREAMTYPE_VIDEO; } - postMonitorQueue(1000000ll); - } -} + sp<AnotherPacketSource> subtitleSource; + if ((streamMask & STREAMTYPE_SUBTITLES) && uri == subtitleURI) { + subtitleSource = mPacketSources.valueFor(STREAMTYPE_SUBTITLES); + resumeMask |= STREAMTYPE_SUBTITLES; + } -status_t LiveSession::decryptBuffer( - size_t playlistIndex, const sp<ABuffer> &buffer) { - sp<AMessage> itemMeta; - bool found = false; - AString method; + CHECK_NE(resumeMask, 0u); - for (ssize_t i = playlistIndex; i >= 0; --i) { - AString uri; - CHECK(mPlaylist->itemAt(i, &uri, &itemMeta)); + ALOGV("resuming fetchers for mask 0x%08x", resumeMask); - if (itemMeta->findString("cipher-method", &method)) { - found = true; - break; - } - } + streamMask &= ~resumeMask; - if (!found) { - method = "NONE"; + mFetcherInfos.valueAt(i).mFetcher->startAsync( + audioSource, videoSource, subtitleSource); } - if (method == "NONE") { - return OK; - } else if (!(method == "AES-128")) { - ALOGE("Unsupported cipher method '%s'", method.c_str()); - return ERROR_UNSUPPORTED; - } + // streamMask now only contains the types that need a new fetcher created. - AString keyURI; - if (!itemMeta->findString("cipher-uri", &keyURI)) { - ALOGE("Missing key uri"); - return ERROR_MALFORMED; + if (streamMask != 0) { + ALOGV("creating new fetchers for mask 0x%08x", streamMask); } - ssize_t index = mAESKeyForURI.indexOfKey(keyURI); - - sp<ABuffer> key; - if (index >= 0) { - key = mAESKeyForURI.valueAt(index); - } else { - key = new ABuffer(16); - - sp<HTTPBase> keySource = - HTTPBase::Create( - (mFlags & kFlagIncognito) - ? HTTPBase::kFlagIncognito - : 0); + while (streamMask != 0) { + StreamType streamType = (StreamType)(streamMask & ~(streamMask - 1)); - if (mUIDValid) { - keySource->setUID(mUID); + AString uri; + switch (streamType) { + case STREAMTYPE_AUDIO: + uri = audioURI; + break; + case STREAMTYPE_VIDEO: + uri = videoURI; + break; + case STREAMTYPE_SUBTITLES: + uri = subtitleURI; + break; + default: + TRESPASS(); } - status_t err = - keySource->connect( - keyURI.c_str(), - mExtraHeaders.isEmpty() ? NULL : &mExtraHeaders); - - if (err == OK) { - size_t offset = 0; - while (offset < 16) { - ssize_t n = keySource->readAt( - offset, key->data() + offset, 16 - offset); - if (n <= 0) { - err = ERROR_IO; - break; - } + sp<PlaylistFetcher> fetcher = addFetcher(uri.c_str()); + CHECK(fetcher != NULL); - offset += n; - } - } + sp<AnotherPacketSource> audioSource; + if ((streamMask & STREAMTYPE_AUDIO) && uri == audioURI) { + audioSource = mPacketSources.valueFor(STREAMTYPE_AUDIO); + audioSource->clear(); - if (err != OK) { - ALOGE("failed to fetch cipher key from '%s'.", keyURI.c_str()); - return ERROR_IO; + streamMask &= ~STREAMTYPE_AUDIO; } - mAESKeyForURI.add(keyURI, key); - } + sp<AnotherPacketSource> videoSource; + if ((streamMask & STREAMTYPE_VIDEO) && uri == videoURI) { + videoSource = mPacketSources.valueFor(STREAMTYPE_VIDEO); + videoSource->clear(); - AES_KEY aes_key; - if (AES_set_decrypt_key(key->data(), 128, &aes_key) != 0) { - ALOGE("failed to set AES decryption key."); - return UNKNOWN_ERROR; - } - - unsigned char aes_ivec[16]; - - AString iv; - if (itemMeta->findString("cipher-iv", &iv)) { - if ((!iv.startsWith("0x") && !iv.startsWith("0X")) - || iv.size() != 16 * 2 + 2) { - ALOGE("malformed cipher IV '%s'.", iv.c_str()); - return ERROR_MALFORMED; + streamMask &= ~STREAMTYPE_VIDEO; } - memset(aes_ivec, 0, sizeof(aes_ivec)); - for (size_t i = 0; i < 16; ++i) { - char c1 = tolower(iv.c_str()[2 + 2 * i]); - char c2 = tolower(iv.c_str()[3 + 2 * i]); - if (!isxdigit(c1) || !isxdigit(c2)) { - ALOGE("malformed cipher IV '%s'.", iv.c_str()); - return ERROR_MALFORMED; - } - uint8_t nibble1 = isdigit(c1) ? c1 - '0' : c1 - 'a' + 10; - uint8_t nibble2 = isdigit(c2) ? c2 - '0' : c2 - 'a' + 10; + sp<AnotherPacketSource> subtitleSource; + if ((streamMask & STREAMTYPE_SUBTITLES) && uri == subtitleURI) { + subtitleSource = mPacketSources.valueFor(STREAMTYPE_SUBTITLES); + subtitleSource->clear(); - aes_ivec[i] = nibble1 << 4 | nibble2; + streamMask &= ~STREAMTYPE_SUBTITLES; } - } else { - memset(aes_ivec, 0, sizeof(aes_ivec)); - aes_ivec[15] = mSeqNumber & 0xff; - aes_ivec[14] = (mSeqNumber >> 8) & 0xff; - aes_ivec[13] = (mSeqNumber >> 16) & 0xff; - aes_ivec[12] = (mSeqNumber >> 24) & 0xff; + + fetcher->startAsync(audioSource, videoSource, subtitleSource, timeUs); } - AES_cbc_encrypt( - buffer->data(), buffer->data(), buffer->size(), - &aes_key, aes_ivec, AES_DECRYPT); + // All fetchers have now been started, the configuration change + // has completed. - // hexdump(buffer->data(), buffer->size()); + scheduleCheckBandwidthEvent(); - size_t n = buffer->size(); - CHECK_GT(n, 0u); + ALOGV("XXX configuration change completed."); - size_t pad = buffer->data()[n - 1]; + mReconfigurationInProgress = false; - CHECK_GT(pad, 0u); - CHECK_LE(pad, 16u); - CHECK_GE((size_t)n, pad); - for (size_t i = 0; i < pad; ++i) { - CHECK_EQ((unsigned)buffer->data()[n - 1 - i], pad); + if (mDisconnectReplyID != 0) { + finishDisconnect(); } +} - n -= pad; - - buffer->setRange(buffer->offset(), n); - - return OK; +void LiveSession::scheduleCheckBandwidthEvent() { + sp<AMessage> msg = new AMessage(kWhatCheckBandwidth, id()); + msg->setInt32("generation", mCheckBandwidthGeneration); + msg->post(10000000ll); } -void LiveSession::postMonitorQueue(int64_t delayUs) { - sp<AMessage> msg = new AMessage(kWhatMonitorQueue, id()); - msg->setInt32("generation", ++mMonitorQueueGeneration); - msg->post(delayUs); +void LiveSession::cancelCheckBandwidthEvent() { + ++mCheckBandwidthGeneration; } -void LiveSession::onSeek(const sp<AMessage> &msg) { - int64_t timeUs; - CHECK(msg->findInt64("timeUs", &timeUs)); +void LiveSession::onCheckBandwidth() { + if (mReconfigurationInProgress) { + scheduleCheckBandwidthEvent(); + return; + } + + size_t bandwidthIndex = getBandwidthIndex(); + if (mPrevBandwidthIndex < 0 + || bandwidthIndex != (size_t)mPrevBandwidthIndex) { + changeConfiguration(-1ll /* timeUs */, bandwidthIndex); + } - mSeekTimeUs = timeUs; - postMonitorQueue(); + // Handling the kWhatCheckBandwidth even here does _not_ automatically + // schedule another one on return, only an explicit call to + // scheduleCheckBandwidthEvent will do that. + // This ensures that only one configuration change is ongoing at any + // one time, once that completes it'll schedule another check bandwidth + // event. } -status_t LiveSession::getDuration(int64_t *durationUs) const { - Mutex::Autolock autoLock(mLock); - *durationUs = mDurationUs; +void LiveSession::postPrepared(status_t err) { + CHECK(mInPreparationPhase); - return OK; -} + sp<AMessage> notify = mNotify->dup(); + if (err == OK || err == ERROR_END_OF_STREAM) { + notify->setInt32("what", kWhatPrepared); + } else { + notify->setInt32("what", kWhatPreparationFailed); + notify->setInt32("err", err); + } -bool LiveSession::isSeekable() const { - int64_t durationUs; - return getDuration(&durationUs) == OK && durationUs >= 0; -} + notify->post(); -bool LiveSession::hasDynamicDuration() const { - return !mDurationFixed; + mInPreparationPhase = false; } } // namespace android |