blob: d88266d06b22dbe60cd90967b7de340973b58806 [file] [log] [blame]
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "media/blink/buffered_resource_loader.h"
#include "base/bits.h"
#include "base/callback_helpers.h"
#include "base/metrics/histogram.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "media/base/media_log.h"
#include "media/blink/cache_util.h"
#include "net/http/http_byte_range.h"
#include "net/http/http_request_headers.h"
#include "third_party/WebKit/public/platform/WebString.h"
#include "third_party/WebKit/public/platform/WebURLError.h"
#include "third_party/WebKit/public/platform/WebURLResponse.h"
#include "third_party/WebKit/public/web/WebKit.h"
#include "third_party/WebKit/public/web/WebURLLoaderOptions.h"
using blink::WebFrame;
using blink::WebString;
using blink::WebURLError;
using blink::WebURLLoader;
using blink::WebURLLoaderOptions;
using blink::WebURLRequest;
using blink::WebURLResponse;
namespace media {
static const int kHttpOK = 200;
static const int kHttpPartialContent = 206;
// Define the number of bytes in a megabyte.
static const int kMegabyte = 1024 * 1024;
// Minimum capacity of the buffer in forward or backward direction.
//
// 2MB is an arbitrary limit; it just seems to be "good enough" in practice.
static const int kMinBufferCapacity = 2 * kMegabyte;
// Maximum capacity of the buffer in forward or backward direction. This is
// effectively the largest single read the code path can handle.
// 20MB is an arbitrary limit; it just seems to be "good enough" in practice.
static const int kMaxBufferCapacity = 20 * kMegabyte;
// Maximum number of bytes outside the buffer we will wait for in order to
// fulfill a read. If a read starts more than 2MB away from the data we
// currently have in the buffer, we will not wait for buffer to reach the read's
// location and will instead reset the request.
static const int kForwardWaitThreshold = 2 * kMegabyte;
// Computes the suggested backward and forward capacity for the buffer
// if one wants to play at |playback_rate| * the natural playback speed.
// Use a value of 0 for |bitrate| if it is unknown.
static void ComputeTargetBufferWindow(float playback_rate, int bitrate,
int* out_backward_capacity,
int* out_forward_capacity) {
static const int kDefaultBitrate = 200 * 1024 * 8; // 200 Kbps.
static const int kMaxBitrate = 20 * kMegabyte * 8; // 20 Mbps.
static const float kMaxPlaybackRate = 25.0;
static const int kTargetSecondsBufferedAhead = 10;
static const int kTargetSecondsBufferedBehind = 2;
// Use a default bit rate if unknown and clamp to prevent overflow.
if (bitrate <= 0)
bitrate = kDefaultBitrate;
bitrate = std::min(bitrate, kMaxBitrate);
// Only scale the buffer window for playback rates greater than 1.0 in
// magnitude and clamp to prevent overflow.
bool backward_playback = false;
if (playback_rate < 0.0f) {
backward_playback = true;
playback_rate *= -1.0f;
}
playback_rate = std::max(playback_rate, 1.0f);
playback_rate = std::min(playback_rate, kMaxPlaybackRate);
int bytes_per_second = (bitrate / 8.0) * playback_rate;
// Clamp between kMinBufferCapacity and kMaxBufferCapacity.
*out_forward_capacity = std::max(
kTargetSecondsBufferedAhead * bytes_per_second, kMinBufferCapacity);
*out_backward_capacity = std::max(
kTargetSecondsBufferedBehind * bytes_per_second, kMinBufferCapacity);
*out_forward_capacity = std::min(*out_forward_capacity, kMaxBufferCapacity);
*out_backward_capacity = std::min(*out_backward_capacity, kMaxBufferCapacity);
if (backward_playback)
std::swap(*out_forward_capacity, *out_backward_capacity);
}
BufferedResourceLoader::BufferedResourceLoader(
const GURL& url,
CORSMode cors_mode,
int64 first_byte_position,
int64 last_byte_position,
DeferStrategy strategy,
int bitrate,
float playback_rate,
MediaLog* media_log)
: buffer_(kMinBufferCapacity, kMinBufferCapacity),
loader_failed_(false),
defer_strategy_(strategy),
might_be_reused_from_cache_in_future_(true),
range_supported_(false),
saved_forward_capacity_(0),
url_(url),
cors_mode_(cors_mode),
first_byte_position_(first_byte_position),
last_byte_position_(last_byte_position),
single_origin_(true),
offset_(0),
content_length_(kPositionNotSpecified),
instance_size_(kPositionNotSpecified),
read_position_(0),
read_size_(0),
read_buffer_(NULL),
first_offset_(0),
last_offset_(0),
bitrate_(bitrate),
playback_rate_(playback_rate),
media_log_(media_log) {
// Set the initial capacity of |buffer_| based on |bitrate_| and
// |playback_rate_|.
UpdateBufferWindow();
}
BufferedResourceLoader::~BufferedResourceLoader() {}
void BufferedResourceLoader::Start(
const StartCB& start_cb,
const LoadingStateChangedCB& loading_cb,
const ProgressCB& progress_cb,
WebFrame* frame) {
// Make sure we have not started.
DCHECK(start_cb_.is_null());
DCHECK(loading_cb_.is_null());
DCHECK(progress_cb_.is_null());
DCHECK(!start_cb.is_null());
DCHECK(!loading_cb.is_null());
DCHECK(!progress_cb.is_null());
CHECK(frame);
start_cb_ = start_cb;
loading_cb_ = loading_cb;
progress_cb_ = progress_cb;
if (first_byte_position_ != kPositionNotSpecified) {
// TODO(hclam): server may not support range request so |offset_| may not
// equal to |first_byte_position_|.
offset_ = first_byte_position_;
}
// Prepare the request.
WebURLRequest request(url_);
// TODO(mkwst): Split this into video/audio.
request.setRequestContext(WebURLRequest::RequestContextVideo);
if (IsRangeRequest()) {
request.setHTTPHeaderField(
WebString::fromUTF8(net::HttpRequestHeaders::kRange),
WebString::fromUTF8(net::HttpByteRange::Bounded(
first_byte_position_, last_byte_position_).GetHeaderValue()));
}
frame->setReferrerForRequest(request, blink::WebURL());
// Disable compression, compression for audio/video doesn't make sense...
request.setHTTPHeaderField(
WebString::fromUTF8(net::HttpRequestHeaders::kAcceptEncoding),
WebString::fromUTF8("identity;q=1, *;q=0"));
// Check for our test WebURLLoader.
scoped_ptr<WebURLLoader> loader;
if (test_loader_) {
loader = test_loader_.Pass();
} else {
WebURLLoaderOptions options;
if (cors_mode_ == kUnspecified) {
options.allowCredentials = true;
options.crossOriginRequestPolicy =
WebURLLoaderOptions::CrossOriginRequestPolicyAllow;
} else {
options.exposeAllResponseHeaders = true;
// The author header set is empty, no preflight should go ahead.
options.preflightPolicy = WebURLLoaderOptions::PreventPreflight;
options.crossOriginRequestPolicy =
WebURLLoaderOptions::CrossOriginRequestPolicyUseAccessControl;
if (cors_mode_ == kUseCredentials)
options.allowCredentials = true;
}
loader.reset(frame->createAssociatedURLLoader(options));
}
// Start the resource loading.
loader->loadAsynchronously(request, this);
active_loader_.reset(new ActiveLoader(loader.Pass()));
loading_cb_.Run(kLoading);
}
void BufferedResourceLoader::Stop() {
// Reset callbacks.
start_cb_.Reset();
loading_cb_.Reset();
progress_cb_.Reset();
read_cb_.Reset();
// Cancel and reset any active loaders.
active_loader_.reset();
}
void BufferedResourceLoader::Read(
int64 position,
int read_size,
uint8* buffer,
const ReadCB& read_cb) {
DCHECK(start_cb_.is_null());
DCHECK(read_cb_.is_null());
DCHECK(!read_cb.is_null());
DCHECK(buffer);
DCHECK_GT(read_size, 0);
// Save the parameter of reading.
read_cb_ = read_cb;
read_position_ = position;
read_size_ = read_size;
read_buffer_ = buffer;
// Reads should immediately fail if the loader also failed.
if (loader_failed_) {
DoneRead(kFailed, 0);
return;
}
// If we're attempting to read past the end of the file, return a zero
// indicating EOF.
//
// This can happen with callees that read in fixed-sized amounts for parsing
// or at the end of chunked 200 responses when we discover the actual length
// of the file.
if (instance_size_ != kPositionNotSpecified &&
instance_size_ <= read_position_) {
DVLOG(1) << "Appear to have seeked beyond EOS; returning 0.";
DoneRead(kOk, 0);
return;
}
// Make sure |offset_| and |read_position_| does not differ by a large
// amount.
if (read_position_ > offset_ + kint32max ||
read_position_ < offset_ + kint32min) {
DoneRead(kCacheMiss, 0);
return;
}
// Make sure |read_size_| is not too large for the buffer to ever be able to
// fulfill the read request.
if (read_size_ > kMaxBufferCapacity) {
DoneRead(kFailed, 0);
return;
}
// Prepare the parameters.
first_offset_ = read_position_ - offset_;
last_offset_ = first_offset_ + read_size_;
// If we can serve the request now, do the actual read.
if (CanFulfillRead()) {
ReadInternal();
UpdateDeferBehavior();
return;
}
// If we expect the read request to be fulfilled later, expand capacity as
// necessary and disable deferring.
if (WillFulfillRead()) {
// Advance offset as much as possible to create additional capacity.
int advance = std::min(first_offset_, buffer_.forward_bytes());
bool ret = buffer_.Seek(advance);
DCHECK(ret);
offset_ += advance;
first_offset_ -= advance;
last_offset_ -= advance;
// Expand capacity to accomodate a read that extends past the normal
// capacity.
//
// This can happen when reading in a large seek index or when the
// first byte of a read request falls within kForwardWaitThreshold.
if (last_offset_ > buffer_.forward_capacity()) {
saved_forward_capacity_ = buffer_.forward_capacity();
buffer_.set_forward_capacity(last_offset_);
}
// Make sure we stop deferring now that there's additional capacity.
DCHECK(!ShouldDefer())
<< "Capacity was not adjusted properly to prevent deferring.";
UpdateDeferBehavior();
return;
}
// Make a callback to report failure.
DoneRead(kCacheMiss, 0);
}
int64 BufferedResourceLoader::content_length() {
return content_length_;
}
int64 BufferedResourceLoader::instance_size() {
return instance_size_;
}
bool BufferedResourceLoader::range_supported() {
return range_supported_;
}
/////////////////////////////////////////////////////////////////////////////
// blink::WebURLLoaderClient implementation.
void BufferedResourceLoader::willSendRequest(
WebURLLoader* loader,
WebURLRequest& newRequest,
const WebURLResponse& redirectResponse) {
// The load may have been stopped and |start_cb| is destroyed.
// In this case we shouldn't do anything.
if (start_cb_.is_null()) {
// Set the url in the request to an invalid value (empty url).
newRequest.setURL(blink::WebURL());
return;
}
// Only allow |single_origin_| if we haven't seen a different origin yet.
if (single_origin_)
single_origin_ = url_.GetOrigin() == GURL(newRequest.url()).GetOrigin();
url_ = newRequest.url();
}
void BufferedResourceLoader::didSendData(
WebURLLoader* loader,
unsigned long long bytes_sent,
unsigned long long total_bytes_to_be_sent) {
NOTIMPLEMENTED();
}
void BufferedResourceLoader::didReceiveResponse(
WebURLLoader* loader,
const WebURLResponse& response) {
DVLOG(1) << "didReceiveResponse: HTTP/"
<< (response.httpVersion() == WebURLResponse::HTTP_0_9 ? "0.9" :
response.httpVersion() == WebURLResponse::HTTP_1_0 ? "1.0" :
response.httpVersion() == WebURLResponse::HTTP_1_1 ? "1.1" :
"Unknown")
<< " " << response.httpStatusCode();
DCHECK(active_loader_.get());
// The loader may have been stopped and |start_cb| is destroyed.
// In this case we shouldn't do anything.
if (start_cb_.is_null())
return;
uint32 reasons = GetReasonsForUncacheability(response);
might_be_reused_from_cache_in_future_ = reasons == 0;
UMA_HISTOGRAM_BOOLEAN("Media.CacheUseful", reasons == 0);
int shift = 0;
int max_enum = base::bits::Log2Ceiling(kMaxReason);
while (reasons) {
DCHECK_LT(shift, max_enum); // Sanity check.
if (reasons & 0x1) {
UMA_HISTOGRAM_ENUMERATION("Media.UncacheableReason",
shift,
max_enum); // PRESUBMIT_IGNORE_UMA_MAX
}
reasons >>= 1;
++shift;
}
// Expected content length can be |kPositionNotSpecified|, in that case
// |content_length_| is not specified and this is a streaming response.
content_length_ = response.expectedContentLength();
// We make a strong assumption that when we reach here we have either
// received a response from HTTP/HTTPS protocol or the request was
// successful (in particular range request). So we only verify the partial
// response for HTTP and HTTPS protocol.
if (url_.SchemeIsHTTPOrHTTPS()) {
bool partial_response = (response.httpStatusCode() == kHttpPartialContent);
bool ok_response = (response.httpStatusCode() == kHttpOK);
if (IsRangeRequest()) {
// Check to see whether the server supports byte ranges.
std::string accept_ranges =
response.httpHeaderField("Accept-Ranges").utf8();
range_supported_ = (accept_ranges.find("bytes") != std::string::npos);
// If we have verified the partial response and it is correct, we will
// return kOk. It's also possible for a server to support range requests
// without advertising "Accept-Ranges: bytes".
if (partial_response && VerifyPartialResponse(response)) {
range_supported_ = true;
} else if (ok_response && first_byte_position_ == 0 &&
last_byte_position_ == kPositionNotSpecified) {
// We accept a 200 response for a Range:0- request, trusting the
// Accept-Ranges header, because Apache thinks that's a reasonable thing
// to return.
instance_size_ = content_length_;
} else {
DoneStart(kFailed);
return;
}
} else {
instance_size_ = content_length_;
if (response.httpStatusCode() != kHttpOK) {
// We didn't request a range but server didn't reply with "200 OK".
DoneStart(kFailed);
return;
}
}
} else {
CHECK_EQ(instance_size_, kPositionNotSpecified);
if (content_length_ != kPositionNotSpecified) {
if (first_byte_position_ == kPositionNotSpecified)
instance_size_ = content_length_;
else if (last_byte_position_ == kPositionNotSpecified)
instance_size_ = content_length_ + first_byte_position_;
}
}
// Calls with a successful response.
DoneStart(kOk);
}
void BufferedResourceLoader::didReceiveData(
WebURLLoader* loader,
const char* data,
int data_length,
int encoded_data_length) {
DVLOG(1) << "didReceiveData: " << data_length << " bytes";
DCHECK(active_loader_.get());
DCHECK_GT(data_length, 0);
buffer_.Append(reinterpret_cast<const uint8*>(data), data_length);
// If there is an active read request, try to fulfill the request.
if (HasPendingRead() && CanFulfillRead())
ReadInternal();
// At last see if the buffer is full and we need to defer the downloading.
UpdateDeferBehavior();
// Consume excess bytes from our in-memory buffer if necessary.
if (buffer_.forward_bytes() > buffer_.forward_capacity()) {
int excess = buffer_.forward_bytes() - buffer_.forward_capacity();
bool success = buffer_.Seek(excess);
DCHECK(success);
offset_ += first_offset_ + excess;
}
// Notify latest progress and buffered offset.
progress_cb_.Run(offset_ + buffer_.forward_bytes() - 1);
Log();
}
void BufferedResourceLoader::didDownloadData(
blink::WebURLLoader* loader,
int dataLength,
int encoded_data_length) {
NOTIMPLEMENTED();
}
void BufferedResourceLoader::didReceiveCachedMetadata(
WebURLLoader* loader,
const char* data,
int data_length) {
NOTIMPLEMENTED();
}
void BufferedResourceLoader::didFinishLoading(
WebURLLoader* loader,
double finishTime,
int64_t total_encoded_data_length) {
DVLOG(1) << "didFinishLoading";
DCHECK(active_loader_.get());
// We're done with the loader.
active_loader_.reset();
loading_cb_.Run(kLoadingFinished);
// If we didn't know the |instance_size_| we do now.
if (instance_size_ == kPositionNotSpecified) {
instance_size_ = offset_ + buffer_.forward_bytes();
}
// If there is a start callback, run it.
if (!start_cb_.is_null()) {
DCHECK(read_cb_.is_null())
<< "Shouldn't have a read callback during start";
DoneStart(kOk);
return;
}
// Don't leave read callbacks hanging around.
if (HasPendingRead()) {
// Try to fulfill with what is in the buffer.
if (CanFulfillRead())
ReadInternal();
else
DoneRead(kCacheMiss, 0);
}
}
void BufferedResourceLoader::didFail(
WebURLLoader* loader,
const WebURLError& error) {
DVLOG(1) << "didFail: reason=" << error.reason
<< ", isCancellation=" << error.isCancellation
<< ", domain=" << error.domain.utf8().data()
<< ", localizedDescription="
<< error.localizedDescription.utf8().data();
DCHECK(active_loader_.get());
// We don't need to continue loading after failure.
//
// Keep it alive until we exit this method so that |error| remains valid.
scoped_ptr<ActiveLoader> active_loader = active_loader_.Pass();
loader_failed_ = true;
loading_cb_.Run(kLoadingFailed);
// Don't leave start callbacks hanging around.
if (!start_cb_.is_null()) {
DCHECK(read_cb_.is_null())
<< "Shouldn't have a read callback during start";
DoneStart(kFailed);
return;
}
// Don't leave read callbacks hanging around.
if (HasPendingRead()) {
DoneRead(kFailed, 0);
}
}
bool BufferedResourceLoader::HasSingleOrigin() const {
DCHECK(start_cb_.is_null())
<< "Start() must complete before calling HasSingleOrigin()";
return single_origin_;
}
bool BufferedResourceLoader::DidPassCORSAccessCheck() const {
DCHECK(start_cb_.is_null())
<< "Start() must complete before calling DidPassCORSAccessCheck()";
return !loader_failed_ && cors_mode_ != kUnspecified;
}
void BufferedResourceLoader::UpdateDeferStrategy(DeferStrategy strategy) {
if (!might_be_reused_from_cache_in_future_ && strategy == kNeverDefer)
strategy = kCapacityDefer;
defer_strategy_ = strategy;
UpdateDeferBehavior();
}
void BufferedResourceLoader::SetPlaybackRate(float playback_rate) {
playback_rate_ = playback_rate;
// This is a pause so don't bother updating the buffer window as we'll likely
// get unpaused in the future.
if (playback_rate_ == 0.0)
return;
UpdateBufferWindow();
}
void BufferedResourceLoader::SetBitrate(int bitrate) {
DCHECK(bitrate >= 0);
bitrate_ = bitrate;
UpdateBufferWindow();
}
/////////////////////////////////////////////////////////////////////////////
// Helper methods.
void BufferedResourceLoader::UpdateBufferWindow() {
int backward_capacity;
int forward_capacity;
ComputeTargetBufferWindow(
playback_rate_, bitrate_, &backward_capacity, &forward_capacity);
// This does not evict data from the buffer if the new capacities are less
// than the current capacities; the new limits will be enforced after the
// existing excess buffered data is consumed.
buffer_.set_backward_capacity(backward_capacity);
buffer_.set_forward_capacity(forward_capacity);
}
void BufferedResourceLoader::UpdateDeferBehavior() {
if (!active_loader_)
return;
SetDeferred(ShouldDefer());
}
void BufferedResourceLoader::SetDeferred(bool deferred) {
if (active_loader_->deferred() == deferred)
return;
active_loader_->SetDeferred(deferred);
loading_cb_.Run(deferred ? kLoadingDeferred : kLoading);
}
bool BufferedResourceLoader::ShouldDefer() const {
switch(defer_strategy_) {
case kNeverDefer:
return false;
case kReadThenDefer:
DCHECK(read_cb_.is_null() || last_offset_ > buffer_.forward_bytes())
<< "We shouldn't stop deferring if we can fulfill the read";
return read_cb_.is_null();
case kCapacityDefer:
return buffer_.forward_bytes() >= buffer_.forward_capacity();
}
NOTREACHED();
return false;
}
bool BufferedResourceLoader::CanFulfillRead() const {
// If we are reading too far in the backward direction.
if (first_offset_ < 0 && (first_offset_ + buffer_.backward_bytes()) < 0)
return false;
// If the start offset is too far ahead.
if (first_offset_ >= buffer_.forward_bytes())
return false;
// At the point, we verified that first byte requested is within the buffer.
// If the request has completed, then just returns with what we have now.
if (!active_loader_)
return true;
// If the resource request is still active, make sure the whole requested
// range is covered.
if (last_offset_ > buffer_.forward_bytes())
return false;
return true;
}
bool BufferedResourceLoader::WillFulfillRead() const {
// Trying to read too far behind.
if (first_offset_ < 0 && (first_offset_ + buffer_.backward_bytes()) < 0)
return false;
// Trying to read too far ahead.
if ((first_offset_ - buffer_.forward_bytes()) >= kForwardWaitThreshold)
return false;
// The resource request has completed, there's no way we can fulfill the
// read request.
if (!active_loader_)
return false;
return true;
}
void BufferedResourceLoader::ReadInternal() {
// Seek to the first byte requested.
bool ret = buffer_.Seek(first_offset_);
DCHECK(ret);
// Then do the read.
int read = buffer_.Read(read_buffer_, read_size_);
offset_ += first_offset_ + read;
// And report with what we have read.
DoneRead(kOk, read);
}
int64 BufferedResourceLoader::first_byte_position() const {
return first_byte_position_;
}
// static
bool BufferedResourceLoader::ParseContentRange(
const std::string& content_range_str, int64* first_byte_position,
int64* last_byte_position, int64* instance_size) {
const std::string kUpThroughBytesUnit = "bytes ";
if (content_range_str.find(kUpThroughBytesUnit) != 0)
return false;
std::string range_spec =
content_range_str.substr(kUpThroughBytesUnit.length());
size_t dash_offset = range_spec.find("-");
size_t slash_offset = range_spec.find("/");
if (dash_offset == std::string::npos || slash_offset == std::string::npos ||
slash_offset < dash_offset || slash_offset + 1 == range_spec.length()) {
return false;
}
if (!base::StringToInt64(range_spec.substr(0, dash_offset),
first_byte_position) ||
!base::StringToInt64(range_spec.substr(dash_offset + 1,
slash_offset - dash_offset - 1),
last_byte_position)) {
return false;
}
if (slash_offset == range_spec.length() - 2 &&
range_spec[slash_offset + 1] == '*') {
*instance_size = kPositionNotSpecified;
} else {
if (!base::StringToInt64(range_spec.substr(slash_offset + 1),
instance_size)) {
return false;
}
}
if (*last_byte_position < *first_byte_position ||
(*instance_size != kPositionNotSpecified &&
*last_byte_position >= *instance_size)) {
return false;
}
return true;
}
bool BufferedResourceLoader::VerifyPartialResponse(
const WebURLResponse& response) {
int64 first_byte_position, last_byte_position, instance_size;
if (!ParseContentRange(response.httpHeaderField("Content-Range").utf8(),
&first_byte_position, &last_byte_position,
&instance_size)) {
return false;
}
if (instance_size != kPositionNotSpecified) {
instance_size_ = instance_size;
}
if (first_byte_position_ != kPositionNotSpecified &&
first_byte_position_ != first_byte_position) {
return false;
}
// TODO(hclam): I should also check |last_byte_position|, but since
// we will never make such a request that it is ok to leave it unimplemented.
return true;
}
void BufferedResourceLoader::DoneRead(Status status, int bytes_read) {
if (saved_forward_capacity_) {
buffer_.set_forward_capacity(saved_forward_capacity_);
saved_forward_capacity_ = 0;
}
read_position_ = 0;
read_size_ = 0;
read_buffer_ = NULL;
first_offset_ = 0;
last_offset_ = 0;
Log();
base::ResetAndReturn(&read_cb_).Run(status, bytes_read);
}
void BufferedResourceLoader::DoneStart(Status status) {
base::ResetAndReturn(&start_cb_).Run(status);
}
bool BufferedResourceLoader::IsRangeRequest() const {
return first_byte_position_ != kPositionNotSpecified;
}
void BufferedResourceLoader::Log() {
media_log_->AddEvent(
media_log_->CreateBufferedExtentsChangedEvent(
offset_ - buffer_.backward_bytes(),
offset_,
offset_ + buffer_.forward_bytes()));
}
} // namespace media