summaryrefslogtreecommitdiffstats
path: root/media/libstagefright/httplive
diff options
context:
space:
mode:
authorAndreas Huber <andih@google.com>2013-01-15 09:04:18 -0800
committerAndreas Huber <andih@google.com>2013-05-31 10:30:45 -0700
commit14f7672b5d450ed26a06fd3bb3ce045ea78b11b2 (patch)
tree1730ace584297a64946ed5dccc0ca596c2570d21 /media/libstagefright/httplive
parentfbb70ce416b193655fbe5ff7f6c8676050bdf524 (diff)
downloadframeworks_av-14f7672b5d450ed26a06fd3bb3ce045ea78b11b2.zip
frameworks_av-14f7672b5d450ed26a06fd3bb3ce045ea78b11b2.tar.gz
frameworks_av-14f7672b5d450ed26a06fd3bb3ce045ea78b11b2.tar.bz2
New HLS implementation supporting independent stream sources, audio-only streams
and more. Change-Id: Icfc45a0100243b2f7a14a9e65696be45b67d6495
Diffstat (limited to 'media/libstagefright/httplive')
-rw-r--r--media/libstagefright/httplive/Android.mk11
-rw-r--r--media/libstagefright/httplive/LiveSession.cpp1216
-rw-r--r--media/libstagefright/httplive/LiveSession.h172
-rw-r--r--media/libstagefright/httplive/M3UParser.cpp493
-rw-r--r--media/libstagefright/httplive/M3UParser.h104
-rw-r--r--media/libstagefright/httplive/PlaylistFetcher.cpp969
-rw-r--r--media/libstagefright/httplive/PlaylistFetcher.h155
7 files changed, 2520 insertions, 600 deletions
diff --git a/media/libstagefright/httplive/Android.mk b/media/libstagefright/httplive/Android.mk
index a3fa7a3..85bd492 100644
--- a/media/libstagefright/httplive/Android.mk
+++ b/media/libstagefright/httplive/Android.mk
@@ -6,16 +6,25 @@ LOCAL_SRC_FILES:= \
LiveDataSource.cpp \
LiveSession.cpp \
M3UParser.cpp \
+ PlaylistFetcher.cpp \
LOCAL_C_INCLUDES:= \
$(TOP)/frameworks/av/media/libstagefright \
$(TOP)/frameworks/native/include/media/openmax \
$(TOP)/external/openssl/include
+LOCAL_SHARED_LIBRARIES := \
+ libcrypto \
+ libcutils \
+ libmedia \
+ libstagefright \
+ libstagefright_foundation \
+ libutils \
+
LOCAL_MODULE:= libstagefright_httplive
ifeq ($(TARGET_ARCH),arm)
LOCAL_CFLAGS += -Wno-psabi
endif
-include $(BUILD_STATIC_LIBRARY)
+include $(BUILD_SHARED_LIBRARY)
diff --git a/media/libstagefright/httplive/LiveSession.cpp b/media/libstagefright/httplive/LiveSession.cpp
index 505bdb3..fff13eb 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,107 @@ 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),
+ 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) {
+ int64_t timeUs;
+ CHECK((*accessUnit)->meta()->findInt64("timeUs", &timeUs));
+ ALOGV("[%s] read buffer at time %lld us", streamStr, timeUs);
+
+ mLastDequeuedTimeUs = timeUs;
+ } 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 +164,184 @@ 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 kWhatMonitorQueue:
+ 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);
+
+ 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 kWhatChangeConfiguration2:
+ {
+ onChangeConfiguration2(msg);
+ break;
+ }
+
+ case kWhatChangeConfiguration3:
+ {
+ onChangeConfiguration3(msg);
+ break;
+ }
+
+ case kWhatFinishDisconnect2:
+ {
+ onFinishDisconnect2();
break;
+ }
default:
TRESPASS();
@@ -172,48 +374,127 @@ 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);
}
- postMonitorQueue();
+ changeConfiguration(0ll /* timeUs */, initialBandwidthIndex);
}
-void LiveSession::onDisconnect() {
- ALOGI("onDisconnect");
+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();
+ }
+
+ sp<AMessage> msg = new AMessage(kWhatFinishDisconnect2, id());
- signalEOS(ERROR_END_OF_STREAM);
+ mContinuationCounter = mFetcherInfos.size();
+ mContinuation = msg;
- Mutex::Autolock autoLock(mLock);
- mDisconnectPending = false;
+ 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;
+}
+
+sp<PlaylistFetcher> LiveSession::addFetcher(const char *uri) {
+ ssize_t index = mFetcherInfos.indexOfKey(uri);
+
+ if (index >= 0) {
+ return NULL;
+ }
+
+ sp<AMessage> notify = new AMessage(kWhatFetcherNotify, id());
+ notify->setString("uri", uri);
+
+ 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 +510,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 +588,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 +613,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 +623,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 +640,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 +650,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;
+ 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 +704,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 +717,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 +725,381 @@ 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;
-
- 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));
-
- int64_t itemDurationUs;
- CHECK(itemMeta->findInt64("durationUs", &itemDurationUs));
+ return OK;
+}
- if (mSeekTimeUs < segmentStartUs + itemDurationUs) {
- break;
- }
+bool LiveSession::isSeekable() const {
+ int64_t durationUs;
+ return getDuration(&durationUs) == OK && durationUs >= 0;
+}
- segmentStartUs += itemDurationUs;
- ++index;
- }
+bool LiveSession::hasDynamicDuration() const {
+ return false;
+}
- if (index < mPlaylist->size()) {
- int32_t newSeqNumber = firstSeqNumberInPlaylist + index;
+void LiveSession::changeConfiguration(int64_t timeUs, size_t bandwidthIndex) {
+ CHECK(!mReconfigurationInProgress);
+ mReconfigurationInProgress = true;
- ALOGI("seeking to seq no %d", newSeqNumber);
+ mPrevBandwidthIndex = bandwidthIndex;
- mSeqNumber = newSeqNumber;
+ ALOGV("changeConfiguration => timeUs:%lld us, bwIndex:%d",
+ timeUs, bandwidthIndex);
- mDataSource->reset();
+ 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.
+ AString subtitleURI;
+ if (mPlaylist->getSubtitleURI(item.mPlaylistIndex, &subtitleURI)) {
+ streamMask |= STREAMTYPE_SUBTITLES;
+ }
- ALOGI("new bandwidth does not have the sequence number "
- "we're looking for, switching back to previous bandwidth");
+ // 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);
- mLastPlaylistFetchTimeUs = -1;
- bandwidthIndex = mPrevBandwidthIndex;
- goto rinse_repeat;
- }
+ bool discardFetcher = true;
- if (!mPlaylist->isComplete() && mNumRetries < kMaxNumRetries) {
- ++mNumRetries;
-
- 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);
-
- 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;
+ }
- 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
diff --git a/media/libstagefright/httplive/LiveSession.h b/media/libstagefright/httplive/LiveSession.h
new file mode 100644
index 0000000..b134725
--- /dev/null
+++ b/media/libstagefright/httplive/LiveSession.h
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef LIVE_SESSION_H_
+
+#define LIVE_SESSION_H_
+
+#include <media/stagefright/foundation/AHandler.h>
+
+#include <utils/String8.h>
+
+namespace android {
+
+struct ABuffer;
+struct AnotherPacketSource;
+struct DataSource;
+struct HTTPBase;
+struct LiveDataSource;
+struct M3UParser;
+struct PlaylistFetcher;
+
+struct LiveSession : public AHandler {
+ enum Flags {
+ // Don't log any URLs.
+ kFlagIncognito = 1,
+ };
+ LiveSession(
+ const sp<AMessage> &notify,
+ uint32_t flags = 0, bool uidValid = false, uid_t uid = 0);
+
+ enum StreamType {
+ STREAMTYPE_AUDIO = 1,
+ STREAMTYPE_VIDEO = 2,
+ STREAMTYPE_SUBTITLES = 4,
+ };
+ status_t dequeueAccessUnit(StreamType stream, sp<ABuffer> *accessUnit);
+
+ status_t getStreamFormat(StreamType stream, sp<AMessage> *format);
+
+ void connectAsync(
+ const char *url,
+ const KeyedVector<String8, String8> *headers = NULL);
+
+ status_t disconnect();
+
+ // Blocks until seek is complete.
+ status_t seekTo(int64_t timeUs);
+
+ status_t getDuration(int64_t *durationUs) const;
+
+ bool isSeekable() const;
+ bool hasDynamicDuration() const;
+
+ enum {
+ kWhatStreamsChanged,
+ kWhatError,
+ kWhatPrepared,
+ kWhatPreparationFailed,
+ };
+
+protected:
+ virtual ~LiveSession();
+
+ virtual void onMessageReceived(const sp<AMessage> &msg);
+
+private:
+ friend struct PlaylistFetcher;
+
+ enum {
+ kWhatConnect = 'conn',
+ kWhatDisconnect = 'disc',
+ kWhatSeek = 'seek',
+ kWhatFetcherNotify = 'notf',
+ kWhatCheckBandwidth = 'bndw',
+ kWhatChangeConfiguration2 = 'chC2',
+ kWhatChangeConfiguration3 = 'chC3',
+ kWhatFinishDisconnect2 = 'fin2',
+ };
+
+ struct BandwidthItem {
+ size_t mPlaylistIndex;
+ unsigned long mBandwidth;
+ };
+
+ struct FetcherInfo {
+ sp<PlaylistFetcher> mFetcher;
+ int64_t mDurationUs;
+ bool mIsPrepared;
+ };
+
+ sp<AMessage> mNotify;
+ uint32_t mFlags;
+ bool mUIDValid;
+ uid_t mUID;
+
+ bool mInPreparationPhase;
+
+ sp<HTTPBase> mHTTPDataSource;
+ KeyedVector<String8, String8> mExtraHeaders;
+
+ AString mMasterURL;
+
+ Vector<BandwidthItem> mBandwidthItems;
+ ssize_t mPrevBandwidthIndex;
+
+ sp<M3UParser> mPlaylist;
+
+ KeyedVector<AString, FetcherInfo> mFetcherInfos;
+ AString mAudioURI, mVideoURI, mSubtitleURI;
+ uint32_t mStreamMask;
+
+ KeyedVector<StreamType, sp<AnotherPacketSource> > mPacketSources;
+
+ int32_t mCheckBandwidthGeneration;
+
+ size_t mContinuationCounter;
+ sp<AMessage> mContinuation;
+
+ int64_t mLastDequeuedTimeUs;
+
+ bool mReconfigurationInProgress;
+ uint32_t mDisconnectReplyID;
+
+ sp<PlaylistFetcher> addFetcher(const char *uri);
+
+ void onConnect(const sp<AMessage> &msg);
+ status_t onSeek(const sp<AMessage> &msg);
+ void onFinishDisconnect2();
+
+ status_t fetchFile(
+ const char *url, sp<ABuffer> *out,
+ int64_t range_offset = 0, int64_t range_length = -1);
+
+ sp<M3UParser> fetchPlaylist(
+ const char *url, uint8_t *curPlaylistHash, bool *unchanged);
+
+ size_t getBandwidthIndex();
+
+ static int SortByBandwidth(const BandwidthItem *, const BandwidthItem *);
+
+ void changeConfiguration(int64_t timeUs, size_t bandwidthIndex);
+ void onChangeConfiguration2(const sp<AMessage> &msg);
+ void onChangeConfiguration3(const sp<AMessage> &msg);
+
+ void scheduleCheckBandwidthEvent();
+ void cancelCheckBandwidthEvent();
+
+ void onCheckBandwidth();
+
+ void finishDisconnect();
+
+ void postPrepared(status_t err);
+
+ DISALLOW_EVIL_CONSTRUCTORS(LiveSession);
+};
+
+} // namespace android
+
+#endif // LIVE_SESSION_H_
diff --git a/media/libstagefright/httplive/M3UParser.cpp b/media/libstagefright/httplive/M3UParser.cpp
index 68bbca2..be66252 100644
--- a/media/libstagefright/httplive/M3UParser.cpp
+++ b/media/libstagefright/httplive/M3UParser.cpp
@@ -18,14 +18,153 @@
#define LOG_TAG "M3UParser"
#include <utils/Log.h>
-#include "include/M3UParser.h"
+#include "M3UParser.h"
+#include <cutils/properties.h>
#include <media/stagefright/foundation/ADebug.h>
#include <media/stagefright/foundation/AMessage.h>
#include <media/stagefright/MediaErrors.h>
namespace android {
+struct M3UParser::MediaGroup : public RefBase {
+ enum Type {
+ TYPE_AUDIO,
+ TYPE_VIDEO,
+ TYPE_SUBS,
+ };
+
+ enum FlagBits {
+ FLAG_AUTOSELECT = 1,
+ FLAG_DEFAULT = 2,
+ FLAG_FORCED = 4,
+ FLAG_HAS_LANGUAGE = 8,
+ FLAG_HAS_URI = 16,
+ };
+
+ MediaGroup(Type type);
+
+ Type type() const;
+
+ status_t addMedia(
+ const char *name,
+ const char *uri,
+ const char *language,
+ uint32_t flags);
+
+ bool getActiveURI(AString *uri) const;
+
+ void pickRandomMediaItems();
+
+protected:
+ virtual ~MediaGroup();
+
+private:
+ struct Media {
+ AString mName;
+ AString mURI;
+ AString mLanguage;
+ uint32_t mFlags;
+ };
+
+ Type mType;
+ Vector<Media> mMediaItems;
+
+ ssize_t mSelectedIndex;
+
+ DISALLOW_EVIL_CONSTRUCTORS(MediaGroup);
+};
+
+M3UParser::MediaGroup::MediaGroup(Type type)
+ : mType(type),
+ mSelectedIndex(-1) {
+}
+
+M3UParser::MediaGroup::~MediaGroup() {
+}
+
+M3UParser::MediaGroup::Type M3UParser::MediaGroup::type() const {
+ return mType;
+}
+
+status_t M3UParser::MediaGroup::addMedia(
+ const char *name,
+ const char *uri,
+ const char *language,
+ uint32_t flags) {
+ mMediaItems.push();
+ Media &item = mMediaItems.editItemAt(mMediaItems.size() - 1);
+
+ item.mName = name;
+
+ if (uri) {
+ item.mURI = uri;
+ }
+
+ if (language) {
+ item.mLanguage = language;
+ }
+
+ item.mFlags = flags;
+
+ return OK;
+}
+
+void M3UParser::MediaGroup::pickRandomMediaItems() {
+#if 1
+ switch (mType) {
+ case TYPE_AUDIO:
+ {
+ char value[PROPERTY_VALUE_MAX];
+ if (property_get("media.httplive.audio-index", value, NULL)) {
+ char *end;
+ mSelectedIndex = strtoul(value, &end, 10);
+ CHECK(end > value && *end == '\0');
+
+ if (mSelectedIndex >= mMediaItems.size()) {
+ mSelectedIndex = mMediaItems.size() - 1;
+ }
+ } else {
+ mSelectedIndex = 0;
+ }
+ break;
+ }
+
+ case TYPE_VIDEO:
+ {
+ mSelectedIndex = 0;
+ break;
+ }
+
+ case TYPE_SUBS:
+ {
+ mSelectedIndex = -1;
+ break;
+ }
+
+ default:
+ TRESPASS();
+ }
+#else
+ mSelectedIndex = (rand() * mMediaItems.size()) / RAND_MAX;
+#endif
+}
+
+bool M3UParser::MediaGroup::getActiveURI(AString *uri) const {
+ for (size_t i = 0; i < mMediaItems.size(); ++i) {
+ if (mSelectedIndex >= 0 && i == (size_t)mSelectedIndex) {
+ const Media &item = mMediaItems.itemAt(i);
+
+ *uri = item.mURI;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
M3UParser::M3UParser(
const char *baseURI, const void *data, size_t size)
: mInitCheck(NO_INIT),
@@ -92,6 +231,58 @@ bool M3UParser::itemAt(size_t index, AString *uri, sp<AMessage> *meta) {
return true;
}
+void M3UParser::pickRandomMediaItems() {
+ for (size_t i = 0; i < mMediaGroups.size(); ++i) {
+ mMediaGroups.valueAt(i)->pickRandomMediaItems();
+ }
+}
+
+bool M3UParser::getTypeURI(size_t index, const char *key, AString *uri) const {
+ if (!mIsVariantPlaylist) {
+ *uri = mBaseURI;
+
+ // Assume media without any more specific attribute contains
+ // audio and video, but no subtitles.
+ return !strcmp("audio", key) || !strcmp("video", key);
+ }
+
+ CHECK_LT(index, mItems.size());
+
+ sp<AMessage> meta = mItems.itemAt(index).mMeta;
+
+ AString groupID;
+ if (!meta->findString(key, &groupID)) {
+ *uri = mItems.itemAt(index).mURI;
+
+ // Assume media without any more specific attribute contains
+ // audio and video, but no subtitles.
+ return !strcmp("audio", key) || !strcmp("video", key);
+ }
+
+ sp<MediaGroup> group = mMediaGroups.valueFor(groupID);
+ if (!group->getActiveURI(uri)) {
+ return false;
+ }
+
+ if ((*uri).empty()) {
+ *uri = mItems.itemAt(index).mURI;
+ }
+
+ return true;
+}
+
+bool M3UParser::getAudioURI(size_t index, AString *uri) const {
+ return getTypeURI(index, "audio", uri);
+}
+
+bool M3UParser::getVideoURI(size_t index, AString *uri) const {
+ return getTypeURI(index, "video", uri);
+}
+
+bool M3UParser::getSubtitleURI(size_t index, AString *uri) const {
+ return getTypeURI(index, "subtitles", uri);
+}
+
static bool MakeURL(const char *baseURL, const char *url, AString *out) {
out->clear();
@@ -241,6 +432,8 @@ status_t M3UParser::parse(const void *_data, size_t size) {
segmentRangeOffset = offset + length;
}
+ } else if (line.startsWith("#EXT-X-MEDIA")) {
+ err = parseMedia(line);
}
if (err != OK) {
@@ -322,9 +515,31 @@ status_t M3UParser::parseMetaDataDuration(
return OK;
}
-// static
+// Find the next occurence of the character "what" at or after "offset",
+// but ignore occurences between quotation marks.
+// Return the index of the occurrence or -1 if not found.
+static ssize_t FindNextUnquoted(
+ const AString &line, char what, size_t offset) {
+ CHECK_NE((int)what, (int)'"');
+
+ bool quoted = false;
+ while (offset < line.size()) {
+ char c = line.c_str()[offset];
+
+ if (c == '"') {
+ quoted = !quoted;
+ } else if (c == what && !quoted) {
+ return offset;
+ }
+
+ ++offset;
+ }
+
+ return -1;
+}
+
status_t M3UParser::parseStreamInf(
- const AString &line, sp<AMessage> *meta) {
+ const AString &line, sp<AMessage> *meta) const {
ssize_t colonPos = line.find(":");
if (colonPos < 0) {
@@ -334,7 +549,7 @@ status_t M3UParser::parseStreamInf(
size_t offset = colonPos + 1;
while (offset < line.size()) {
- ssize_t end = line.find(",", offset);
+ ssize_t end = FindNextUnquoted(line, ',', offset);
if (end < 0) {
end = line.size();
}
@@ -371,33 +586,35 @@ status_t M3UParser::parseStreamInf(
*meta = new AMessage;
}
(*meta)->setInt32("bandwidth", x);
- }
- }
+ } else if (!strcasecmp("audio", key.c_str())
+ || !strcasecmp("video", key.c_str())
+ || !strcasecmp("subtitles", key.c_str())) {
+ if (val.size() < 2
+ || val.c_str()[0] != '"'
+ || val.c_str()[val.size() - 1] != '"') {
+ ALOGE("Expected quoted string for %s attribute, "
+ "got '%s' instead.",
+ key.c_str(), val.c_str());
+
+ return ERROR_MALFORMED;
+ }
- return OK;
-}
+ AString groupID(val, 1, val.size() - 2);
+ ssize_t groupIndex = mMediaGroups.indexOfKey(groupID);
-// Find the next occurence of the character "what" at or after "offset",
-// but ignore occurences between quotation marks.
-// Return the index of the occurrence or -1 if not found.
-static ssize_t FindNextUnquoted(
- const AString &line, char what, size_t offset) {
- CHECK_NE((int)what, (int)'"');
+ if (groupIndex < 0) {
+ ALOGE("Undefined media group '%s' referenced in stream info.",
+ groupID.c_str());
- bool quoted = false;
- while (offset < line.size()) {
- char c = line.c_str()[offset];
+ return ERROR_MALFORMED;
+ }
- if (c == '"') {
- quoted = !quoted;
- } else if (c == what && !quoted) {
- return offset;
+ key.tolower();
+ (*meta)->setString(key.c_str(), groupID.c_str());
}
-
- ++offset;
}
- return -1;
+ return OK;
}
// static
@@ -515,6 +732,234 @@ status_t M3UParser::parseByteRange(
return OK;
}
+status_t M3UParser::parseMedia(const AString &line) {
+ ssize_t colonPos = line.find(":");
+
+ if (colonPos < 0) {
+ return ERROR_MALFORMED;
+ }
+
+ bool haveGroupType = false;
+ MediaGroup::Type groupType = MediaGroup::TYPE_AUDIO;
+
+ bool haveGroupID = false;
+ AString groupID;
+
+ bool haveGroupLanguage = false;
+ AString groupLanguage;
+
+ bool haveGroupName = false;
+ AString groupName;
+
+ bool haveGroupAutoselect = false;
+ bool groupAutoselect = false;
+
+ bool haveGroupDefault = false;
+ bool groupDefault = false;
+
+ bool haveGroupForced = false;
+ bool groupForced = false;
+
+ bool haveGroupURI = false;
+ AString groupURI;
+
+ size_t offset = colonPos + 1;
+
+ while (offset < line.size()) {
+ ssize_t end = FindNextUnquoted(line, ',', offset);
+ if (end < 0) {
+ end = line.size();
+ }
+
+ AString attr(line, offset, end - offset);
+ attr.trim();
+
+ offset = end + 1;
+
+ ssize_t equalPos = attr.find("=");
+ if (equalPos < 0) {
+ continue;
+ }
+
+ AString key(attr, 0, equalPos);
+ key.trim();
+
+ AString val(attr, equalPos + 1, attr.size() - equalPos - 1);
+ val.trim();
+
+ ALOGV("key=%s value=%s", key.c_str(), val.c_str());
+
+ if (!strcasecmp("type", key.c_str())) {
+ if (!strcasecmp("subtitles", val.c_str())) {
+ groupType = MediaGroup::TYPE_SUBS;
+ } else if (!strcasecmp("audio", val.c_str())) {
+ groupType = MediaGroup::TYPE_AUDIO;
+ } else if (!strcasecmp("video", val.c_str())) {
+ groupType = MediaGroup::TYPE_VIDEO;
+ } else {
+ ALOGE("Invalid media group type '%s'", val.c_str());
+ return ERROR_MALFORMED;
+ }
+
+ haveGroupType = true;
+ } else if (!strcasecmp("group-id", key.c_str())) {
+ if (val.size() < 2
+ || val.c_str()[0] != '"'
+ || val.c_str()[val.size() - 1] != '"') {
+ ALOGE("Expected quoted string for GROUP-ID, got '%s' instead.",
+ val.c_str());
+
+ return ERROR_MALFORMED;
+ }
+
+ groupID.setTo(val, 1, val.size() - 2);
+ haveGroupID = true;
+ } else if (!strcasecmp("language", key.c_str())) {
+ if (val.size() < 2
+ || val.c_str()[0] != '"'
+ || val.c_str()[val.size() - 1] != '"') {
+ ALOGE("Expected quoted string for LANGUAGE, got '%s' instead.",
+ val.c_str());
+
+ return ERROR_MALFORMED;
+ }
+
+ groupLanguage.setTo(val, 1, val.size() - 2);
+ haveGroupLanguage = true;
+ } else if (!strcasecmp("name", key.c_str())) {
+ if (val.size() < 2
+ || val.c_str()[0] != '"'
+ || val.c_str()[val.size() - 1] != '"') {
+ ALOGE("Expected quoted string for NAME, got '%s' instead.",
+ val.c_str());
+
+ return ERROR_MALFORMED;
+ }
+
+ groupName.setTo(val, 1, val.size() - 2);
+ haveGroupName = true;
+ } else if (!strcasecmp("autoselect", key.c_str())) {
+ groupAutoselect = false;
+ if (!strcasecmp("YES", val.c_str())) {
+ groupAutoselect = true;
+ } else if (!strcasecmp("NO", val.c_str())) {
+ groupAutoselect = false;
+ } else {
+ ALOGE("Expected YES or NO for AUTOSELECT attribute, "
+ "got '%s' instead.",
+ val.c_str());
+
+ return ERROR_MALFORMED;
+ }
+
+ haveGroupAutoselect = true;
+ } else if (!strcasecmp("default", key.c_str())) {
+ groupDefault = false;
+ if (!strcasecmp("YES", val.c_str())) {
+ groupDefault = true;
+ } else if (!strcasecmp("NO", val.c_str())) {
+ groupDefault = false;
+ } else {
+ ALOGE("Expected YES or NO for DEFAULT attribute, "
+ "got '%s' instead.",
+ val.c_str());
+
+ return ERROR_MALFORMED;
+ }
+
+ haveGroupDefault = true;
+ } else if (!strcasecmp("forced", key.c_str())) {
+ groupForced = false;
+ if (!strcasecmp("YES", val.c_str())) {
+ groupForced = true;
+ } else if (!strcasecmp("NO", val.c_str())) {
+ groupForced = false;
+ } else {
+ ALOGE("Expected YES or NO for FORCED attribute, "
+ "got '%s' instead.",
+ val.c_str());
+
+ return ERROR_MALFORMED;
+ }
+
+ haveGroupForced = true;
+ } else if (!strcasecmp("uri", key.c_str())) {
+ if (val.size() < 2
+ || val.c_str()[0] != '"'
+ || val.c_str()[val.size() - 1] != '"') {
+ ALOGE("Expected quoted string for URI, got '%s' instead.",
+ val.c_str());
+
+ return ERROR_MALFORMED;
+ }
+
+ AString tmp(val, 1, val.size() - 2);
+
+ if (!MakeURL(mBaseURI.c_str(), tmp.c_str(), &groupURI)) {
+ ALOGI("Failed to make absolute URI from '%s'.", tmp.c_str());
+ }
+
+ haveGroupURI = true;
+ }
+ }
+
+ if (!haveGroupType || !haveGroupID || !haveGroupName) {
+ ALOGE("Incomplete EXT-X-MEDIA element.");
+ return ERROR_MALFORMED;
+ }
+
+ uint32_t flags = 0;
+ if (haveGroupAutoselect && groupAutoselect) {
+ flags |= MediaGroup::FLAG_AUTOSELECT;
+ }
+ if (haveGroupDefault && groupDefault) {
+ flags |= MediaGroup::FLAG_DEFAULT;
+ }
+ if (haveGroupForced) {
+ if (groupType != MediaGroup::TYPE_SUBS) {
+ ALOGE("The FORCED attribute MUST not be present on anything "
+ "but SUBS media.");
+
+ return ERROR_MALFORMED;
+ }
+
+ if (groupForced) {
+ flags |= MediaGroup::FLAG_FORCED;
+ }
+ }
+ if (haveGroupLanguage) {
+ flags |= MediaGroup::FLAG_HAS_LANGUAGE;
+ }
+ if (haveGroupURI) {
+ flags |= MediaGroup::FLAG_HAS_URI;
+ }
+
+ ssize_t groupIndex = mMediaGroups.indexOfKey(groupID);
+ sp<MediaGroup> group;
+
+ if (groupIndex < 0) {
+ group = new MediaGroup(groupType);
+ mMediaGroups.add(groupID, group);
+ } else {
+ group = mMediaGroups.valueAt(groupIndex);
+
+ if (group->type() != groupType) {
+ ALOGE("Attempt to put media item under group of different type "
+ "(groupType = %d, item type = %d",
+ group->type(),
+ groupType);
+
+ return ERROR_MALFORMED;
+ }
+ }
+
+ return group->addMedia(
+ groupName.c_str(),
+ haveGroupURI ? groupURI.c_str() : NULL,
+ haveGroupLanguage ? groupLanguage.c_str() : NULL,
+ flags);
+}
+
// static
status_t M3UParser::ParseInt32(const char *s, int32_t *x) {
char *end;
diff --git a/media/libstagefright/httplive/M3UParser.h b/media/libstagefright/httplive/M3UParser.h
new file mode 100644
index 0000000..abea286
--- /dev/null
+++ b/media/libstagefright/httplive/M3UParser.h
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef M3U_PARSER_H_
+
+#define M3U_PARSER_H_
+
+#include <media/stagefright/foundation/ABase.h>
+#include <media/stagefright/foundation/AMessage.h>
+#include <media/stagefright/foundation/AString.h>
+#include <utils/Vector.h>
+
+namespace android {
+
+struct M3UParser : public RefBase {
+ M3UParser(const char *baseURI, const void *data, size_t size);
+
+ status_t initCheck() const;
+
+ bool isExtM3U() const;
+ bool isVariantPlaylist() const;
+ bool isComplete() const;
+ bool isEvent() const;
+
+ sp<AMessage> meta();
+
+ size_t size();
+ bool itemAt(size_t index, AString *uri, sp<AMessage> *meta = NULL);
+
+ void pickRandomMediaItems();
+
+ bool getAudioURI(size_t index, AString *uri) const;
+ bool getVideoURI(size_t index, AString *uri) const;
+ bool getSubtitleURI(size_t index, AString *uri) const;
+
+protected:
+ virtual ~M3UParser();
+
+private:
+ struct MediaGroup;
+
+ struct Item {
+ AString mURI;
+ sp<AMessage> mMeta;
+ };
+
+ status_t mInitCheck;
+
+ AString mBaseURI;
+ bool mIsExtM3U;
+ bool mIsVariantPlaylist;
+ bool mIsComplete;
+ bool mIsEvent;
+
+ sp<AMessage> mMeta;
+ Vector<Item> mItems;
+
+ // Media groups keyed by group ID.
+ KeyedVector<AString, sp<MediaGroup> > mMediaGroups;
+
+ status_t parse(const void *data, size_t size);
+
+ static status_t parseMetaData(
+ const AString &line, sp<AMessage> *meta, const char *key);
+
+ static status_t parseMetaDataDuration(
+ const AString &line, sp<AMessage> *meta, const char *key);
+
+ status_t parseStreamInf(
+ const AString &line, sp<AMessage> *meta) const;
+
+ static status_t parseCipherInfo(
+ const AString &line, sp<AMessage> *meta, const AString &baseURI);
+
+ static status_t parseByteRange(
+ const AString &line, uint64_t curOffset,
+ uint64_t *length, uint64_t *offset);
+
+ status_t parseMedia(const AString &line);
+
+ bool getTypeURI(size_t index, const char *key, AString *uri) const;
+
+ static status_t ParseInt32(const char *s, int32_t *x);
+ static status_t ParseDouble(const char *s, double *x);
+
+ DISALLOW_EVIL_CONSTRUCTORS(M3UParser);
+};
+
+} // namespace android
+
+#endif // M3U_PARSER_H_
diff --git a/media/libstagefright/httplive/PlaylistFetcher.cpp b/media/libstagefright/httplive/PlaylistFetcher.cpp
new file mode 100644
index 0000000..8ae70b7
--- /dev/null
+++ b/media/libstagefright/httplive/PlaylistFetcher.cpp
@@ -0,0 +1,969 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+//#define LOG_NDEBUG 0
+#define LOG_TAG "PlaylistFetcher"
+#include <utils/Log.h>
+
+#include "PlaylistFetcher.h"
+
+#include "LiveDataSource.h"
+#include "LiveSession.h"
+#include "M3UParser.h"
+
+#include "include/avc_utils.h"
+#include "include/HTTPBase.h"
+#include "include/ID3.h"
+#include "mpeg2ts/AnotherPacketSource.h"
+
+#include <media/IStreamSource.h>
+#include <media/stagefright/foundation/ABitReader.h>
+#include <media/stagefright/foundation/ABuffer.h>
+#include <media/stagefright/foundation/ADebug.h>
+#include <media/stagefright/foundation/hexdump.h>
+#include <media/stagefright/FileSource.h>
+#include <media/stagefright/MediaDefs.h>
+#include <media/stagefright/MetaData.h>
+#include <media/stagefright/Utils.h>
+
+#include <ctype.h>
+#include <openssl/aes.h>
+#include <openssl/md5.h>
+
+namespace android {
+
+// static
+const int64_t PlaylistFetcher::kMinBufferedDurationUs = 10000000ll;
+
+PlaylistFetcher::PlaylistFetcher(
+ const sp<AMessage> &notify,
+ const sp<LiveSession> &session,
+ const char *uri)
+ : mNotify(notify),
+ mSession(session),
+ mURI(uri),
+ mStreamTypeMask(0),
+ mStartTimeUs(-1ll),
+ mLastPlaylistFetchTimeUs(-1ll),
+ mSeqNumber(-1),
+ mNumRetries(0),
+ mStartup(true),
+ mNextPTSTimeUs(-1ll),
+ mMonitorQueueGeneration(0),
+ mRefreshState(INITIAL_MINIMUM_RELOAD_DELAY),
+ mFirstPTSValid(false),
+ mAbsoluteTimeAnchorUs(0ll) {
+ memset(mPlaylistHash, 0, sizeof(mPlaylistHash));
+}
+
+PlaylistFetcher::~PlaylistFetcher() {
+}
+
+int64_t PlaylistFetcher::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;
+}
+
+bool PlaylistFetcher::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;
+ }
+
+ default:
+ TRESPASS();
+ break;
+ }
+
+ return mLastPlaylistFetchTimeUs + minPlaylistAgeUs <= nowUs;
+}
+
+status_t PlaylistFetcher::decryptBuffer(
+ size_t playlistIndex, const sp<ABuffer> &buffer) {
+ sp<AMessage> itemMeta;
+ bool found = false;
+ AString method;
+
+ for (ssize_t i = playlistIndex; i >= 0; --i) {
+ AString uri;
+ CHECK(mPlaylist->itemAt(i, &uri, &itemMeta));
+
+ if (itemMeta->findString("cipher-method", &method)) {
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ method = "NONE";
+ }
+
+ if (method == "NONE") {
+ return OK;
+ } else if (!(method == "AES-128")) {
+ ALOGE("Unsupported cipher method '%s'", method.c_str());
+ return ERROR_UNSUPPORTED;
+ }
+
+ AString keyURI;
+ if (!itemMeta->findString("cipher-uri", &keyURI)) {
+ ALOGE("Missing key uri");
+ return ERROR_MALFORMED;
+ }
+
+ ssize_t index = mAESKeyForURI.indexOfKey(keyURI);
+
+ sp<ABuffer> key;
+ if (index >= 0) {
+ key = mAESKeyForURI.valueAt(index);
+ } else {
+ status_t err = mSession->fetchFile(keyURI.c_str(), &key);
+
+ if (err != OK) {
+ ALOGE("failed to fetch cipher key from '%s'.", keyURI.c_str());
+ return ERROR_IO;
+ } else if (key->size() != 16) {
+ ALOGE("key file '%s' wasn't 16 bytes in size.", keyURI.c_str());
+ return ERROR_MALFORMED;
+ }
+
+ mAESKeyForURI.add(keyURI, key);
+ }
+
+ 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;
+ }
+
+ 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;
+
+ aes_ivec[i] = nibble1 << 4 | nibble2;
+ }
+ } 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;
+ }
+
+ AES_cbc_encrypt(
+ buffer->data(), buffer->data(), buffer->size(),
+ &aes_key, aes_ivec, AES_DECRYPT);
+
+ // hexdump(buffer->data(), buffer->size());
+
+ size_t n = buffer->size();
+ CHECK_GT(n, 0u);
+
+ size_t pad = buffer->data()[n - 1];
+
+ 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);
+ }
+
+ n -= pad;
+
+ buffer->setRange(buffer->offset(), n);
+
+ return OK;
+}
+
+void PlaylistFetcher::postMonitorQueue(int64_t delayUs) {
+ sp<AMessage> msg = new AMessage(kWhatMonitorQueue, id());
+ msg->setInt32("generation", mMonitorQueueGeneration);
+ msg->post(delayUs);
+}
+
+void PlaylistFetcher::cancelMonitorQueue() {
+ ++mMonitorQueueGeneration;
+}
+
+void PlaylistFetcher::startAsync(
+ const sp<AnotherPacketSource> &audioSource,
+ const sp<AnotherPacketSource> &videoSource,
+ const sp<AnotherPacketSource> &subtitleSource,
+ int64_t startTimeUs) {
+ sp<AMessage> msg = new AMessage(kWhatStart, id());
+
+ uint32_t streamTypeMask = 0ul;
+
+ if (audioSource != NULL) {
+ msg->setPointer("audioSource", audioSource.get());
+ streamTypeMask |= LiveSession::STREAMTYPE_AUDIO;
+ }
+
+ if (videoSource != NULL) {
+ msg->setPointer("videoSource", videoSource.get());
+ streamTypeMask |= LiveSession::STREAMTYPE_VIDEO;
+ }
+
+ if (subtitleSource != NULL) {
+ msg->setPointer("subtitleSource", subtitleSource.get());
+ streamTypeMask |= LiveSession::STREAMTYPE_SUBTITLES;
+ }
+
+ msg->setInt32("streamTypeMask", streamTypeMask);
+ msg->setInt64("startTimeUs", startTimeUs);
+ msg->post();
+}
+
+void PlaylistFetcher::pauseAsync() {
+ (new AMessage(kWhatPause, id()))->post();
+}
+
+void PlaylistFetcher::stopAsync() {
+ (new AMessage(kWhatStop, id()))->post();
+}
+
+void PlaylistFetcher::onMessageReceived(const sp<AMessage> &msg) {
+ switch (msg->what()) {
+ case kWhatStart:
+ {
+ status_t err = onStart(msg);
+
+ sp<AMessage> notify = mNotify->dup();
+ notify->setInt32("what", kWhatStarted);
+ notify->setInt32("err", err);
+ notify->post();
+ break;
+ }
+
+ case kWhatPause:
+ {
+ onPause();
+
+ sp<AMessage> notify = mNotify->dup();
+ notify->setInt32("what", kWhatPaused);
+ notify->post();
+ break;
+ }
+
+ case kWhatStop:
+ {
+ onStop();
+
+ sp<AMessage> notify = mNotify->dup();
+ notify->setInt32("what", kWhatStopped);
+ notify->post();
+ break;
+ }
+
+ case kWhatMonitorQueue:
+ {
+ int32_t generation;
+ CHECK(msg->findInt32("generation", &generation));
+
+ if (generation != mMonitorQueueGeneration) {
+ // Stale event
+ break;
+ }
+
+ onMonitorQueue();
+ break;
+ }
+
+ default:
+ TRESPASS();
+ }
+}
+
+status_t PlaylistFetcher::onStart(const sp<AMessage> &msg) {
+ mPacketSources.clear();
+
+ uint32_t streamTypeMask;
+ CHECK(msg->findInt32("streamTypeMask", (int32_t *)&streamTypeMask));
+
+ int64_t startTimeUs;
+ CHECK(msg->findInt64("startTimeUs", &startTimeUs));
+
+ if (streamTypeMask & LiveSession::STREAMTYPE_AUDIO) {
+ void *ptr;
+ CHECK(msg->findPointer("audioSource", &ptr));
+
+ mPacketSources.add(
+ LiveSession::STREAMTYPE_AUDIO,
+ static_cast<AnotherPacketSource *>(ptr));
+ }
+
+ if (streamTypeMask & LiveSession::STREAMTYPE_VIDEO) {
+ void *ptr;
+ CHECK(msg->findPointer("videoSource", &ptr));
+
+ mPacketSources.add(
+ LiveSession::STREAMTYPE_VIDEO,
+ static_cast<AnotherPacketSource *>(ptr));
+ }
+
+ if (streamTypeMask & LiveSession::STREAMTYPE_SUBTITLES) {
+ void *ptr;
+ CHECK(msg->findPointer("subtitleSource", &ptr));
+
+ mPacketSources.add(
+ LiveSession::STREAMTYPE_SUBTITLES,
+ static_cast<AnotherPacketSource *>(ptr));
+ }
+
+ mStreamTypeMask = streamTypeMask;
+ mStartTimeUs = startTimeUs;
+
+ if (mStartTimeUs >= 0ll) {
+ mSeqNumber = -1;
+ mStartup = true;
+ }
+
+ postMonitorQueue();
+
+ return OK;
+}
+
+void PlaylistFetcher::onPause() {
+ cancelMonitorQueue();
+
+ mPacketSources.clear();
+ mStreamTypeMask = 0;
+}
+
+void PlaylistFetcher::onStop() {
+ cancelMonitorQueue();
+
+ for (size_t i = 0; i < mPacketSources.size(); ++i) {
+ mPacketSources.valueAt(i)->clear();
+ }
+
+ mPacketSources.clear();
+ mStreamTypeMask = 0;
+}
+
+void PlaylistFetcher::notifyError(status_t err) {
+ sp<AMessage> notify = mNotify->dup();
+ notify->setInt32("what", kWhatError);
+ notify->setInt32("err", err);
+ notify->post();
+}
+
+void PlaylistFetcher::queueDiscontinuity(
+ ATSParser::DiscontinuityType type, const sp<AMessage> &extra) {
+ for (size_t i = 0; i < mPacketSources.size(); ++i) {
+ mPacketSources.valueAt(i)->queueDiscontinuity(type, extra);
+ }
+}
+
+void PlaylistFetcher::onMonitorQueue() {
+ bool downloadMore = false;
+
+ status_t finalResult;
+ if (mStreamTypeMask == LiveSession::STREAMTYPE_SUBTITLES) {
+ sp<AnotherPacketSource> packetSource =
+ mPacketSources.valueFor(LiveSession::STREAMTYPE_SUBTITLES);
+
+ downloadMore = packetSource->hasBufferAvailable(&finalResult);
+ } else {
+ bool first = true;
+ int64_t minBufferedDurationUs = 0ll;
+
+ for (size_t i = 0; i < mPacketSources.size(); ++i) {
+ if ((mStreamTypeMask & mPacketSources.keyAt(i)) == 0) {
+ continue;
+ }
+
+ int64_t bufferedDurationUs =
+ mPacketSources.valueAt(i)->getBufferedDurationUs(&finalResult);
+
+ if (first || bufferedDurationUs < minBufferedDurationUs) {
+ minBufferedDurationUs = bufferedDurationUs;
+ first = false;
+ }
+ }
+
+ downloadMore =
+ !first && (minBufferedDurationUs < kMinBufferedDurationUs);
+ }
+
+ if (finalResult == OK && downloadMore) {
+ onDownloadNext();
+ } else {
+ // Nothing to do yet, try again in a second.
+
+ sp<AMessage> msg = mNotify->dup();
+ msg->setInt32("what", kWhatTemporarilyDoneFetching);
+ msg->post();
+
+ postMonitorQueue(1000000ll);
+ }
+}
+
+void PlaylistFetcher::onDownloadNext() {
+ int64_t nowUs = ALooper::GetNowUs();
+
+ if (mLastPlaylistFetchTimeUs < 0ll
+ || (!mPlaylist->isComplete() && timeToRefreshPlaylist(nowUs))) {
+ bool unchanged;
+ sp<M3UParser> playlist = mSession->fetchPlaylist(
+ mURI.c_str(), mPlaylistHash, &unchanged);
+
+ if (playlist == NULL) {
+ if (unchanged) {
+ // We succeeded in fetching the playlist, but it was
+ // unchanged from the last time we tried.
+
+ if (mRefreshState != THIRD_UNCHANGED_RELOAD_ATTEMPT) {
+ mRefreshState = (RefreshState)(mRefreshState + 1);
+ }
+ } else {
+ ALOGE("failed to load playlist at url '%s'", mURI.c_str());
+ notifyError(ERROR_IO);
+ return;
+ }
+ } else {
+ mRefreshState = INITIAL_MINIMUM_RELOAD_DELAY;
+ mPlaylist = playlist;
+
+ if (mPlaylist->isComplete() || mPlaylist->isEvent()) {
+ updateDuration();
+ }
+ }
+
+ mLastPlaylistFetchTimeUs = ALooper::GetNowUs();
+ }
+
+ int32_t firstSeqNumberInPlaylist;
+ if (mPlaylist->meta() == NULL || !mPlaylist->meta()->findInt32(
+ "media-sequence", &firstSeqNumberInPlaylist)) {
+ firstSeqNumberInPlaylist = 0;
+ }
+
+ bool seekDiscontinuity = false;
+ bool explicitDiscontinuity = false;
+
+ const int32_t lastSeqNumberInPlaylist =
+ firstSeqNumberInPlaylist + (int32_t)mPlaylist->size() - 1;
+
+ if (mSeqNumber < 0) {
+ CHECK_GE(mStartTimeUs, 0ll);
+
+ if (mPlaylist->isComplete() || mPlaylist->isEvent()) {
+ mSeqNumber = getSeqNumberForTime(mStartTimeUs);
+ } else {
+ // If this is a live session, start 3 segments from the end.
+ mSeqNumber = lastSeqNumberInPlaylist - 3;
+ if (mSeqNumber < firstSeqNumberInPlaylist) {
+ mSeqNumber = firstSeqNumberInPlaylist;
+ }
+ }
+
+ mStartTimeUs = -1ll;
+ }
+
+ if (mSeqNumber < firstSeqNumberInPlaylist
+ || mSeqNumber > lastSeqNumberInPlaylist) {
+ if (!mPlaylist->isComplete() && mNumRetries < kMaxNumRetries) {
+ ++mNumRetries;
+
+ if (mSeqNumber > lastSeqNumberInPlaylist) {
+ mLastPlaylistFetchTimeUs = -1;
+ postMonitorQueue(3000000ll);
+ return;
+ }
+
+ // 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
+ } else {
+ ALOGE("Cannot find sequence number %d in playlist "
+ "(contains %d - %d)",
+ mSeqNumber, firstSeqNumberInPlaylist,
+ firstSeqNumberInPlaylist + mPlaylist->size() - 1);
+
+ notifyError(ERROR_END_OF_STREAM);
+ return;
+ }
+ }
+
+ 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;
+ }
+
+ 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;
+ }
+
+ ALOGV("fetching segment %d from (%d .. %d)",
+ mSeqNumber, firstSeqNumberInPlaylist, lastSeqNumberInPlaylist);
+
+ ALOGV("fetching '%s'", uri.c_str());
+
+ sp<ABuffer> buffer;
+ status_t err = mSession->fetchFile(
+ uri.c_str(), &buffer, range_offset, range_length);
+
+ if (err != OK) {
+ ALOGE("failed to fetch .ts segment at url '%s'", uri.c_str());
+ notifyError(err);
+ return;
+ }
+
+ CHECK(buffer != NULL);
+
+ err = decryptBuffer(mSeqNumber - firstSeqNumberInPlaylist, buffer);
+
+ if (err != OK) {
+ ALOGE("decryptBuffer failed w/ error %d", err);
+
+ notifyError(err);
+ return;
+ }
+
+ if (mStartup || seekDiscontinuity || explicitDiscontinuity) {
+ // Signal discontinuity.
+
+ 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.
+ mNextPTSTimeUs = getSegmentStartTimeUs(mSeqNumber);
+ }
+
+ if (seekDiscontinuity || explicitDiscontinuity) {
+ ALOGI("queueing discontinuity (seek=%d, explicit=%d)",
+ seekDiscontinuity, explicitDiscontinuity);
+
+ queueDiscontinuity(
+ explicitDiscontinuity
+ ? ATSParser::DISCONTINUITY_FORMATCHANGE
+ : ATSParser::DISCONTINUITY_SEEK,
+ NULL /* extra */);
+ }
+ }
+
+ err = extractAndQueueAccessUnits(buffer);
+
+ if (err != OK) {
+ notifyError(err);
+ return;
+ }
+
+ ++mSeqNumber;
+
+ postMonitorQueue();
+
+ mStartup = false;
+}
+
+int32_t PlaylistFetcher::getSeqNumberForTime(int64_t timeUs) const {
+ int32_t firstSeqNumberInPlaylist;
+ if (mPlaylist->meta() == NULL || !mPlaylist->meta()->findInt32(
+ "media-sequence", &firstSeqNumberInPlaylist)) {
+ firstSeqNumberInPlaylist = 0;
+ }
+
+ size_t index = 0;
+ int64_t segmentStartUs = 0;
+ while (index < mPlaylist->size()) {
+ sp<AMessage> itemMeta;
+ CHECK(mPlaylist->itemAt(
+ index, NULL /* uri */, &itemMeta));
+
+ int64_t itemDurationUs;
+ CHECK(itemMeta->findInt64("durationUs", &itemDurationUs));
+
+ if (timeUs < segmentStartUs + itemDurationUs) {
+ break;
+ }
+
+ segmentStartUs += itemDurationUs;
+ ++index;
+ }
+
+ if (index >= mPlaylist->size()) {
+ index = mPlaylist->size() - 1;
+ }
+
+ return firstSeqNumberInPlaylist + index;
+}
+
+status_t PlaylistFetcher::extractAndQueueAccessUnits(
+ const sp<ABuffer> &buffer) {
+ if (buffer->size() > 0 && buffer->data()[0] == 0x47) {
+ // Let's assume this is an MPEG2 transport stream.
+
+ if ((buffer->size() % 188) != 0) {
+ ALOGE("MPEG2 transport stream is not an even multiple of 188 "
+ "bytes in length.");
+ return ERROR_MALFORMED;
+ }
+
+ if (mTSParser == NULL) {
+ mTSParser = new ATSParser;
+ }
+
+ if (mNextPTSTimeUs >= 0ll) {
+ sp<AMessage> extra = new AMessage;
+ extra->setInt64(IStreamListener::kKeyMediaTimeUs, mNextPTSTimeUs);
+
+ mTSParser->signalDiscontinuity(
+ ATSParser::DISCONTINUITY_SEEK, extra);
+
+ mNextPTSTimeUs = -1ll;
+ }
+
+ size_t offset = 0;
+ while (offset < buffer->size()) {
+ status_t err = mTSParser->feedTSPacket(buffer->data() + offset, 188);
+
+ if (err != OK) {
+ return err;
+ }
+
+ offset += 188;
+ }
+
+ for (size_t i = mPacketSources.size(); i-- > 0;) {
+ sp<AnotherPacketSource> packetSource = mPacketSources.valueAt(i);
+
+ ATSParser::SourceType type;
+ switch (mPacketSources.keyAt(i)) {
+ case LiveSession::STREAMTYPE_VIDEO:
+ type = ATSParser::VIDEO;
+ break;
+
+ case LiveSession::STREAMTYPE_AUDIO:
+ type = ATSParser::AUDIO;
+ break;
+
+ case LiveSession::STREAMTYPE_SUBTITLES:
+ {
+ ALOGE("MPEG2 Transport streams do not contain subtitles.");
+ return ERROR_MALFORMED;
+ break;
+ }
+
+ default:
+ TRESPASS();
+ }
+
+ sp<AnotherPacketSource> source =
+ static_cast<AnotherPacketSource *>(
+ mTSParser->getSource(type).get());
+
+ if (source == NULL) {
+ ALOGW("MPEG2 Transport stream does not contain %s data.",
+ type == ATSParser::VIDEO ? "video" : "audio");
+
+ mStreamTypeMask &= ~mPacketSources.keyAt(i);
+ mPacketSources.removeItemsAt(i);
+ continue;
+ }
+
+ sp<ABuffer> accessUnit;
+ status_t finalResult;
+ while (source->hasBufferAvailable(&finalResult)
+ && source->dequeueAccessUnit(&accessUnit) == OK) {
+ // Note that we do NOT dequeue any discontinuities.
+
+ packetSource->queueAccessUnit(accessUnit);
+ }
+
+ if (packetSource->getFormat() == NULL) {
+ packetSource->setFormat(source->getFormat());
+ }
+ }
+
+ return OK;
+ } else if (buffer->size() >= 7 && !memcmp("WEBVTT\n", buffer->data(), 7)) {
+ if (mStreamTypeMask != LiveSession::STREAMTYPE_SUBTITLES) {
+ ALOGE("This stream only contains subtitles.");
+ return ERROR_MALFORMED;
+ }
+
+ const sp<AnotherPacketSource> packetSource =
+ mPacketSources.valueFor(LiveSession::STREAMTYPE_SUBTITLES);
+
+ buffer->meta()->setInt64("timeUs", 0ll);
+
+ packetSource->queueAccessUnit(buffer);
+ return OK;
+ }
+
+ if (mNextPTSTimeUs >= 0ll) {
+ mFirstPTSValid = false;
+ mAbsoluteTimeAnchorUs = mNextPTSTimeUs;
+ mNextPTSTimeUs = -1ll;
+ }
+
+ // This better be an ISO 13818-7 (AAC) or ISO 13818-1 (MPEG) audio
+ // stream prefixed by an ID3 tag.
+
+ bool firstID3Tag = true;
+ uint64_t PTS = 0;
+
+ for (;;) {
+ // Make sure to skip all ID3 tags preceding the audio data.
+ // At least one must be present to provide the PTS timestamp.
+
+ ID3 id3(buffer->data(), buffer->size(), true /* ignoreV1 */);
+ if (!id3.isValid()) {
+ if (firstID3Tag) {
+ ALOGE("Unable to parse ID3 tag.");
+ return ERROR_MALFORMED;
+ } else {
+ break;
+ }
+ }
+
+ if (firstID3Tag) {
+ bool found = false;
+
+ ID3::Iterator it(id3, "PRIV");
+ while (!it.done()) {
+ size_t length;
+ const uint8_t *data = it.getData(&length);
+
+ static const char *kMatchName =
+ "com.apple.streaming.transportStreamTimestamp";
+ static const size_t kMatchNameLen = strlen(kMatchName);
+
+ if (length == kMatchNameLen + 1 + 8
+ && !strncmp((const char *)data, kMatchName, kMatchNameLen)) {
+ found = true;
+ PTS = U64_AT(&data[kMatchNameLen + 1]);
+ }
+
+ it.next();
+ }
+
+ if (!found) {
+ ALOGE("Unable to extract transportStreamTimestamp from ID3 tag.");
+ return ERROR_MALFORMED;
+ }
+ }
+
+ // skip the ID3 tag
+ buffer->setRange(
+ buffer->offset() + id3.rawSize(), buffer->size() - id3.rawSize());
+
+ firstID3Tag = false;
+ }
+
+ if (!mFirstPTSValid) {
+ mFirstPTSValid = true;
+ mFirstPTS = PTS;
+ }
+ PTS -= mFirstPTS;
+
+ int64_t timeUs = (PTS * 100ll) / 9ll + mAbsoluteTimeAnchorUs;
+
+ if (mStreamTypeMask != LiveSession::STREAMTYPE_AUDIO) {
+ ALOGW("This stream only contains audio data!");
+
+ mStreamTypeMask &= LiveSession::STREAMTYPE_AUDIO;
+
+ if (mStreamTypeMask == 0) {
+ return OK;
+ }
+ }
+
+ sp<AnotherPacketSource> packetSource =
+ mPacketSources.valueFor(LiveSession::STREAMTYPE_AUDIO);
+
+ if (packetSource->getFormat() == NULL && buffer->size() >= 7) {
+ ABitReader bits(buffer->data(), buffer->size());
+
+ // adts_fixed_header
+
+ CHECK_EQ(bits.getBits(12), 0xfffu);
+ bits.skipBits(3); // ID, layer
+ bool protection_absent = bits.getBits(1) != 0;
+
+ unsigned profile = bits.getBits(2);
+ CHECK_NE(profile, 3u);
+ unsigned sampling_freq_index = bits.getBits(4);
+ bits.getBits(1); // private_bit
+ unsigned channel_configuration = bits.getBits(3);
+ CHECK_NE(channel_configuration, 0u);
+ bits.skipBits(2); // original_copy, home
+
+ sp<MetaData> meta = MakeAACCodecSpecificData(
+ profile, sampling_freq_index, channel_configuration);
+
+ meta->setInt32(kKeyIsADTS, true);
+
+ packetSource->setFormat(meta);
+ }
+
+ int64_t numSamples = 0ll;
+ int32_t sampleRate;
+ CHECK(packetSource->getFormat()->findInt32(kKeySampleRate, &sampleRate));
+
+ size_t offset = 0;
+ while (offset < buffer->size()) {
+ const uint8_t *adtsHeader = buffer->data() + offset;
+ CHECK_LT(offset + 5, buffer->size());
+
+ unsigned aac_frame_length =
+ ((adtsHeader[3] & 3) << 11)
+ | (adtsHeader[4] << 3)
+ | (adtsHeader[5] >> 5);
+
+ CHECK_LE(offset + aac_frame_length, buffer->size());
+
+ sp<ABuffer> unit = new ABuffer(aac_frame_length);
+ memcpy(unit->data(), adtsHeader, aac_frame_length);
+
+ int64_t unitTimeUs = timeUs + numSamples * 1000000ll / sampleRate;
+ unit->meta()->setInt64("timeUs", unitTimeUs);
+
+ // Each AAC frame encodes 1024 samples.
+ numSamples += 1024;
+
+ packetSource->queueAccessUnit(unit);
+
+ offset += aac_frame_length;
+ }
+
+ return OK;
+}
+
+void PlaylistFetcher::updateDuration() {
+ int64_t durationUs = 0ll;
+ for (size_t index = 0; index < mPlaylist->size(); ++index) {
+ sp<AMessage> itemMeta;
+ CHECK(mPlaylist->itemAt(
+ index, NULL /* uri */, &itemMeta));
+
+ int64_t itemDurationUs;
+ CHECK(itemMeta->findInt64("durationUs", &itemDurationUs));
+
+ durationUs += itemDurationUs;
+ }
+
+ sp<AMessage> msg = mNotify->dup();
+ msg->setInt32("what", kWhatDurationUpdate);
+ msg->setInt64("durationUs", durationUs);
+ msg->post();
+}
+
+} // namespace android
diff --git a/media/libstagefright/httplive/PlaylistFetcher.h b/media/libstagefright/httplive/PlaylistFetcher.h
new file mode 100644
index 0000000..5a2b901
--- /dev/null
+++ b/media/libstagefright/httplive/PlaylistFetcher.h
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef PLAYLIST_FETCHER_H_
+
+#define PLAYLIST_FETCHER_H_
+
+#include <media/stagefright/foundation/AHandler.h>
+
+#include "mpeg2ts/ATSParser.h"
+#include "LiveSession.h"
+
+namespace android {
+
+struct ABuffer;
+struct AnotherPacketSource;
+struct DataSource;
+struct HTTPBase;
+struct LiveDataSource;
+struct M3UParser;
+struct String8;
+
+struct PlaylistFetcher : public AHandler {
+ enum {
+ kWhatStarted,
+ kWhatPaused,
+ kWhatStopped,
+ kWhatError,
+ kWhatDurationUpdate,
+ kWhatTemporarilyDoneFetching,
+ kWhatPrepared,
+ kWhatPreparationFailed,
+ };
+
+ PlaylistFetcher(
+ const sp<AMessage> &notify,
+ const sp<LiveSession> &session,
+ const char *uri);
+
+ sp<DataSource> getDataSource();
+
+ void startAsync(
+ const sp<AnotherPacketSource> &audioSource,
+ const sp<AnotherPacketSource> &videoSource,
+ const sp<AnotherPacketSource> &subtitleSource,
+ int64_t startTimeUs = -1ll);
+
+ void pauseAsync();
+
+ void stopAsync();
+
+protected:
+ virtual ~PlaylistFetcher();
+ virtual void onMessageReceived(const sp<AMessage> &msg);
+
+private:
+ enum {
+ kMaxNumRetries = 5,
+ };
+
+ enum {
+ kWhatStart = 'strt',
+ kWhatPause = 'paus',
+ kWhatStop = 'stop',
+ kWhatMonitorQueue = 'moni',
+ };
+
+ static const int64_t kMinBufferedDurationUs;
+
+ sp<AMessage> mNotify;
+ sp<LiveSession> mSession;
+ AString mURI;
+
+ uint32_t mStreamTypeMask;
+ int64_t mStartTimeUs;
+
+ KeyedVector<LiveSession::StreamType, sp<AnotherPacketSource> >
+ mPacketSources;
+
+ KeyedVector<AString, sp<ABuffer> > mAESKeyForURI;
+
+ int64_t mLastPlaylistFetchTimeUs;
+ sp<M3UParser> mPlaylist;
+ int32_t mSeqNumber;
+ int32_t mNumRetries;
+ bool mStartup;
+ int64_t mNextPTSTimeUs;
+
+ int32_t mMonitorQueueGeneration;
+
+ enum RefreshState {
+ INITIAL_MINIMUM_RELOAD_DELAY,
+ FIRST_UNCHANGED_RELOAD_ATTEMPT,
+ SECOND_UNCHANGED_RELOAD_ATTEMPT,
+ THIRD_UNCHANGED_RELOAD_ATTEMPT
+ };
+ RefreshState mRefreshState;
+
+ uint8_t mPlaylistHash[16];
+
+ sp<ATSParser> mTSParser;
+
+ bool mFirstPTSValid;
+ uint64_t mFirstPTS;
+ int64_t mAbsoluteTimeAnchorUs;
+
+ status_t decryptBuffer(
+ size_t playlistIndex, const sp<ABuffer> &buffer);
+
+ void postMonitorQueue(int64_t delayUs = 0);
+ void cancelMonitorQueue();
+
+ bool timeToRefreshPlaylist(int64_t nowUs) const;
+
+ // Returns the media time in us of the segment specified by seqNumber.
+ // This is computed by summing the durations of all segments before it.
+ int64_t getSegmentStartTimeUs(int32_t seqNumber) const;
+
+ status_t onStart(const sp<AMessage> &msg);
+ void onPause();
+ void onStop();
+ void onMonitorQueue();
+ void onDownloadNext();
+
+ status_t extractAndQueueAccessUnits(const sp<ABuffer> &buffer);
+
+ void notifyError(status_t err);
+
+ void queueDiscontinuity(
+ ATSParser::DiscontinuityType type, const sp<AMessage> &extra);
+
+ int32_t getSeqNumberForTime(int64_t timeUs) const;
+
+ void updateDuration();
+
+ DISALLOW_EVIL_CONSTRUCTORS(PlaylistFetcher);
+};
+
+} // namespace android
+
+#endif // PLAYLIST_FETCHER_H_
+