Index: media/blink/multibuffer_resource_loader.cc |
diff --git a/media/blink/multibuffer_resource_loader.cc b/media/blink/multibuffer_resource_loader.cc |
new file mode 100644 |
index 0000000000000000000000000000000000000000..9b50ddf1235cef4d524d5ec7ea7f771f5502b1ab |
--- /dev/null |
+++ b/media/blink/multibuffer_resource_loader.cc |
@@ -0,0 +1,477 @@ |
+// Copyright 2015 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 "base/bind.h" |
+#include "base/bits.h" |
+#include "base/callback_helpers.h" |
+#include "base/message_loop/message_loop.h" |
+#include "base/metrics/histogram.h" |
+#include "base/strings/string_number_conversions.h" |
+#include "media/base/media_export.h" |
+#include "media/blink/active_loader.h" |
+#include "media/blink/cache_util.h" |
+#include "media/blink/multibuffer_resource_loader.h" |
+#include "net/http/http_byte_range.h" |
+#include "net/http/http_request_headers.h" |
+#include "third_party/WebKit/public/platform/WebURLError.h" |
+#include "third_party/WebKit/public/platform/WebURLResponse.h" |
+ |
+using blink::WebFrame; |
+using blink::WebString; |
+using blink::WebURLError; |
+using blink::WebURLLoader; |
+using blink::WebURLLoaderOptions; |
+using blink::WebURLRequest; |
+using blink::WebURLResponse; |
+ |
+namespace media { |
+ |
+// The number of milliseconds to wait before retrying a failed load. |
+const int kLoaderFailedRetryDelayMs = 250; |
+ |
+static const int kHttpOK = 200; |
+static const int kHttpPartialContent = 206; |
+static const int kMaxRetries = 3; |
+ |
+ResourceMultiBuffer::ResourceMultiBuffer(blink::WebFrame* frame) |
+ : MultiBuffer(15), |
+ frame_(frame) { |
+} |
+ResourceMultiBuffer::~ResourceMultiBuffer() {} |
+ |
+MultiBuffer::DataProvider* ResourceMultiBuffer::CreateWriter( |
+ const MultiBufferBlockId& pos) { |
+ ResourceMultiBufferDataProvider* ret = |
+ new ResourceMultiBufferDataProvider(pos, this); |
+ ret->Start(); |
+ return ret; |
+} |
+ |
+void ResourceMultiBuffer::OnRedirect(const scoped_refptr<UrlData>& from, |
+ const scoped_refptr<UrlData>& to) { |
+ UpdateUrlData(from, to); |
+} |
+ |
+void ResourceMultiBuffer::Fail(const scoped_refptr<UrlData>& from) { |
+ // Or possibly just register a failure in the urldata class? |
+ UpdateUrlData(from, kUnknownUrlData); |
+} |
+ |
+ResourceMultiBufferDataProvider::ResourceMultiBufferDataProvider( |
+ const MultiBufferBlockId& pos, |
+ ResourceMultiBuffer* resource_multibuffer) : |
+ pos_(pos), |
+ resource_multibuffer_(resource_multibuffer), |
+ retries_(0), |
+ weak_factory_(this) { |
+ url_data_ = pos_.url_data(); |
+ DCHECK(url_data_) << " pos = " << pos; |
+ DCHECK_GE(pos.block_num(), 0); |
+ original_url_data_ = url_data_; |
+} |
+ |
+void ResourceMultiBufferDataProvider::Start() { |
+ // Prepare the request. |
+ WebURLRequest request(url_data_->url()); |
+ // TODO(mkwst): Split this into video/audio. |
+ request.setRequestContext(WebURLRequest::RequestContextVideo); |
+ |
+ request.setHTTPHeaderField( |
+ WebString::fromUTF8(net::HttpRequestHeaders::kRange), |
+ WebString::fromUTF8(net::HttpByteRange::RightUnbounded( |
+ byte_pos()).GetHeaderValue())); |
+ |
+ resource_multibuffer_->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")); |
+ |
+ WebURLLoaderOptions options; |
+ if (url_data_->cors_mode() == UrlData::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 (url_data_->cors_mode() == UrlData::kUseCredentials) |
+ options.allowCredentials = true; |
+ } |
+ scoped_ptr<WebURLLoader> loader( |
+ resource_multibuffer_->frame_->createAssociatedURLLoader(options)); |
+ |
+ // Start the resource loading. |
+ loader->loadAsynchronously(request, this); |
+ active_loader_.reset(new ActiveLoader(loader.Pass())); |
+} |
+ |
+ResourceMultiBufferDataProvider::~ResourceMultiBufferDataProvider() {} |
+ |
+///////////////////////////////////////////////////////////////////////////// |
+// MultiBufferDataProvider implementation. |
+MultiBufferBlockId ResourceMultiBufferDataProvider::Tell() const { |
+ return pos_; |
+} |
+ |
+bool ResourceMultiBufferDataProvider::Available() const { |
+ if (fifo_.empty()) return false; |
+ if (fifo_.back()->end_of_stream()) return true; |
+ if (fifo_.front()->data_size() == block_size()) return true; |
+ return false; |
+} |
+ |
+scoped_refptr<DataBuffer> ResourceMultiBufferDataProvider::Read() { |
+ DCHECK(Available()); |
+ scoped_refptr<DataBuffer> ret = fifo_.front(); |
+ fifo_.pop_front(); |
+ ++pos_; |
+ return ret; |
+} |
+ |
+void ResourceMultiBufferDataProvider::SetAvailableCallback( |
+ const base::Closure& cb) { |
+ DCHECK(!Available()); |
+ cb_ = cb; |
+} |
+ |
+void ResourceMultiBufferDataProvider::SetDeferred(bool deferred) { |
+ if (active_loader_) { |
+ if (active_loader_->deferred() != deferred) { |
+ active_loader_->SetDeferred(deferred); |
+ } |
+ } |
+} |
+ |
+///////////////////////////////////////////////////////////////////////////// |
+// WebURLLoaderClient implementation. |
+ |
+void ResourceMultiBufferDataProvider::willFollowRedirect( |
+ WebURLLoader* loader, |
+ WebURLRequest& newRequest, |
+ const WebURLResponse& redirectResponse) { |
+ |
+ GURL url(newRequest.url()); |
+ scoped_refptr<UrlData> new_url_data = |
+ resource_multibuffer_->url_index()->GetByUrl(url, url_data_->cors_mode()); |
+ new_url_data->Use(); |
+ redirected_url_data_ = url_data_; |
+ url_data_ = new_url_data; |
+ |
+ redirected_url_data_->set_valid_until( |
+ GetMemoryCacheValidUntil(redirectResponse)); |
+} |
+ |
+void ResourceMultiBufferDataProvider::didSendData( |
+ WebURLLoader* loader, |
+ unsigned long long bytes_sent, |
+ unsigned long long total_bytes_to_be_sent) { |
+ NOTIMPLEMENTED(); |
+} |
+ |
+void ResourceMultiBufferDataProvider::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_); |
+ |
+ // This test is vital for security! |
+ if (original_url_data_->cors_mode() == UrlData::kUnspecified) { |
liberato (no reviews please)
2015/10/16 21:50:36
we'll probably want somebody to check this whole C
hubbe
2015/10/16 23:47:06
Acknowledged.
|
+ // Unless we already passed a CORS check, make sure that |
+ // we're on the same origin. |
+ if (original_url_data_->url().GetOrigin() != |
+ url_data_->url().GetOrigin()) { |
+ resource_multibuffer_->Fail(url_data_); |
+ return; |
+ } |
+ } |
+ |
+ base::Time last_modified; |
+ if (base::Time::FromString( |
+ response.httpHeaderField("Last-Modified").utf8().data(), |
+ &last_modified)) { |
+ scoped_refptr<UrlData> new_url_data = url_data_->set_last_modified( |
+ last_modified); |
+ new_url_data->Use(); |
+ if (new_url_data != url_data_) { |
+ // Not technically a redirect, but we found a better URLData. |
+ resource_multibuffer_->OnRedirect(url_data_, new_url_data); |
+ url_data_ = new_url_data; |
+ } |
+ } |
+ |
+ url_data_->set_valid_until(GetMemoryCacheValidUntil(response)); |
+ |
+ if (redirected_url_data_) { |
+ redirected_url_data_->set_redirects_to(url_data_->url()); |
+ resource_multibuffer_->OnRedirect(redirected_url_data_, url_data_); |
+ redirected_url_data_ = NULL; |
+ |
+ scoped_ptr<MultiBuffer::DataProvider> self( |
+ resource_multibuffer_->RemoveProvider(this)); |
+ pos_ = MultiBufferBlockId(url_data_, pos_.block_num()); |
+ resource_multibuffer_->AddProvider(self.Pass()); |
+ } |
+ |
+ uint32 reasons = GetReasonsForUncacheability(response); |
+ url_data_->set_cacheable(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. |
+ int64 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_data_->url().SchemeIsHTTPOrHTTPS()) { |
+ bool partial_response = (response.httpStatusCode() == kHttpPartialContent); |
+ bool ok_response = (response.httpStatusCode() == kHttpOK); |
+ |
+ // Check to see whether the server supports byte ranges. |
+ std::string accept_ranges = |
+ response.httpHeaderField("Accept-Ranges").utf8(); |
+ if (accept_ranges.find("bytes") != std::string::npos) |
+ url_data_->set_range_supported(); |
+ |
+ // 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)) { |
+ url_data_->set_range_supported(); |
+ } else if (ok_response && pos_.block_num() == 0) { |
+ // 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. |
+ url_data_->set_length(content_length); |
+ } else if (response.httpStatusCode() == 416) { |
+ // Really, we should never request a range that doesn't exist, but |
+ // if we do, let's handle it in a sane way. |
+ // Unsatisfiable range |
+ fifo_.push_back(DataBuffer::CreateEOSBuffer()); |
+ cb_.Run(); |
+ } else { |
+ resource_multibuffer_->Fail(url_data_); |
+ return; |
+ } |
+ } else { |
+ if (content_length != kPositionNotSpecified) { |
+ url_data_->set_length(content_length + byte_pos()); |
+ } |
+ } |
+} |
+ |
+void ResourceMultiBufferDataProvider::didReceiveData( |
+ WebURLLoader* loader, |
+ const char* data, |
+ int data_length, |
+ int encoded_data_length) { |
+ DVLOG(1) << "didReceiveData: " << data_length << " bytes"; |
+ DCHECK(!Available()); |
+ DCHECK(active_loader_); |
+ DCHECK_GT(data_length, 0); |
+ |
+ // When we receive data, we allow more retries. |
+ retries_ = 0; |
+ |
+ while (data_length) { |
+ if (fifo_.empty() || fifo_.back()->data_size() == block_size()) { |
+ fifo_.push_back(new DataBuffer(block_size())); |
+ fifo_.back()->set_data_size(0); |
+ } |
+ size_t to_append = std::min<size_t>( |
+ data_length, block_size() - fifo_.back()->data_size()); |
+ DCHECK_GT(to_append, 0); |
+ memcpy(fifo_.back()->writable_data() + fifo_.back()->data_size(), |
+ data, |
+ to_append); |
+ data += to_append; |
+ fifo_.back()->set_data_size(fifo_.back()->data_size() + to_append); |
+ data_length -= to_append; |
+ } |
+ |
+ if (Available()) |
+ cb_.Run(); |
+ |
+ // Beware, this object might be deleted here. |
+} |
+ |
+void ResourceMultiBufferDataProvider::didDownloadData( |
+ WebURLLoader* loader, |
+ int dataLength, |
+ int encoded_data_length) { |
+ NOTIMPLEMENTED(); |
+} |
+ |
+void ResourceMultiBufferDataProvider::didReceiveCachedMetadata( |
+ WebURLLoader* loader, |
+ const char* data, |
+ int data_length) { |
+ NOTIMPLEMENTED(); |
+} |
+ |
+void ResourceMultiBufferDataProvider::didFinishLoading( |
+ WebURLLoader* loader, |
+ double finishTime, |
+ int64_t total_encoded_data_length) { |
+ DVLOG(1) << "didFinishLoading"; |
+ DCHECK(active_loader_.get()); |
+ DCHECK(!Available()); |
+ |
+ // We're done with the loader. |
+ active_loader_.reset(); |
+ |
+ // If we didn't know the |instance_size_| we do now. |
+ size_t size = byte_pos(); |
+ if (!fifo_.empty()) |
+ size += fifo_.back()->data_size(); |
+ |
+ // This request reports something smaller than what we've seen in the past, |
+ // Maybe it's transient error? |
+ if (url_data_->length() != kPositionNotSpecified && |
+ size < url_data_->length()) { |
+ if (retries_ < kMaxRetries) { |
+ retries_++; |
+ base::MessageLoop::current()->PostDelayedTask( |
+ FROM_HERE, base::Bind(&ResourceMultiBufferDataProvider::Start, |
liberato (no reviews please)
2015/10/16 21:50:36
not sure that this works -- it starts fetching fro
hubbe
2015/10/16 23:47:06
Added fifo_.clear() to Start with a comment.
|
+ weak_factory_.GetWeakPtr()), |
+ base::TimeDelta::FromMilliseconds(kLoaderFailedRetryDelayMs)); |
+ return; |
+ } else { |
+ scoped_ptr<ActiveLoader> active_loader = active_loader_.Pass(); |
+ resource_multibuffer_->Fail(url_data_); |
+ return; |
+ } |
+ } |
+ |
+ url_data_->set_length(size); |
+ fifo_.push_back(DataBuffer::CreateEOSBuffer()); |
+ |
+ DCHECK(Available()); |
+ cb_.Run(); |
+ |
+ // Beware, this object might be deleted here. |
+} |
+ |
+void ResourceMultiBufferDataProvider::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()); |
+ |
+ if (retries_ < kMaxRetries) { |
+ retries_++; |
+ base::MessageLoop::current()->PostDelayedTask( |
+ FROM_HERE, base::Bind(&ResourceMultiBufferDataProvider::Start, |
liberato (no reviews please)
2015/10/16 21:50:36
same concern about |pos_| + |fifo_| here.
hubbe
2015/10/16 23:47:06
and the same solution.
|
+ weak_factory_.GetWeakPtr()), |
+ base::TimeDelta::FromMilliseconds(kLoaderFailedRetryDelayMs)); |
+ } else { |
+ // 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(); |
+ resource_multibuffer_->Fail(url_data_); |
+ } |
+} |
+ |
+bool ResourceMultiBufferDataProvider::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; |
+} |
+ |
+int64_t ResourceMultiBufferDataProvider::byte_pos() const { |
+ int64_t ret = pos_.block_num(); |
+ return ret << resource_multibuffer_->block_size_shift(); |
+} |
+ |
+int64_t ResourceMultiBufferDataProvider::block_size() const { |
+ int64_t ret = 1; |
+ return ret << resource_multibuffer_->block_size_shift(); |
+} |
+ |
+bool ResourceMultiBufferDataProvider::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 (url_data_->length() == kPositionNotSpecified) { |
+ url_data_->set_length(instance_size); |
+ } |
+ |
+ if (byte_pos() != first_byte_position) { |
+ return false; |
+ } |
+ |
+ return true; |
+} |
+ |
+} // namespace media |