| //#define LOG_NDEBUG 0 |
| #define LOG_TAG "NuHTTPDataSource" |
| #include <utils/Log.h> |
| |
| #include "include/NuHTTPDataSource.h" |
| |
| #include <cutils/properties.h> |
| #include <media/stagefright/foundation/ALooper.h> |
| #include <media/stagefright/MediaDebug.h> |
| #include <media/stagefright/MediaErrors.h> |
| |
| namespace android { |
| |
| static bool ParseSingleUnsignedLong( |
| const char *from, unsigned long *x) { |
| char *end; |
| *x = strtoul(from, &end, 10); |
| |
| if (end == from || *end != '\0') { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| static bool ParseURL( |
| const char *url, String8 *host, unsigned *port, String8 *path) { |
| host->setTo(""); |
| *port = 0; |
| path->setTo(""); |
| |
| if (strncasecmp("http://", url, 7)) { |
| return false; |
| } |
| |
| const char *slashPos = strchr(&url[7], '/'); |
| |
| if (slashPos == NULL) { |
| host->setTo(&url[7]); |
| path->setTo("/"); |
| } else { |
| host->setTo(&url[7], slashPos - &url[7]); |
| path->setTo(slashPos); |
| } |
| |
| const char *colonPos = strchr(host->string(), ':'); |
| |
| if (colonPos != NULL) { |
| unsigned long x; |
| if (!ParseSingleUnsignedLong(colonPos + 1, &x) || x >= 65536) { |
| return false; |
| } |
| |
| *port = x; |
| |
| size_t colonOffset = colonPos - host->string(); |
| String8 tmp(host->string(), colonOffset); |
| *host = tmp; |
| } else { |
| *port = 80; |
| } |
| |
| return true; |
| } |
| |
| NuHTTPDataSource::NuHTTPDataSource() |
| : mState(DISCONNECTED), |
| mPort(0), |
| mOffset(0), |
| mContentLength(0), |
| mContentLengthValid(false), |
| mHasChunkedTransferEncoding(false), |
| mChunkDataBytesLeft(0), |
| mNumBandwidthHistoryItems(0), |
| mTotalTransferTimeUs(0), |
| mTotalTransferBytes(0), |
| mDecryptHandle(NULL), |
| mDrmManagerClient(NULL) { |
| } |
| |
| NuHTTPDataSource::~NuHTTPDataSource() { |
| if (mDecryptHandle != NULL) { |
| // To release mDecryptHandle |
| mDrmManagerClient->closeDecryptSession(mDecryptHandle); |
| mDecryptHandle = NULL; |
| } |
| |
| if (mDrmManagerClient != NULL) { |
| delete mDrmManagerClient; |
| mDrmManagerClient = NULL; |
| } |
| } |
| |
| status_t NuHTTPDataSource::connect( |
| const char *uri, |
| const KeyedVector<String8, String8> *overrides, |
| off64_t offset) { |
| String8 headers; |
| MakeFullHeaders(overrides, &headers); |
| |
| return connect(uri, headers, offset); |
| } |
| |
| status_t NuHTTPDataSource::connect( |
| const char *uri, |
| const String8 &headers, |
| off64_t offset) { |
| String8 host, path; |
| unsigned port; |
| |
| mUri = uri; |
| |
| if (!ParseURL(uri, &host, &port, &path)) { |
| return ERROR_MALFORMED; |
| } |
| |
| return connect(host, port, path, headers, offset); |
| } |
| |
| static bool IsRedirectStatusCode(int httpStatus) { |
| return httpStatus == 301 || httpStatus == 302 |
| || httpStatus == 303 || httpStatus == 307; |
| } |
| |
| status_t NuHTTPDataSource::connect( |
| const char *host, unsigned port, const char *path, |
| const String8 &headers, |
| off64_t offset) { |
| LOGI("connect to %s:%u%s @%lld", host, port, path, offset); |
| |
| bool needsToReconnect = true; |
| |
| if (mState == CONNECTED && host == mHost && port == mPort |
| && offset == mOffset) { |
| if (mContentLengthValid && mOffset == mContentLength) { |
| LOGI("Didn't have to reconnect, old one's still good."); |
| needsToReconnect = false; |
| } |
| } |
| |
| mHost = host; |
| mPort = port; |
| mPath = path; |
| mHeaders = headers; |
| |
| status_t err = OK; |
| |
| mState = CONNECTING; |
| |
| if (needsToReconnect) { |
| mHTTP.disconnect(); |
| err = mHTTP.connect(host, port); |
| } |
| |
| if (err != OK) { |
| mState = DISCONNECTED; |
| } else if (mState != CONNECTING) { |
| err = UNKNOWN_ERROR; |
| } else { |
| mState = CONNECTED; |
| |
| mOffset = offset; |
| mContentLength = 0; |
| mContentLengthValid = false; |
| |
| String8 request("GET "); |
| request.append(mPath); |
| request.append(" HTTP/1.1\r\n"); |
| request.append("Host: "); |
| request.append(mHost); |
| if (mPort != 80) { |
| request.append(StringPrintf(":%u", mPort).c_str()); |
| } |
| request.append("\r\n"); |
| |
| if (offset != 0) { |
| char rangeHeader[128]; |
| sprintf(rangeHeader, "Range: bytes=%lld-\r\n", offset); |
| request.append(rangeHeader); |
| } |
| |
| request.append(mHeaders); |
| request.append("\r\n"); |
| |
| int httpStatus; |
| if ((err = mHTTP.send(request.string(), request.size())) != OK |
| || (err = mHTTP.receive_header(&httpStatus)) != OK) { |
| mHTTP.disconnect(); |
| mState = DISCONNECTED; |
| return err; |
| } |
| |
| if (IsRedirectStatusCode(httpStatus)) { |
| AString value; |
| CHECK(mHTTP.find_header_value("Location", &value)); |
| |
| mState = DISCONNECTED; |
| |
| mHTTP.disconnect(); |
| |
| return connect(value.c_str(), headers, offset); |
| } |
| |
| if (httpStatus < 200 || httpStatus >= 300) { |
| mState = DISCONNECTED; |
| mHTTP.disconnect(); |
| |
| return ERROR_IO; |
| } |
| |
| mHasChunkedTransferEncoding = false; |
| |
| { |
| AString value; |
| if (mHTTP.find_header_value("Transfer-Encoding", &value)) { |
| // We don't currently support any transfer encodings but |
| // chunked. |
| |
| if (!strcasecmp(value.c_str(), "chunked")) { |
| LOGI("Chunked transfer encoding applied."); |
| mHasChunkedTransferEncoding = true; |
| mChunkDataBytesLeft = 0; |
| } else { |
| mState = DISCONNECTED; |
| mHTTP.disconnect(); |
| |
| LOGE("We don't support '%s' transfer encoding.", value.c_str()); |
| |
| return ERROR_UNSUPPORTED; |
| } |
| } |
| } |
| |
| applyTimeoutResponse(); |
| |
| if (offset == 0) { |
| AString value; |
| unsigned long x; |
| if (mHTTP.find_header_value(AString("Content-Length"), &value) |
| && ParseSingleUnsignedLong(value.c_str(), &x)) { |
| mContentLength = (off64_t)x; |
| mContentLengthValid = true; |
| } else { |
| LOGW("Server did not give us the content length!"); |
| } |
| } else { |
| if (httpStatus != 206 /* Partial Content */) { |
| // We requested a range but the server didn't support that. |
| LOGE("We requested a range but the server didn't " |
| "support that."); |
| return ERROR_UNSUPPORTED; |
| } |
| |
| AString value; |
| unsigned long x; |
| if (mHTTP.find_header_value(AString("Content-Range"), &value)) { |
| const char *slashPos = strchr(value.c_str(), '/'); |
| if (slashPos != NULL |
| && ParseSingleUnsignedLong(slashPos + 1, &x)) { |
| mContentLength = x; |
| mContentLengthValid = true; |
| } |
| } |
| } |
| } |
| |
| return err; |
| } |
| |
| void NuHTTPDataSource::disconnect() { |
| if (mState == CONNECTING || mState == CONNECTED) { |
| mHTTP.disconnect(); |
| } |
| mState = DISCONNECTED; |
| } |
| |
| status_t NuHTTPDataSource::initCheck() const { |
| return mState == CONNECTED ? OK : NO_INIT; |
| } |
| |
| ssize_t NuHTTPDataSource::internalRead(void *data, size_t size) { |
| if (!mHasChunkedTransferEncoding) { |
| return mHTTP.receive(data, size); |
| } |
| |
| if (mChunkDataBytesLeft < 0) { |
| return 0; |
| } else if (mChunkDataBytesLeft == 0) { |
| char line[1024]; |
| status_t err = mHTTP.receive_line(line, sizeof(line)); |
| |
| if (err != OK) { |
| return err; |
| } |
| |
| LOGV("line = '%s'", line); |
| |
| char *end; |
| unsigned long n = strtoul(line, &end, 16); |
| |
| if (end == line || (*end != ';' && *end != '\0')) { |
| LOGE("malformed HTTP chunk '%s'", line); |
| return ERROR_MALFORMED; |
| } |
| |
| mChunkDataBytesLeft = n; |
| LOGV("chunk data size = %lu", n); |
| |
| if (mChunkDataBytesLeft == 0) { |
| mChunkDataBytesLeft = -1; |
| return 0; |
| } |
| |
| // fall through |
| } |
| |
| if (size > (size_t)mChunkDataBytesLeft) { |
| size = mChunkDataBytesLeft; |
| } |
| |
| ssize_t n = mHTTP.receive(data, size); |
| |
| if (n < 0) { |
| return n; |
| } |
| |
| mChunkDataBytesLeft -= (size_t)n; |
| |
| if (mChunkDataBytesLeft == 0) { |
| char line[1024]; |
| status_t err = mHTTP.receive_line(line, sizeof(line)); |
| |
| if (err != OK) { |
| return err; |
| } |
| |
| if (line[0] != '\0') { |
| LOGE("missing HTTP chunk terminator."); |
| return ERROR_MALFORMED; |
| } |
| } |
| |
| return n; |
| } |
| |
| ssize_t NuHTTPDataSource::readAt(off64_t offset, void *data, size_t size) { |
| LOGV("readAt offset %ld, size %d", offset, size); |
| |
| Mutex::Autolock autoLock(mLock); |
| |
| if (offset != mOffset) { |
| String8 host = mHost; |
| String8 path = mPath; |
| String8 headers = mHeaders; |
| status_t err = connect(host, mPort, path, headers, offset); |
| |
| if (err != OK) { |
| return err; |
| } |
| } |
| |
| if (mContentLengthValid) { |
| size_t avail = |
| (offset >= mContentLength) ? 0 : mContentLength - offset; |
| |
| if (size > avail) { |
| size = avail; |
| } |
| } |
| |
| size_t numBytesRead = 0; |
| while (numBytesRead < size) { |
| int64_t startTimeUs = ALooper::GetNowUs(); |
| |
| ssize_t n = |
| internalRead((uint8_t *)data + numBytesRead, size - numBytesRead); |
| |
| if (n < 0) { |
| return n; |
| } |
| |
| int64_t delayUs = ALooper::GetNowUs() - startTimeUs; |
| addBandwidthMeasurement_l(n, delayUs); |
| |
| numBytesRead += (size_t)n; |
| |
| if (n == 0) { |
| if (mContentLengthValid) { |
| // We know the content length and made sure not to read beyond |
| // it and yet the server closed the connection on us. |
| return ERROR_IO; |
| } |
| |
| break; |
| } |
| } |
| |
| mOffset += numBytesRead; |
| |
| return numBytesRead; |
| } |
| |
| status_t NuHTTPDataSource::getSize(off64_t *size) { |
| *size = 0; |
| |
| if (mState != CONNECTED) { |
| return ERROR_IO; |
| } |
| |
| if (mContentLengthValid) { |
| *size = mContentLength; |
| return OK; |
| } |
| |
| return ERROR_UNSUPPORTED; |
| } |
| |
| uint32_t NuHTTPDataSource::flags() { |
| return kWantsPrefetching; |
| } |
| |
| // static |
| void NuHTTPDataSource::MakeFullHeaders( |
| const KeyedVector<String8, String8> *overrides, String8 *headers) { |
| headers->setTo(""); |
| |
| headers->append("User-Agent: stagefright/1.1 (Linux;Android "); |
| |
| #if (PROPERTY_VALUE_MAX < 8) |
| #error "PROPERTY_VALUE_MAX must be at least 8" |
| #endif |
| |
| char value[PROPERTY_VALUE_MAX]; |
| property_get("ro.build.version.release", value, "Unknown"); |
| headers->append(value); |
| headers->append(")\r\n"); |
| |
| if (overrides == NULL) { |
| return; |
| } |
| |
| for (size_t i = 0; i < overrides->size(); ++i) { |
| String8 line; |
| line.append(overrides->keyAt(i)); |
| line.append(": "); |
| line.append(overrides->valueAt(i)); |
| line.append("\r\n"); |
| |
| headers->append(line); |
| } |
| } |
| |
| void NuHTTPDataSource::applyTimeoutResponse() { |
| AString timeout; |
| if (mHTTP.find_header_value("X-SocketTimeout", &timeout)) { |
| const char *s = timeout.c_str(); |
| char *end; |
| long tmp = strtol(s, &end, 10); |
| if (end == s || *end != '\0') { |
| LOGW("Illegal X-SocketTimeout value given."); |
| return; |
| } |
| |
| LOGI("overriding default timeout, new timeout is %ld seconds", tmp); |
| mHTTP.setReceiveTimeout(tmp); |
| } |
| } |
| |
| bool NuHTTPDataSource::estimateBandwidth(int32_t *bandwidth_bps) { |
| Mutex::Autolock autoLock(mLock); |
| |
| if (mNumBandwidthHistoryItems < 2) { |
| return false; |
| } |
| |
| *bandwidth_bps = ((double)mTotalTransferBytes * 8E6 / mTotalTransferTimeUs); |
| |
| return true; |
| } |
| |
| void NuHTTPDataSource::addBandwidthMeasurement_l( |
| size_t numBytes, int64_t delayUs) { |
| BandwidthEntry entry; |
| entry.mDelayUs = delayUs; |
| entry.mNumBytes = numBytes; |
| mTotalTransferTimeUs += delayUs; |
| mTotalTransferBytes += numBytes; |
| |
| mBandwidthHistory.push_back(entry); |
| if (++mNumBandwidthHistoryItems > 100) { |
| BandwidthEntry *entry = &*mBandwidthHistory.begin(); |
| mTotalTransferTimeUs -= entry->mDelayUs; |
| mTotalTransferBytes -= entry->mNumBytes; |
| mBandwidthHistory.erase(mBandwidthHistory.begin()); |
| --mNumBandwidthHistoryItems; |
| } |
| } |
| |
| DecryptHandle* NuHTTPDataSource::DrmInitialization() { |
| if (mDrmManagerClient == NULL) { |
| mDrmManagerClient = new DrmManagerClient(); |
| } |
| |
| if (mDrmManagerClient == NULL) { |
| return NULL; |
| } |
| |
| if (mDecryptHandle == NULL) { |
| /* Note if redirect occurs, mUri is the redirect uri instead of the |
| * original one |
| */ |
| mDecryptHandle = mDrmManagerClient->openDecryptSession(mUri); |
| } |
| |
| if (mDecryptHandle == NULL) { |
| delete mDrmManagerClient; |
| mDrmManagerClient = NULL; |
| } |
| |
| return mDecryptHandle; |
| } |
| |
| void NuHTTPDataSource::getDrmInfo(DecryptHandle **handle, DrmManagerClient **client) { |
| *handle = mDecryptHandle; |
| |
| *client = mDrmManagerClient; |
| } |
| |
| String8 NuHTTPDataSource::getUri() { |
| return mUri; |
| } |
| |
| } // namespace android |