Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6a1f5f90 authored by Andreas Huber's avatar Andreas Huber
Browse files

More HTTP live support, AES encryption etc.

Change-Id: Ia5088042dd0a2181cb73cf8c7a2ff81e34b3064c
related-to-bug: 2368598
parent 98d50a0b
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -68,7 +68,8 @@ LOCAL_SHARED_LIBRARIES := \
        libsurfaceflinger_client \
        libstagefright_yuv \
        libcamera_client \
        libdrmframework
        libdrmframework  \
        libcrypto

LOCAL_STATIC_LIBRARIES := \
        libstagefright_aacdec \
+14 −2
Original line number Diff line number Diff line
@@ -49,6 +49,8 @@

#include <media/stagefright/foundation/ALooper.h>

#define USE_SURFACE_ALLOC 1

namespace android {

static int64_t kLowWaterMarkUs = 2000000ll;  // 2secs
@@ -294,6 +296,16 @@ status_t AwesomePlayer::setDataSource_l(

    mUri = uri;

    if (!strncmp("http://", uri, 7)) {
        // Hack to support http live.

        size_t len = strlen(uri);
        if (!strcasecmp(&uri[len - 5], ".m3u8")) {
            mUri = "httplive://";
            mUri.append(&uri[7]);
        }
    }

    if (headers) {
        mUriHeaders = *headers;
    }
@@ -873,7 +885,7 @@ void AwesomePlayer::initRenderer_l() {
        IPCThreadState::self()->flushCommands();

        if (mSurface != NULL) {
            if (strncmp(component, "OMX.", 4) == 0) {
            if (USE_SURFACE_ALLOC && strncmp(component, "OMX.", 4) == 0) {
                // Hardware decoders avoid the CPU color conversion by decoding
                // directly to ANativeBuffers, so we must use a renderer that
                // just pushes those buffers to the ANativeWindow.
@@ -1143,7 +1155,7 @@ status_t AwesomePlayer::initVideoDecoder(uint32_t flags) {
            mClient.interface(), mVideoTrack->getFormat(),
            false, // createEncoder
            mVideoTrack,
            NULL, flags, mSurface);
            NULL, flags, USE_SURFACE_ALLOC ? mSurface : NULL);

    if (mVideoSource != NULL) {
        int64_t durationUs;
+2 −1
Original line number Diff line number Diff line
@@ -9,7 +9,8 @@ LOCAL_SRC_FILES:= \
LOCAL_C_INCLUDES:= \
	$(JNI_H_INCLUDE) \
	$(TOP)/frameworks/base/include/media/stagefright/openmax \
        $(TOP)/frameworks/base/media/libstagefright
        $(TOP)/frameworks/base/media/libstagefright \
        $(TOP)/external/openssl/include

LOCAL_MODULE:= libstagefright_httplive

+262 −39
Original line number Diff line number Diff line
@@ -22,9 +22,14 @@
#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 <media/stagefright/MediaDebug.h>

#include <ctype.h>
#include <openssl/aes.h>

namespace android {

@@ -38,7 +43,9 @@ LiveSource::LiveSource(const char *url)
      mSourceSize(0),
      mOffsetBias(0),
      mSignalDiscontinuity(false),
      mPrevBandwidthIndex(-1) {
      mPrevBandwidthIndex(-1),
      mAESKey((AES_KEY *)malloc(sizeof(AES_KEY))),
      mStreamEncrypted(false) {
    if (switchToNext()) {
        mInitCheck = OK;

@@ -47,6 +54,8 @@ LiveSource::LiveSource(const char *url)
}

LiveSource::~LiveSource() {
    free(mAESKey);
    mAESKey = NULL;
}

status_t LiveSource::initCheck() const {
@@ -68,7 +77,77 @@ static double uniformRand() {
    return (double)rand() / RAND_MAX;
}

bool LiveSource::loadPlaylist(bool fetchMaster) {
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();
@@ -112,49 +191,35 @@ bool LiveSource::loadPlaylist(bool fetchMaster) {

            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());
            }
        }
    }

    if (mBandwidthItems.size() > 0) {
#if 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)
        size_t index;
        if (uniformRand() < 0.5) {
            index = mPrevBandwidthIndex < 0 ? 0 : (size_t)mPrevBandwidthIndex;
        } else {
            if (mPrevBandwidthIndex < 0) {
                index = 0;
            } else {
                index = mPrevBandwidthIndex + 1;
                if (index == mBandwidthItems.size()) {
                    index = 0;
            bandwidthIndex = getBandwidthIndex();
        }
    }
        }
#else
        // Stay on the lowest bandwidth available.
        size_t index = mBandwidthItems.size() - 1;  // Highest bandwidth stream
#endif

        mURL = mBandwidthItems.editItemAt(index).mURI;
    if (mBandwidthItems.size() > 0) {
        mURL = mBandwidthItems.editItemAt(bandwidthIndex).mURI;

        if (mPrevBandwidthIndex >= 0 && (size_t)mPrevBandwidthIndex != index) {
        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 = index;
        mPrevBandwidthIndex = bandwidthIndex;
    } else {
        mURL = mMasterURL;
    }
@@ -199,12 +264,15 @@ bool LiveSource::switchToNext() {
    mOffsetBias += mSourceSize;
    mSourceSize = 0;

    size_t bandwidthIndex = getBandwidthIndex();

    if (mLastFetchTimeUs < 0 || getNowUs() >= mLastFetchTimeUs + 15000000ll
        || mPlaylistIndex == mPlaylist->size()) {
        || mPlaylistIndex == mPlaylist->size()
        || (ssize_t)bandwidthIndex != mPrevBandwidthIndex) {
        int32_t nextSequenceNumber =
            mPlaylistIndex + mFirstItemSequenceNumber;

        if (!loadPlaylist(mLastFetchTimeUs < 0)) {
        if (!loadPlaylist(mLastFetchTimeUs < 0, bandwidthIndex)) {
            LOGE("failed to reload playlist");
            return false;
        }
@@ -227,6 +295,10 @@ bool LiveSource::switchToNext() {
        mLastFetchTimeUs = getNowUs();
    }

    if (!setupCipher()) {
        return false;
    }

    AString uri;
    sp<AMessage> itemMeta;
    CHECK(mPlaylist->itemAt(mPlaylistIndex, &uri, &itemMeta));
@@ -243,6 +315,121 @@ bool LiveSource::switchToNext() {
    }

    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;
}

@@ -279,6 +466,7 @@ ssize_t LiveSource::readAt(off_t offset, void *data, size_t size) {
        return avail;
    }

    bool done = false;
    size_t numRead = 0;
    while (numRead < size) {
        ssize_t n = mSource->readAt(
@@ -289,7 +477,44 @@ ssize_t LiveSource::readAt(off_t offset, void *data, size_t size) {
            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;
@@ -359,19 +584,17 @@ bool LiveSource::seekTo(int64_t seekTimeUs) {
        return false;
    }

    size_t newPlaylistIndex = mFirstItemSequenceNumber + index;

    if (newPlaylistIndex == mPlaylistIndex) {
    if (index == mPlaylistIndex) {
        return false;
    }

    mPlaylistIndex = newPlaylistIndex;
    mPlaylistIndex = index;

    LOGV("seeking to index %lld", index);

    switchToNext();
    mOffsetBias = 0;

    LOGV("seeking to index %lld", index);

    return true;
}

+56 −0
Original line number Diff line number Diff line
@@ -158,6 +158,11 @@ status_t M3UParser::parse(const void *_data, size_t size) {
                    return ERROR_MALFORMED;
                }
                err = parseMetaData(line, &mMeta, "media-sequence");
            } else if (line.startsWith("#EXT-X-KEY")) {
                if (mIsVariantPlaylist) {
                    return ERROR_MALFORMED;
                }
                err = parseCipherInfo(line, &itemMeta);
            } else if (line.startsWith("#EXT-X-ENDLIST")) {
                mIsComplete = true;
            } else if (line.startsWith("#EXTINF")) {
@@ -291,6 +296,57 @@ status_t M3UParser::parseStreamInf(
    return OK;
}

// static
status_t M3UParser::parseCipherInfo(
        const AString &line, sp<AMessage> *meta) {
    ssize_t colonPos = line.find(":");

    if (colonPos < 0) {
        return ERROR_MALFORMED;
    }

    size_t offset = colonPos + 1;

    while (offset < line.size()) {
        ssize_t end = line.find(",", 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();

        LOGV("key=%s value=%s", key.c_str(), val.c_str());

        key.tolower();

        if (key == "method" || key == "uri" || key == "iv") {
            if (meta->get() == NULL) {
                *meta = new AMessage;
            }

            key.insert(AString("cipher-"), 0);

            (*meta)->setString(key.c_str(), val.c_str(), val.size());
        }
    }

    return OK;
}

// static
status_t M3UParser::ParseInt32(const char *s, int32_t *x) {
    char *end;
Loading