blob: f9d27ebc0e0d8e2d6307f133cb2ed2d68f088da4 [file] [log] [blame]
/*
* 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.
*/
//#define LOG_NDEBUG 0
#define LOG_TAG "LiveSource"
#include <utils/Log.h>
#include "include/LiveSource.h"
#include "include/M3UParser.h"
#include "include/NuHTTPDataSource.h"
#include <cutils/properties.h>
#include <media/stagefright/foundation/hexdump.h>
#include <media/stagefright/foundation/ABuffer.h>
#include <media/stagefright/foundation/ADebug.h>
#include <media/stagefright/FileSource.h>
#include <ctype.h>
#include <openssl/aes.h>
namespace android {
LiveSource::LiveSource(const char *url)
: mMasterURL(url),
mInitCheck(NO_INIT),
mDurationUs(-1),
mPlaylistIndex(0),
mLastFetchTimeUs(-1),
mSource(new NuHTTPDataSource),
mSourceSize(0),
mOffsetBias(0),
mSignalDiscontinuity(false),
mPrevBandwidthIndex(-1),
mAESKey((AES_KEY *)malloc(sizeof(AES_KEY))),
mStreamEncrypted(false) {
if (switchToNext()) {
mInitCheck = OK;
determineSeekability();
}
}
LiveSource::~LiveSource() {
free(mAESKey);
mAESKey = NULL;
}
status_t LiveSource::initCheck() const {
return mInitCheck;
}
// static
int LiveSource::SortByBandwidth(const BandwidthItem *a, const BandwidthItem *b) {
if (a->mBandwidth < b->mBandwidth) {
return -1;
} else if (a->mBandwidth == b->mBandwidth) {
return 0;
}
return 1;
}
static double uniformRand() {
return (double)rand() / RAND_MAX;
}
size_t LiveSource::getBandwidthIndex() {
if (mBandwidthItems.size() == 0) {
return 0;
}
#if 1
int32_t bandwidthBps;
if (mSource != NULL && mSource->estimateBandwidth(&bandwidthBps)) {
LOGI("bandwidth estimated at %.2f kbps", bandwidthBps / 1024.0f);
} else {
LOGI("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)) {
char *end;
long maxBw = strtoul(value, &end, 10);
if (end > value && *end == '\0') {
if (maxBw > 0 && bandwidthBps > maxBw) {
LOGV("bandwidth capped to %ld bps", maxBw);
bandwidthBps = maxBw;
}
}
}
// Consider only 80% of the available bandwidth usable.
bandwidthBps = (bandwidthBps * 8) / 10;
// Pick the highest bandwidth stream below or equal to estimated bandwidth.
size_t index = mBandwidthItems.size() - 1;
while (index > 0 && mBandwidthItems.itemAt(index).mBandwidth
> (size_t)bandwidthBps) {
--index;
}
#elif 0
// Change bandwidth at random()
size_t index = uniformRand() * mBandwidthItems.size();
#elif 0
// There's a 50% chance to stay on the current bandwidth and
// a 50% chance to switch to the next higher bandwidth (wrapping around
// to lowest)
const size_t kMinIndex = 0;
size_t index;
if (mPrevBandwidthIndex < 0) {
index = kMinIndex;
} else if (uniformRand() < 0.5) {
index = (size_t)mPrevBandwidthIndex;
} else {
index = mPrevBandwidthIndex + 1;
if (index == mBandwidthItems.size()) {
index = kMinIndex;
}
}
#elif 0
// Pick the highest bandwidth stream below or equal to 1.2 Mbit/sec
size_t index = mBandwidthItems.size() - 1;
while (index > 0 && mBandwidthItems.itemAt(index).mBandwidth > 1200000) {
--index;
}
#else
size_t index = mBandwidthItems.size() - 1; // Highest bandwidth stream
#endif
return index;
}
bool LiveSource::loadPlaylist(bool fetchMaster, size_t bandwidthIndex) {
mSignalDiscontinuity = false;
mPlaylist.clear();
mPlaylistIndex = 0;
if (fetchMaster) {
mPrevBandwidthIndex = -1;
sp<ABuffer> buffer;
status_t err = fetchM3U(mMasterURL.c_str(), &buffer);
if (err != OK) {
return false;
}
mPlaylist = new M3UParser(
mMasterURL.c_str(), buffer->data(), buffer->size());
if (mPlaylist->initCheck() != OK) {
return false;
}
if (mPlaylist->isVariantPlaylist()) {
for (size_t i = 0; i < mPlaylist->size(); ++i) {
BandwidthItem item;
sp<AMessage> meta;
mPlaylist->itemAt(i, &item.mURI, &meta);
unsigned long bandwidth;
CHECK(meta->findInt32("bandwidth", (int32_t *)&item.mBandwidth));
mBandwidthItems.push(item);
}
mPlaylist.clear();
// fall through
if (mBandwidthItems.size() == 0) {
return false;
}
mBandwidthItems.sort(SortByBandwidth);
#if 1 // XXX
if (mBandwidthItems.size() > 1) {
// Remove the lowest bandwidth stream, this is sometimes
// an AAC program stream, which we don't support at this point.
mBandwidthItems.removeItemsAt(0);
}
#endif
for (size_t i = 0; i < mBandwidthItems.size(); ++i) {
const BandwidthItem &item = mBandwidthItems.itemAt(i);
LOGV("item #%d: %s", i, item.mURI.c_str());
}
bandwidthIndex = getBandwidthIndex();
}
}
if (mBandwidthItems.size() > 0) {
mURL = mBandwidthItems.editItemAt(bandwidthIndex).mURI;
if (mPrevBandwidthIndex >= 0
&& (size_t)mPrevBandwidthIndex != bandwidthIndex) {
// If we switched streams because of bandwidth changes,
// we'll signal this discontinuity by inserting a
// special transport stream packet into the stream.
mSignalDiscontinuity = true;
}
mPrevBandwidthIndex = bandwidthIndex;
} else {
mURL = mMasterURL;
}
if (mPlaylist == NULL) {
sp<ABuffer> buffer;
status_t err = fetchM3U(mURL.c_str(), &buffer);
if (err != OK) {
return false;
}
mPlaylist = new M3UParser(mURL.c_str(), buffer->data(), buffer->size());
if (mPlaylist->initCheck() != OK) {
return false;
}
if (mPlaylist->isVariantPlaylist()) {
return false;
}
}
if (!mPlaylist->meta()->findInt32(
"media-sequence", &mFirstItemSequenceNumber)) {
mFirstItemSequenceNumber = 0;
}
return true;
}
static int64_t getNowUs() {
struct timeval tv;
gettimeofday(&tv, NULL);
return (int64_t)tv.tv_usec + tv.tv_sec * 1000000ll;
}
bool LiveSource::switchToNext() {
mSignalDiscontinuity = false;
mOffsetBias += mSourceSize;
mSourceSize = 0;
size_t bandwidthIndex = getBandwidthIndex();
if (mLastFetchTimeUs < 0 || getNowUs() >= mLastFetchTimeUs + 15000000ll
|| mPlaylistIndex == mPlaylist->size()
|| (ssize_t)bandwidthIndex != mPrevBandwidthIndex) {
int32_t nextSequenceNumber =
mPlaylistIndex + mFirstItemSequenceNumber;
if (!loadPlaylist(mLastFetchTimeUs < 0, bandwidthIndex)) {
LOGE("failed to reload playlist");
return false;
}
if (mLastFetchTimeUs < 0) {
if (isSeekable()) {
mPlaylistIndex = 0;
} else {
// This is live streamed content, the first seqnum in the
// various bandwidth' streams may be slightly off, so don't
// start at the very first entry.
// With a segment duration of 6-10secs, this really only
// delays playback up to 30secs compared to real time.
mPlaylistIndex = 3;
if (mPlaylistIndex >= mPlaylist->size()) {
mPlaylistIndex = mPlaylist->size() - 1;
}
}
} else {
if (nextSequenceNumber < mFirstItemSequenceNumber
|| nextSequenceNumber
>= mFirstItemSequenceNumber + (int32_t)mPlaylist->size()) {
LOGE("Cannot find sequence number %d in new playlist",
nextSequenceNumber);
return false;
}
mPlaylistIndex = nextSequenceNumber - mFirstItemSequenceNumber;
}
mLastFetchTimeUs = getNowUs();
}
if (!setupCipher()) {
return false;
}
AString uri;
sp<AMessage> itemMeta;
CHECK(mPlaylist->itemAt(mPlaylistIndex, &uri, &itemMeta));
LOGV("switching to %s", uri.c_str());
if (mSource->connect(uri.c_str()) != OK
|| mSource->getSize(&mSourceSize) != OK) {
return false;
}
int32_t val;
if (itemMeta->findInt32("discontinuity", &val) && val != 0) {
mSignalDiscontinuity = true;
}
mPlaylistIndex++;
return true;
}
bool LiveSource::setupCipher() {
sp<AMessage> itemMeta;
bool found = false;
AString method;
for (ssize_t i = mPlaylistIndex; i >= 0; --i) {
AString uri;
CHECK(mPlaylist->itemAt(i, &uri, &itemMeta));
if (itemMeta->findString("cipher-method", &method)) {
found = true;
break;
}
}
if (!found) {
method = "NONE";
}
mStreamEncrypted = false;
if (method == "AES-128") {
AString keyURI;
if (!itemMeta->findString("cipher-uri", &keyURI)) {
LOGE("Missing key uri");
return false;
}
if (keyURI.size() >= 2
&& keyURI.c_str()[0] == '"'
&& keyURI.c_str()[keyURI.size() - 1] == '"') {
// Remove surrounding quotes.
AString tmp(keyURI, 1, keyURI.size() - 2);
keyURI = tmp;
}
ssize_t index = mAESKeyForURI.indexOfKey(keyURI);
sp<ABuffer> key;
if (index >= 0) {
key = mAESKeyForURI.valueAt(index);
} else {
key = new ABuffer(16);
sp<NuHTTPDataSource> keySource = new NuHTTPDataSource;
status_t err = keySource->connect(keyURI.c_str());
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;
}
offset += n;
}
}
if (err != OK) {
LOGE("failed to fetch cipher key from '%s'.", keyURI.c_str());
return false;
}
mAESKeyForURI.add(keyURI, key);
}
if (AES_set_decrypt_key(key->data(), 128, (AES_KEY *)mAESKey) != 0) {
LOGE("failed to set AES decryption key.");
return false;
}
AString iv;
if (itemMeta->findString("cipher-iv", &iv)) {
if ((!iv.startsWith("0x") && !iv.startsWith("0X"))
|| iv.size() != 16 * 2 + 2) {
LOGE("malformed cipher IV '%s'.", iv.c_str());
return false;
}
memset(mAESIVec, 0, sizeof(mAESIVec));
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)) {
LOGE("malformed cipher IV '%s'.", iv.c_str());
return false;
}
uint8_t nibble1 = isdigit(c1) ? c1 - '0' : c1 - 'a' + 10;
uint8_t nibble2 = isdigit(c2) ? c2 - '0' : c2 - 'a' + 10;
mAESIVec[i] = nibble1 << 4 | nibble2;
}
} else {
size_t seqNum = mPlaylistIndex + mFirstItemSequenceNumber;
memset(mAESIVec, 0, sizeof(mAESIVec));
mAESIVec[15] = seqNum & 0xff;
mAESIVec[14] = (seqNum >> 8) & 0xff;
mAESIVec[13] = (seqNum >> 16) & 0xff;
mAESIVec[12] = (seqNum >> 24) & 0xff;
}
mStreamEncrypted = true;
} else if (!(method == "NONE")) {
LOGE("Unsupported cipher method '%s'", method.c_str());
return false;
}
return true;
}
static const ssize_t kHeaderSize = 188;
ssize_t LiveSource::readAt(off_t offset, void *data, size_t size) {
CHECK(offset >= mOffsetBias);
offset -= mOffsetBias;
off_t delta = mSignalDiscontinuity ? kHeaderSize : 0;
if (offset >= mSourceSize + delta) {
CHECK_EQ(offset, mSourceSize + delta);
offset -= mSourceSize + delta;
if (!switchToNext()) {
return ERROR_END_OF_STREAM;
}
if (mSignalDiscontinuity) {
LOGV("switchToNext changed streams");
} else {
LOGV("switchToNext stayed within the same stream");
}
mOffsetBias += delta;
delta = mSignalDiscontinuity ? kHeaderSize : 0;
}
if (offset < delta) {
size_t avail = delta - offset;
memset(data, 0, avail);
return avail;
}
bool done = false;
size_t numRead = 0;
while (numRead < size) {
ssize_t n = mSource->readAt(
offset + numRead - delta,
(uint8_t *)data + numRead, size - numRead);
if (n <= 0) {
break;
}
if (mStreamEncrypted) {
size_t nmod = n % 16;
CHECK(nmod == 0);
sp<ABuffer> tmp = new ABuffer(n);
AES_cbc_encrypt((const unsigned char *)data + numRead,
tmp->data(),
n,
(const AES_KEY *)mAESKey,
mAESIVec,
AES_DECRYPT);
if (mSourceSize == (off_t)(offset + numRead - delta + n)) {
// check for padding at the end of the file.
size_t pad = tmp->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)tmp->data()[n - 1 - i], pad);
}
n -= pad;
mSourceSize -= pad;
done = true;
}
memcpy((uint8_t *)data + numRead, tmp->data(), n);
}
numRead += n;
if (done) {
break;
}
}
return numRead;
}
status_t LiveSource::fetchM3U(const char *url, sp<ABuffer> *out) {
*out = NULL;
sp<DataSource> source;
if (!strncasecmp(url, "file://", 7)) {
source = new FileSource(url + 7);
} else {
CHECK(!strncasecmp(url, "http://", 7));
status_t err = mSource->connect(url);
if (err != OK) {
return err;
}
source = mSource;
}
off_t size;
status_t err = source->getSize(&size);
if (err != OK) {
return err;
}
sp<ABuffer> buffer = new ABuffer(size);
size_t offset = 0;
while (offset < (size_t)size) {
ssize_t n = source->readAt(
offset, buffer->data() + offset, size - offset);
if (n <= 0) {
return ERROR_IO;
}
offset += n;
}
*out = buffer;
return OK;
}
bool LiveSource::seekTo(int64_t seekTimeUs) {
LOGV("seek to %lld us", seekTimeUs);
if (!mPlaylist->isComplete()) {
return false;
}
int32_t targetDuration;
if (!mPlaylist->meta()->findInt32("target-duration", &targetDuration)) {
return false;
}
int64_t seekTimeSecs = (seekTimeUs + 500000ll) / 1000000ll;
int64_t index = seekTimeSecs / targetDuration;
if (index < 0 || index >= mPlaylist->size()) {
return false;
}
if (index == mPlaylistIndex) {
return false;
}
mPlaylistIndex = index;
LOGV("seeking to index %lld", index);
switchToNext();
mOffsetBias = 0;
return true;
}
bool LiveSource::getDuration(int64_t *durationUs) const {
if (mDurationUs >= 0) {
*durationUs = mDurationUs;
return true;
}
*durationUs = 0;
return false;
}
bool LiveSource::isSeekable() const {
return mDurationUs >= 0;
}
void LiveSource::determineSeekability() {
mDurationUs = -1;
if (!mPlaylist->isComplete()) {
return;
}
int32_t targetDuration;
if (!mPlaylist->meta()->findInt32("target-duration", &targetDuration)) {
return;
}
mDurationUs = targetDuration * 1000000ll * mPlaylist->size();
}
} // namespace android