Bug 734546: Add full byte range request ability to ChannelMediaResource r=cpearce

This commit is contained in:
Steve Workman 2012-09-29 16:29:04 -07:00
parent 46f019d3df
commit c5ea79ed89
3 changed files with 277 additions and 6 deletions

View File

@ -30,6 +30,18 @@
#include "nsContentUtils.h"
#include "nsBlobProtocolHandler.h"
#ifdef PR_LOGGING
PRLogModuleInfo* gMediaResourceLog;
#define LOG(msg, ...) PR_LOG(gMediaResourceLog, PR_LOG_DEBUG, \
(msg, ##__VA_ARGS__))
// Debug logging macro with object pointer and class name.
#define CMLOG(msg, ...) \
LOG("%p [ChannelMediaResource]: " msg, this, ##__VA_ARGS__)
#else
#define LOG(msg, ...)
#define CMLOG(msg, ...)
#endif
static const uint32_t HTTP_OK_CODE = 200;
static const uint32_t HTTP_PARTIAL_RESPONSE_CODE = 206;
@ -43,8 +55,17 @@ ChannelMediaResource::ChannelMediaResource(nsMediaDecoder* aDecoder,
mCacheStream(this),
mLock("ChannelMediaResource.mLock"),
mIgnoreResume(false),
mSeekingForMetadata(false)
mSeekingForMetadata(false),
mByteRangeDownloads(false),
mByteRangeFirstOpen(true),
mSeekOffsetMonitor("media.dashseekmonitor"),
mSeekOffset(-1)
{
#ifdef PR_LOGGING
if (!gMediaResourceLog) {
gMediaResourceLog = PR_NewLogModule("MediaResource");
}
#endif
}
ChannelMediaResource::~ChannelMediaResource()
@ -201,10 +222,57 @@ ChannelMediaResource::OnStartRequest(nsIRequest* aRequest)
}
}
if (mOffset > 0 && responseStatus == HTTP_OK_CODE) {
// If we get an OK response but we were seeking, we have to assume
// that seeking doesn't work. We also need to tell the cache that
// it's getting data for the start of the stream.
// Check response code for byte-range requests (seeking, chunk requests).
if (!mByteRange.IsNull() && (responseStatus == HTTP_PARTIAL_RESPONSE_CODE)) {
// Byte range requests should get partial response codes and should
// accept ranges.
if (!acceptsRanges) {
CMLOG("Error! HTTP_PARTIAL_RESPONSE_CODE received but server says "
"range requests are not accepted! Channel[%p]", hc.get());
mDecoder->NetworkError();
CloseChannel();
return NS_OK;
}
// Parse Content-Range header.
int64_t rangeStart = 0;
int64_t rangeEnd = 0;
int64_t rangeTotal = 0;
rv = ParseContentRangeHeader(hc, rangeStart, rangeEnd, rangeTotal);
if (NS_FAILED(rv)) {
// Content-Range header text should be parse-able.
CMLOG("Error processing \'Content-Range' for "
"HTTP_PARTIAL_RESPONSE_CODE: rv[%x]channel [%p]", rv, hc.get());
mDecoder->NetworkError();
CloseChannel();
return NS_OK;
}
// Give some warnings if the ranges are unexpected.
// XXX These could be error conditions.
NS_WARN_IF_FALSE(mByteRange.mStart == rangeStart,
"response range start does not match request");
NS_WARN_IF_FALSE(mOffset == rangeStart,
"response range start does not match current offset");
NS_WARN_IF_FALSE(mByteRange.mEnd == rangeEnd,
"response range end does not match request");
// Notify media cache about the length and start offset of data received.
// Note: If aRangeTotal == -1, then the total bytes is unknown at this stage.
// For now, tell the decoder that the stream is infinite.
if (rangeTotal != -1) {
mCacheStream.NotifyDataLength(rangeTotal);
} else {
mDecoder->SetInfinite(true);
}
mCacheStream.NotifyDataStarted(rangeStart);
mOffset = rangeStart;
acceptsRanges = true;
} else if (((mOffset > 0) || !mByteRange.IsNull())
&& (responseStatus == HTTP_OK_CODE)) {
// If we get an OK response but we were seeking, or requesting a byte
// range, then we have to assume that seeking doesn't work. We also need
// to tell the cache that it's getting data for the start of the stream.
mCacheStream.NotifyDataStarted(0);
mOffset = 0;
@ -284,6 +352,53 @@ ChannelMediaResource::OnStartRequest(nsIRequest* aRequest)
return NS_OK;
}
nsresult
ChannelMediaResource::ParseContentRangeHeader(nsIHttpChannel * aHttpChan,
int64_t& aRangeStart,
int64_t& aRangeEnd,
int64_t& aRangeTotal)
{
NS_ENSURE_ARG(aHttpChan);
nsAutoCString rangeStr;
nsresult rv = aHttpChan->GetResponseHeader(NS_LITERAL_CSTRING("Content-Range"),
rangeStr);
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_FALSE(rangeStr.IsEmpty(), NS_ERROR_ILLEGAL_VALUE);
// Parse the range header: e.g. Content-Range: bytes 7000-7999/8000.
int32_t spacePos = rangeStr.Find(NS_LITERAL_CSTRING(" "));
int32_t dashPos = rangeStr.Find(NS_LITERAL_CSTRING("-"), true, spacePos);
int32_t slashPos = rangeStr.Find(NS_LITERAL_CSTRING("/"), true, dashPos);
nsAutoCString aRangeStartText;
rangeStr.Mid(aRangeStartText, spacePos+1, dashPos-(spacePos+1));
aRangeStart = aRangeStartText.ToInteger64(&rv);
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_TRUE(0 <= aRangeStart, NS_ERROR_ILLEGAL_VALUE);
nsAutoCString aRangeEndText;
rangeStr.Mid(aRangeEndText, dashPos+1, slashPos-(dashPos+1));
aRangeEnd = aRangeEndText.ToInteger64(&rv);
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_TRUE(aRangeStart < aRangeEnd, NS_ERROR_ILLEGAL_VALUE);
nsAutoCString aRangeTotalText;
rangeStr.Right(aRangeTotalText, rangeStr.Length()-(slashPos+1));
if (aRangeTotalText[0] == '*') {
aRangeTotal = -1;
} else {
aRangeTotal = aRangeTotalText.ToInteger64(&rv);
NS_ENSURE_TRUE(aRangeEnd < aRangeTotal, NS_ERROR_ILLEGAL_VALUE);
NS_ENSURE_SUCCESS(rv, rv);
}
CMLOG("Received bytes [%d] to [%d] of [%d]",
aRangeStart, aRangeEnd, aRangeTotal);
return NS_OK;
}
nsresult
ChannelMediaResource::OnStopRequest(nsIRequest* aRequest, nsresult aStatus)
{
@ -296,6 +411,14 @@ ChannelMediaResource::OnStopRequest(nsIRequest* aRequest, nsresult aStatus)
mChannelStatistics.Stop(TimeStamp::Now());
}
// If we were loading a byte range, notify decoder and return.
// Skip this for unterminated byte range requests, e.g. seeking for whole
// file downloads.
if (mByteRangeDownloads) {
mDecoder->NotifyDownloadEnded(aStatus);
return NS_OK;
}
// Note that aStatus might have succeeded --- this might be a normal close
// --- even in situations where the server cut us off because we were
// suspended. So we need to "reopen on error" in that case too. The only
@ -361,6 +484,8 @@ ChannelMediaResource::CopySegmentToCache(nsIInputStream *aInStream,
// Keep track of where we're up to
closure->mResource->mOffset += aCount;
LOG("%p [ChannelMediaResource]: CopySegmentToCache new mOffset = %d",
closure->mResource, closure->mResource->mOffset);
closure->mResource->mCacheStream.NotifyDataReceived(aCount, aFromSegment,
closure->mPrincipal);
*aWriteCount = aCount;
@ -400,6 +525,37 @@ ChannelMediaResource::OnDataAvailable(nsIRequest* aRequest,
return NS_OK;
}
/* |OpenByteRange|
* For terminated byte range requests, use this function.
* Callback is |nsBuiltinDecoder|::|NotifyByteRangeDownloaded|().
* See |CacheClientSeek| also.
*/
nsresult
ChannelMediaResource::OpenByteRange(nsIStreamListener** aStreamListener,
MediaByteRange const & aByteRange)
{
NS_ASSERTION(NS_IsMainThread(), "Only call on main thread");
mByteRangeDownloads = true;
mByteRange = aByteRange;
// OpenByteRange may be called multiple times; same URL, different ranges.
// For the first call using this URL, forward to Open for some init.
if (mByteRangeFirstOpen) {
mByteRangeFirstOpen = false;
return Open(aStreamListener);
}
// For subsequent calls, ensure channel is recreated with correct byte range.
CloseChannel();
nsresult rv = RecreateChannel();
NS_ENSURE_SUCCESS(rv, rv);
return OpenChannel(aStreamListener);
}
nsresult ChannelMediaResource::Open(nsIStreamListener **aStreamListener)
{
NS_ASSERTION(NS_IsMainThread(), "Only call on main thread");
@ -481,9 +637,19 @@ void ChannelMediaResource::SetupChannelHeaders()
// requests, and therefore seeking, early.
nsCOMPtr<nsIHttpChannel> hc = do_QueryInterface(mChannel);
if (hc) {
// Use |mByteRange| for a specific chunk, or |mOffset| if seeking in a
// complete file download.
nsAutoCString rangeString("bytes=");
rangeString.AppendInt(mOffset);
if (!mByteRange.IsNull()) {
rangeString.AppendInt(mByteRange.mStart);
mOffset = mByteRange.mStart;
} else {
rangeString.AppendInt(mOffset);
}
rangeString.Append("-");
if (!mByteRange.IsNull()) {
rangeString.AppendInt(mByteRange.mEnd);
}
hc->SetRequestHeader(NS_LITERAL_CSTRING("Range"), rangeString, false);
// Send Accept header for video and audio types only (Bug 489071)
@ -592,6 +758,12 @@ nsresult ChannelMediaResource::Seek(int32_t aWhence, int64_t aOffset)
{
NS_ASSERTION(!NS_IsMainThread(), "Don't call on main thread");
// Remember |aOffset|, because Media Cache may request a diff offset later.
if (mByteRangeDownloads) {
ReentrantMonitorAutoEnter mon(mSeekOffsetMonitor);
mSeekOffset = aOffset;
}
return mCacheStream.Seek(aWhence, aOffset);
}
@ -790,6 +962,51 @@ ChannelMediaResource::CacheClientSeek(int64_t aOffset, bool aResume)
--mSuspendCount;
}
// Note: For chunked downloads, e.g. DASH, we need to determine which chunk
// contains the requested offset, |mOffset|. This is either previously
// requested in |Seek| or updated to the most recent bytes downloaded.
// So the process below is:
// 1 - Query decoder for chunk containing desired offset, |mOffset|.
// Return silently if the offset is not available; suggests decoder is
// yet to get range information.
// Return with NetworkError for all other errors.
//
// 2 - Adjust |mByteRange|.mStart to |aOffset|, requested by media cache.
// For seeking, the media cache always requests the start of the cache
// block, so we need to adjust the first chunk of a seek.
// E.g. For "DASH-WebM On Demand" this means the first chunk after
// seeking will most likely be larger than the subsegment (cluster).
//
// 3 - Call |OpenByteRange| requesting |mByteRange| bytes.
if (mByteRangeDownloads) {
// Query decoder for chunk containing desired offset.
// XXX Implement |nsDASHRepDecoder|::|GetByteRange| in future patch.
nsresult rv;
{
ReentrantMonitorAutoEnter mon(mSeekOffsetMonitor);
// Ensure that media cache can only request an equal or smaller offset;
// it may be trying to include the start of a cache block.
NS_ENSURE_TRUE(aOffset <= mSeekOffset, NS_ERROR_ILLEGAL_VALUE);
rv = mDecoder->GetByteRangeForSeek(mSeekOffset, mByteRange);
mSeekOffset = -1;
}
if (rv == NS_ERROR_NOT_AVAILABLE) {
// Assume decoder will request correct bytes when range information
// becomes available. Return silently.
return NS_OK;
} else if (NS_FAILED(rv) || mByteRange.IsNull()) {
// Decoder reported an error we don't want to handle here; just return.
mDecoder->NetworkError();
CloseChannel();
return rv;
}
// Media cache may decrease offset to start of cache data block.
// Adjust start of byte range accordingly.
mByteRange.mStart = mOffset = aOffset;
return OpenByteRange(nullptr, mByteRange);
}
mOffset = aOffset;
if (mSuspendCount > 0) {

View File

@ -8,7 +8,9 @@
#include "mozilla/Mutex.h"
#include "mozilla/XPCOM.h"
#include "mozilla/ReentrantMonitor.h"
#include "nsIChannel.h"
#include "nsIHttpChannel.h"
#include "nsIPrincipal.h"
#include "nsIURI.h"
#include "nsIStreamListener.h"
@ -111,6 +113,12 @@ public:
return mStart == 0 && mEnd == 0;
}
// Clears byte range values.
void Clear() {
mStart = 0;
mEnd = 0;
}
int64_t mStart, mEnd;
};
@ -282,6 +290,17 @@ public:
*/
virtual nsresult Open(nsIStreamListener** aStreamListener) = 0;
/**
* Open the stream using a specific byte range only. Creates a stream
* listener and returns it in aStreamListener; this listener needs to be
* notified of incoming data. Byte range is specified in aByteRange.
*/
virtual nsresult OpenByteRange(nsIStreamListener** aStreamListener,
MediaByteRange const &aByteRange)
{
return Open(aStreamListener);
}
/**
* Fills aRanges with MediaByteRanges representing the data which is cached
* in the media cache. Stream should be pinned during call and while
@ -364,6 +383,8 @@ public:
// Main thread
virtual nsresult Open(nsIStreamListener** aStreamListener);
virtual nsresult OpenByteRange(nsIStreamListener** aStreamListener,
MediaByteRange const & aByteRange);
virtual nsresult Close();
virtual void Suspend(bool aCloseImmediately);
virtual void Resume();
@ -435,6 +456,14 @@ protected:
// Closes the channel. Main thread only.
void CloseChannel();
// Parses 'Content-Range' header and returns results via parameters.
// Returns error if header is not available, values are not parse-able or
// values are out of range.
nsresult ParseContentRangeHeader(nsIHttpChannel * aHttpChan,
int64_t& aRangeStart,
int64_t& aRangeEnd,
int64_t& aRangeTotal);
void DoNotifyDataReceived();
static NS_METHOD CopySegmentToCache(nsIInputStream *aInStream,
@ -480,6 +509,21 @@ protected:
// True if we are seeking to get the real duration of the file.
bool mSeekingForMetadata;
// Start and end offset of the bytes to be requested.
MediaByteRange mByteRange;
// True if resource was opened with a byte rage request.
bool mByteRangeDownloads;
// Set to false once first byte range request has been made.
bool mByteRangeFirstOpen;
// For byte range requests, set to the offset requested in |Seek|.
// Used in |CacheClientSeek| to find the originally requested byte range.
// Read/Write on multiple threads; use |mSeekMonitor|.
ReentrantMonitor mSeekOffsetMonitor;
int64_t mSeekOffset;
};
}

View File

@ -20,6 +20,7 @@ class nsITimer;
namespace mozilla {
class MediaResource;
class MediaByteRange;
}
// The size to use for audio data frames in MozAudioAvailable events.
@ -39,6 +40,7 @@ class nsMediaDecoder : public nsIObserver
{
public:
typedef mozilla::MediaResource MediaResource;
typedef mozilla::MediaByteRange MediaByteRange;
typedef mozilla::ReentrantMonitor ReentrantMonitor;
typedef mozilla::SourceMediaStream SourceMediaStream;
typedef mozilla::ProcessedMediaStream ProcessedMediaStream;
@ -73,6 +75,14 @@ public:
// Seek to the time position in (seconds) from the start of the video.
virtual nsresult Seek(double aTime) = 0;
// Enables decoders to supply an enclosing byte range for a seek offset.
// E.g. used by ChannelMediaResource to download a whole cluster for
// DASH-WebM.
virtual nsresult GetByteRangeForSeek(int64_t const aOffset,
MediaByteRange &aByteRange) {
return NS_ERROR_NOT_AVAILABLE;
}
// Called by the element when the playback rate has been changed.
// Adjust the speed of the playback, optionally with pitch correction,
// when this is called.