diff options
author | hclam@chromium.org <hclam@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-07-22 21:37:17 +0000 |
---|---|---|
committer | hclam@chromium.org <hclam@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-07-22 21:37:17 +0000 |
commit | 8e296bbd2d147c270d1839ab9dd93418532322e5 (patch) | |
tree | 250887882d757ad020aba85af7db935c4107a105 /webkit/glue/media | |
parent | 944a14b8d46e63e603850c6c8b0d0638dc492a14 (diff) | |
download | chromium_src-8e296bbd2d147c270d1839ab9dd93418532322e5.zip chromium_src-8e296bbd2d147c270d1839ab9dd93418532322e5.tar.gz chromium_src-8e296bbd2d147c270d1839ab9dd93418532322e5.tar.bz2 |
Changes to provide asynchronous read in data source:
1. FFmpegGlue now taks a FFmpegProtocol instead of DataSource as input
2. FFmpegDemuxr implements FFmpegProtocol and does the blocking read and submit asynchronous read request to DataSource (with unit tests)
3. Changed SimpleDataSource to work with asynchronous read (with unit tests)
4. Reimplemented BufferedDataSource to work with asynchronous read (with unit tests)
5. Moved BufferedDataSource from chrome/renderer/media to webkit/glue/media (for faster build/debug and better coverage in automated testing)
TEST=BufferedDataSourceTest.*, SimpleDataSourceTest.*, FFmpegDemuxerTest.*
Review URL: http://codereview.chromium.org/149567
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@21326 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'webkit/glue/media')
-rw-r--r-- | webkit/glue/media/buffered_data_source.cc | 734 | ||||
-rw-r--r-- | webkit/glue/media/buffered_data_source.h | 286 | ||||
-rw-r--r-- | webkit/glue/media/buffered_data_source_unittest.cc | 627 | ||||
-rw-r--r-- | webkit/glue/media/media_resource_loader_bridge_factory.h | 5 | ||||
-rw-r--r-- | webkit/glue/media/mock_media_resource_loader_bridge_factory.h | 36 | ||||
-rw-r--r-- | webkit/glue/media/simple_data_source.cc | 31 | ||||
-rw-r--r-- | webkit/glue/media/simple_data_source.h | 6 | ||||
-rw-r--r-- | webkit/glue/media/simple_data_source_unittest.cc | 246 |
8 files changed, 1949 insertions, 22 deletions
diff --git a/webkit/glue/media/buffered_data_source.cc b/webkit/glue/media/buffered_data_source.cc new file mode 100644 index 0000000..76e6e9d --- /dev/null +++ b/webkit/glue/media/buffered_data_source.cc @@ -0,0 +1,734 @@ +// Copyright (c) 2009 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/compiler_specific.h" +#include "base/message_loop.h" +#include "base/process_util.h" +#include "base/stl_util-inl.h" +#include "base/string_util.h" +#include "chrome/common/extensions/url_pattern.h" +#include "media/base/filter_host.h" +#include "net/base/load_flags.h" +#include "net/base/net_errors.h" +#include "net/http/http_response_headers.h" +#include "webkit/glue/media/buffered_data_source.h" +#include "webkit/glue/webappcachecontext.h" + +namespace { + +const char kHttpScheme[] = "http"; +const char kHttpsScheme[] = "https"; +const int64 kPositionNotSpecified = -1; +const int kHttpOK = 200; +const int kHttpPartialContent = 206; + +// Define the number of bytes in a megabyte. +const size_t kMegabyte = 1024 * 1024; + +// Backward capacity of the buffer, by default 2MB. +const size_t kBackwardCapcity = 2 * kMegabyte; + +// Forward capacity of the buffer, by default 10MB. +const size_t kForwardCapacity = 10 * kMegabyte; + +// The maximum offset to seek to, this value limits the range of seek to +// prevent corruption of calculations. The buffer has a maximum size of 12MB +// so this is enough to contain every valid operations. +const int kMaxSeek = 100 * kMegabyte; + +// The threshold of bytes that we should wait until the data arrives in the +// future instead of restarting a new connection. This number is defined in the +// number of bytes, we should determine this value from typical connection speed +// and amount of time for a suitable wait. Now I just make a guess for this +// number to be 2MB. +// TODO(hclam): determine a better value for this. +const int kForwardWaitThreshold = 2 * kMegabyte; + +// Defines how long we should wait for more data before we declare a connection +// timeout and start a new request. +// TODO(hclam): Use this value when retry is implemented. +// TODO(hclam): Set it to 5s, calibrate this value later. +const int kDataTransferTimeoutSeconds = 5; + +// Defines how many times we should try to read from a buffered resource loader +// before we declare a read error. After each failure of read from a buffered +// resource loader, a new one is created to be read. +// TODO(hclam): Use this value when retry is implemented. +const int kReadTrials = 3; + +// BufferedDataSource has an intermediate buffer, this value governs the initial +// size of that buffer. It is set to 32KB because this is a typical read size +// of FFmpeg. +const int kInitialReadBufferSize = 32768; + +// A helper method that accepts only HTTP, HTTPS and FILE protocol. +bool IsSchemeSupported(const GURL& url) { + return url.SchemeIs(kHttpScheme) || + url.SchemeIs(kHttpsScheme) || + url.SchemeIsFile(); +} + +} // namespace + +namespace webkit_glue { + +///////////////////////////////////////////////////////////////////////////// +// BufferedResourceLoader +BufferedResourceLoader::BufferedResourceLoader( + webkit_glue::MediaResourceLoaderBridgeFactory* bridge_factory, + const GURL& url, + int64 first_byte_position, + int64 last_byte_position) + : buffer_(new media::SeekableBuffer(kBackwardCapcity, kForwardCapacity)), + deferred_(false), + completed_(false), + range_requested_(false), + bridge_factory_(bridge_factory), + url_(url), + first_byte_position_(first_byte_position), + last_byte_position_(last_byte_position), + start_callback_(NULL), + bridge_(NULL), + offset_(0), + content_length_(kPositionNotSpecified), + read_callback_(NULL), + read_position_(0), + read_size_(0), + read_buffer_(NULL), + first_offset_(0), + last_offset_(0) { +} + +BufferedResourceLoader::~BufferedResourceLoader() { +} + +void BufferedResourceLoader::Start(net::CompletionCallback* start_callback) { + // Make sure we have not started. + DCHECK(!bridge_.get()); + DCHECK(!start_callback_.get()); + DCHECK(start_callback); + + start_callback_.reset(start_callback); + + if (first_byte_position_ != kPositionNotSpecified) { + range_requested_ = true; + // TODO(hclam): server may not support range request so |offset_| may not + // equal to |first_byte_position_|. + offset_ = first_byte_position_; + } + + // Creates the bridge on render thread since we can only access + // ResourceDispatcher on this thread. + bridge_.reset(bridge_factory_->CreateBridge(url_, + net::LOAD_BYPASS_CACHE, + first_byte_position_, + last_byte_position_)); + + // And start the resource loading. + bridge_->Start(this); +} + +void BufferedResourceLoader::Stop() { + // Reset callbacks. + start_callback_.reset(); + read_callback_.reset(); + + // Destroy internal buffer. + buffer_.reset(); + + if (bridge_.get()) { + // Cancel the resource request. + bridge_->Cancel(); + bridge_.reset(); + } +} + +void BufferedResourceLoader::Read(int64 position, + int read_size, + uint8* buffer, + net::CompletionCallback* read_callback) { + DCHECK(!read_callback_.get()); + DCHECK(buffer_.get()); + DCHECK(read_callback); + DCHECK(buffer); + + // Saves the parameter of reading. + read_callback_.reset(read_callback); + read_position_ = position; + read_size_ = read_size; + read_buffer_ = buffer; + + // Check that the read parameters are within the range that we can handle. + // If the read request is made too far from the current offset, report that + // we cannot serve the request. + if (VerifyRead()) { + // If we can serve the request now, do the actual read. + if (CanFulfillRead()) { + ReadInternal(); + DisableDeferIfNeeded(); + return; + } + + // If we expected the read request to be fulfilled later, returns + // immediately and let more data to flow in. + if (WillFulfillRead()) + return; + + // Make a callback to report failure. + DoneRead(net::ERR_CACHE_MISS); + return; + } + + // TODO(hclam): We should report a better error code than just 0. + DoneRead(0); +} + +///////////////////////////////////////////////////////////////////////////// +// BufferedResourceLoader, +// webkit_glue::ResourceLoaderBridge::Peer implementations +void BufferedResourceLoader::OnReceivedRedirect(const GURL& new_url) { + DCHECK(bridge_.get()); + DCHECK(start_callback_.get()); + + // Saves the new URL. + url_ = new_url; + + // If we got redirected to an unsupported protocol then stop. + if (!IsSchemeSupported(new_url)) { + DoneStart(net::ERR_ADDRESS_INVALID); + Stop(); + } +} + +void BufferedResourceLoader::OnReceivedResponse( + const webkit_glue::ResourceLoaderBridge::ResponseInfo& info, + bool content_filtered) { + DCHECK(bridge_.get()); + DCHECK(start_callback_.get()); + + int64 first_byte_position = -1; + int64 last_byte_position = -1; + int64 instance_size = -1; + + // The file:// protocol should be able to serve any request we want, so we + // take an exception for file protocol. + if (!url_.SchemeIsFile()) { + int error = net::OK; + if (!info.headers) { + // We expect to receive headers because this is a HTTP or HTTPS protocol, + // if not report failure. + error = net::ERR_INVALID_RESPONSE; + } else if (range_requested_) { + if (info.headers->response_code() != kHttpPartialContent || + !info.headers->GetContentRange(&first_byte_position, + &last_byte_position, + &instance_size)) { + // We requested a range, but server didn't reply with partial content or + // the "Content-Range" header is corrupted. + // TODO(hclam): should also make sure this is the range we requested. + error = net::ERR_INVALID_RESPONSE; + } + } else if (info.headers->response_code() != kHttpOK) { + // We didn't request a range but server didn't reply with "200 OK". + error = net::ERR_FAILED; + } + + if (error != net::OK) { + DoneStart(error); + Stop(); + return; + } + } + + // |info.content_length| can be -1, in that case |content_length_| is + // not specified and this is a streaming response. + content_length_ = info.content_length; + + // We only care about the first byte position if it's given by the server. + // TODO(hclam): If server replies with a different offset, consider failing + // here. + if (first_byte_position != kPositionNotSpecified) + offset_ = first_byte_position; + + // Calls with a successful response. + DoneStart(net::OK); +} + +void BufferedResourceLoader::OnReceivedData(const char* data, int len) { + DCHECK(bridge_.get()); + DCHECK(buffer_.get()); + + // Writes more data to |buffer_|. + buffer_->Append(len, reinterpret_cast<const uint8*>(data)); + + // 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. + EnableDeferIfNeeded(); +} + +void BufferedResourceLoader::OnCompletedRequest( + const URLRequestStatus& status, const std::string& security_info) { + DCHECK(bridge_.get()); + DCHECK(buffer_.get()); + + // Saves the information that the request has completed. + completed_ = true; + + // After the response has completed, we don't need the bridge any more. + bridge_.reset(); + + // If there is a start callback, calls it. + if (start_callback_.get()) { + DoneStart(status.os_error()); + } + + // If there is a pending read but the request has ended, returns with what we + // have. + if (HasPendingRead()) { + // If the request has failed, then fail the read. + if (!status.is_success()) { + DoneRead(net::ERR_FAILED); + return; + } + + // Otherwise try to fulfill with what is in the buffer. + if (CanFulfillRead()) + ReadInternal(); + else + DoneRead(net::ERR_CACHE_MISS); + } + + // There must not be any outstanding read request. + DCHECK(!read_callback_.get()); +} + +///////////////////////////////////////////////////////////////////////////// +// BufferedResourceLoader, private +void BufferedResourceLoader::EnableDeferIfNeeded() { + if (!deferred_ && + buffer_->forward_bytes() >= buffer_->forward_capacity()) { + deferred_ = true; + + if (bridge_.get()) + bridge_->SetDefersLoading(true); + } +} + +void BufferedResourceLoader::DisableDeferIfNeeded() { + if (deferred_ && + buffer_->forward_bytes() < buffer_->forward_capacity() / 2) { + deferred_ = false; + + if (bridge_.get()) + bridge_->SetDefersLoading(false); + } +} + +bool BufferedResourceLoader::CanFulfillRead() { + // If we are reading too far in the backward direction. + if (first_offset_ < 0 && + first_offset_ + static_cast<int>(buffer_->backward_bytes()) < 0) + return false; + + // If the start offset is too far ahead. + if (first_offset_ >= static_cast<int>(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 (completed_) + return true; + + // If the resource request is still active, make sure the whole requested + // range is covered. + if (last_offset_ > static_cast<int>(buffer_->forward_bytes())) + return false; + + return true; +} + +bool BufferedResourceLoader::WillFulfillRead() { + // Reading too far in the backward direction. + if (first_offset_ < 0 && + first_offset_ + static_cast<int>(buffer_->backward_bytes()) < 0) + return false; + + // Try to read too far ahead. + if (last_offset_ > kForwardWaitThreshold) + return false; + + // The resource request has completed, there's no way we can fulfill the + // read request. + if (completed_) + return false; + + return true; +} + +bool BufferedResourceLoader::VerifyRead() { + // Make sure |offset_| and |read_position_| does not differ by a large + // amount + if (read_position_ > offset_ + kMaxSeek) + return false; + else if (read_position_ < offset_ - kMaxSeek) + return false; + + // If we can manage the read request with int32 math, then prepare the + // parameters. + first_offset_ = static_cast<int>(read_position_ - offset_); + last_offset_ = first_offset_ + read_size_; + 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 = static_cast<int>(buffer_->Read(read_size_, read_buffer_)); + offset_ += first_offset_ + read; + + // And report with what we have read. + DoneRead(read); +} + +void BufferedResourceLoader::DoneRead(int error) { + read_callback_->RunWithParams(Tuple1<int>(error)); + read_callback_.reset(); + read_position_ = 0; + read_size_ = 0; + read_buffer_ = NULL; + first_offset_ = 0; + last_offset_ = 0; +} + +void BufferedResourceLoader::DoneStart(int error) { + start_callback_->RunWithParams(Tuple1<int>(error)); + start_callback_.reset(); +} + +////////////////////////////////////////////////////////////////////////////// +// BufferedDataSource, protected +BufferedDataSource::BufferedDataSource( + MessageLoop* render_loop, + webkit_glue::MediaResourceLoaderBridgeFactory* bridge_factory) + : total_bytes_(kPositionNotSpecified), + bridge_factory_(bridge_factory), + loader_(NULL), + read_callback_(NULL), + read_position_(0), + read_size_(0), + read_buffer_(NULL), + intermediate_read_buffer_(new uint8[kInitialReadBufferSize]), + intermediate_read_buffer_size_(kInitialReadBufferSize), + render_loop_(render_loop), + initialize_callback_(NULL), + stopped_(false) { +} + +BufferedDataSource::~BufferedDataSource() { +} + +// A factory method to create BufferedResourceLoader using the read parameters. +// This method can be overrided to inject mock BufferedResourceLoader object +// for testing purpose. +BufferedResourceLoader* BufferedDataSource::CreateLoader( + int64 first_byte_position, int64 last_byte_position) { + DCHECK(MessageLoop::current() == render_loop_); + + return new BufferedResourceLoader(bridge_factory_.get(), url_, + first_byte_position, + last_byte_position); +} + +///////////////////////////////////////////////////////////////////////////// +// BufferedDataSource, media::MediaFilter implementation +void BufferedDataSource::Initialize(const std::string& url, + media::FilterCallback* callback) { + DCHECK(callback); + initialize_callback_.reset(callback); + + // Saves the url. + url_ = GURL(url); + + if (!IsSchemeSupported(url_)) { + host()->SetError(media::PIPELINE_ERROR_NETWORK); + DoneInitialization(); + return; + } + + media_format_.SetAsString(media::MediaFormat::kMimeType, + media::mime_type::kApplicationOctetStream); + media_format_.SetAsString(media::MediaFormat::kURL, url); + + // Post a task to complete the initialization task. + render_loop_->PostTask(FROM_HERE, + NewRunnableMethod(this, &BufferedDataSource::InitializeTask)); +} + +void BufferedDataSource::Stop() { + { + AutoLock auto_lock(lock_); + stopped_ = true; + } + render_loop_->PostTask(FROM_HERE, + NewRunnableMethod(this, &BufferedDataSource::StopTask)); +} + +///////////////////////////////////////////////////////////////////////////// +// BufferedDataSource, media::DataSource implementation +void BufferedDataSource::Read(int64 position, size_t size, + uint8* data, + media::DataSource::ReadCallback* read_callback) { + render_loop_->PostTask(FROM_HERE, + NewRunnableMethod(this, &BufferedDataSource::ReadTask, + position, static_cast<int>(size), data, read_callback)); +} + +bool BufferedDataSource::GetSize(int64* size_out) { + if (total_bytes_ != kPositionNotSpecified) { + *size_out = total_bytes_; + return true; + } + *size_out = 0; + return false; +} + +bool BufferedDataSource::IsSeekable() { + return total_bytes_ != kPositionNotSpecified; +} + +///////////////////////////////////////////////////////////////////////////// +// BufferedDataSource, render thread tasks +void BufferedDataSource::InitializeTask() { + DCHECK(MessageLoop::current() == render_loop_); + DCHECK(!loader_.get()); + + // Creates a new resource loader with the full range. + loader_.reset(CreateLoader(-1, -1)); + + // And then start the resource request. + loader_->Start(NewCallback(this, + &BufferedDataSource::InitializeStartCallback)); +} + +void BufferedDataSource::ReadTask( + int64 position, int read_size, uint8* buffer, + media::DataSource::ReadCallback* read_callback) { + DCHECK(MessageLoop::current() == render_loop_); + DCHECK(!read_callback_.get()); + DCHECK(read_callback); + + // Saves the read parameters. + read_position_ = position; + read_size_ = read_size; + read_callback_.reset(read_callback); + read_buffer_ = buffer; + + // Call to read internal to perform the actual read. + ReadInternal(); +} + +void BufferedDataSource::StopTask() { + DCHECK(MessageLoop::current() == render_loop_); + + // We just need to stop the loader, so it stops activity. + if (loader_.get()) { + loader_->Stop(); + loader_.reset(); + } +} + +void BufferedDataSource::SwapLoaderTask(BufferedResourceLoader* loader) { + DCHECK(MessageLoop::current() == render_loop_); + DCHECK(loader); + + loader_.reset(loader); + loader_->Start(NewCallback(this, + &BufferedDataSource::PartialReadStartCallback)); +} + +// This method is the place where actual read happens, |loader_| must be valid +// prior to make this method call. +void BufferedDataSource::ReadInternal() { + DCHECK(MessageLoop::current() == render_loop_); + DCHECK(loader_.get()); + + // First we prepare the intermediate read buffer for BufferedResourceLoader + // to write to. + if (read_size_ > intermediate_read_buffer_size_) { + intermediate_read_buffer_.reset(new uint8[read_size_]); + } + + // Perform the actual read with BufferedResourceLoader. + loader_->Read(read_position_, read_size_, + intermediate_read_buffer_.get(), + NewCallback(this, &BufferedDataSource::ReadCallback)); +} + +// Method to report the results of the current read request. Also reset all +// the read parameters. +void BufferedDataSource::DoneRead(int error) { + DCHECK(MessageLoop::current() == render_loop_); + lock_.AssertAcquired(); + + if (error >= 0) { + read_callback_->RunWithParams(Tuple1<size_t>(error)); + } else { + read_callback_->RunWithParams( + Tuple1<size_t>(static_cast<size_t>(media::DataSource::kReadError))); + } + + read_callback_.reset(); + read_position_ = 0; + read_size_ = 0; + read_buffer_ = 0; +} + +void BufferedDataSource::DoneInitialization() { + DCHECK(initialize_callback_.get()); + lock_.AssertAcquired(); + + initialize_callback_->Run(); + initialize_callback_.reset(); +} + +///////////////////////////////////////////////////////////////////////////// +// BufferedDataSource, callback methods. +// These methods are called on the render thread for the events reported by +// BufferedResourceLoader. +void BufferedDataSource::InitializeStartCallback(int error) { + DCHECK(MessageLoop::current() == render_loop_); + + // We need to prevent calling to filter host and running the callback if + // we have received the stop signal. We need to lock down the whole callback + // method to prevent bad things from happening. The reason behind this is + // that we cannot guarantee tasks on render thread have completely stopped + // when we receive the Stop() method call. The only way to solve this is to + // let tasks on render thread to run but make sure they don't call outside + // this object when Stop() method is ever called. Locking this method is safe + // because |lock_| is only acquired in tasks on render thread. + AutoLock auto_lock(lock_); + if (stopped_) + return; + + DCHECK(loader_.get()); + + if (error == net::OK) { + total_bytes_ = loader_->content_length(); + // TODO(hclam): Figure out what to do when total bytes is not known. + if (total_bytes_ >= 0) { + host()->SetTotalBytes(total_bytes_); + + // This value governs the range that we can seek to. + // TODO(hclam): Report the correct value of buffered bytes. + host()->SetBufferedBytes(total_bytes_); + } + } else { + // TODO(hclam): In case of failure, we can retry several times. + // Also it might be bad to access host() here. + host()->SetError(media::PIPELINE_ERROR_NETWORK); + + // Stops the loader, just to be safe. + loader_->Stop(); + } + + DoneInitialization(); +} + +void BufferedDataSource::PartialReadStartCallback(int error) { + DCHECK(MessageLoop::current() == render_loop_); + DCHECK(loader_.get()); + + if (error == net::OK) { + // Once the range request has start successfully, we can proceed with + // reading from it. + ReadInternal(); + } else { + // We need to prevent calling to filter host and running the callback if + // we have received the stop signal. We need to lock down the whole callback + // method to prevent bad things from happening. The reason behind this is + // that we cannot guarantee tasks on render thread have completely stopped + // when we receive the Stop() method call. So only way to solve this is to + // let tasks on render thread to run but make sure they don't call outside + // this object when Stop() method is ever called. Locking this method is + // safe because |lock_| is only acquired in tasks on render thread. + AutoLock auto_lock(lock_); + if (stopped_) + return; + + // TODO(hclam): It may be bad to access host() here. + host()->SetError(media::PIPELINE_ERROR_NETWORK); + + // Kill the loader just to be safe. + loader_->Stop(); + } +} + +void BufferedDataSource::ReadCallback(int error) { + DCHECK(MessageLoop::current() == render_loop_); + + // We need to prevent calling to filter host and running the callback if + // we have received the stop signal. We need to lock down the whole callback + // method to prevent bad things from happening. The reason behind this is + // that we cannot guarantee tasks on render thread have completely stopped + // when we receive the Stop() method call. So only way to solve this is to + // let tasks on render thread to run but make sure they don't call outside + // this object when Stop() method is ever called. Locking this method is safe + // because |lock_| is only acquired in tasks on render thread. + AutoLock auto_lock(lock_); + if (stopped_) + return; + + DCHECK(loader_.get()); + DCHECK(read_callback_.get()); + + if (error >= 0) { + // If a position error code is received, read was successful. So copy + // from intermediate read buffer to the target read buffer. + memcpy(read_buffer_, intermediate_read_buffer_.get(), error); + + DoneRead(error); + } else if (error == net::ERR_CACHE_MISS) { + // If the current loader cannot serve this read request, we need to create + // a new one. + // We have the following conditions: + // 1. Read is beyond the content length of the file (if known). + // 2. We have tried too many times (TODO here). + if (read_position_ >= total_bytes_) { + DoneRead(0); + return; + } + + // TODO(hclam): we need to count how many times it failed to prevent + // excessive trials. + + // Stops the current resource loader. + loader_->Stop(); + + // Since this method is called from the current buffered resource loader, + // we cannot delete it. So we need to post a task to swap in a new + // resource loader and starts it. + render_loop_->PostTask(FROM_HERE, + NewRunnableMethod(this, + &BufferedDataSource::SwapLoaderTask, + CreateLoader(read_position_, -1))); + } else { + // The read has finished with error. + DoneRead(error); + + // TODO(hclam): It may be bad to access host() here. + host()->SetError(media::PIPELINE_ERROR_NETWORK); + + // Stops the laoder. + loader_->Stop(); + } +} + +} // namespace webkit_glue diff --git a/webkit/glue/media/buffered_data_source.h b/webkit/glue/media/buffered_data_source.h new file mode 100644 index 0000000..5d6f1c3 --- /dev/null +++ b/webkit/glue/media/buffered_data_source.h @@ -0,0 +1,286 @@ +// Copyright (c) 2009 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. + +#ifndef WEBKIT_GLUE_MEDIA_BUFFERED_DATA_SOURCE_H_ +#define WEBKIT_GLUE_MEDIA_BUFFERED_DATA_SOURCE_H_ + +#include <string> + +#include "base/lock.h" +#include "base/scoped_ptr.h" +#include "base/condition_variable.h" +#include "googleurl/src/gurl.h" +#include "media/base/factory.h" +#include "media/base/filters.h" +#include "media/base/media_format.h" +#include "media/base/pipeline.h" +#include "media/base/seekable_buffer.h" +#include "net/base/completion_callback.h" +#include "net/base/file_stream.h" +#include "webkit/glue/media/media_resource_loader_bridge_factory.h" + +namespace webkit_glue { + +///////////////////////////////////////////////////////////////////////////// +// BufferedResourceLoader +// This class works inside demuxer thread and render thread. It contains a +// resource loader bridge and does the actual resource loading. This object +// does buffering internally, it defers the resource loading if buffer is +// full and un-defers the resource loading if it is under buffered. +class BufferedResourceLoader : public webkit_glue::ResourceLoaderBridge::Peer { + public: + // |bridge_factory| - Factory to create a ResourceLoaderBridge. + // |url| - URL for the resource to be loaded. + // |first_byte_position| - First byte to start loading from, -1 for not + // specified. + // |last_byte_position| - Last byte to be loaded, -1 for not specified. + BufferedResourceLoader( + webkit_glue::MediaResourceLoaderBridgeFactory* bridge_factory, + const GURL& url, + int64 first_byte_position, + int64 last_byte_position); + virtual ~BufferedResourceLoader(); + + // Start the resource loading with the specified URL and range. + // This method operates in asynchronous mode. Once there's a response from the + // server, success or fail |start_callback| is called with the result. + virtual void Start(net::CompletionCallback* callback); + + // Stop this loader, cancels and request and release internal buffer. + virtual void Stop(); + + // Reads the specified |read_size| from |position| into |buffer| and when + // the operation is done invoke |callback| with number of bytes read or an + // error code. + virtual void Read(int64 position, int read_size, + uint8* buffer, net::CompletionCallback* callback); + + // Gets the content length in bytes of the instance after this loader has been + // started. + virtual int64 content_length() { return content_length_; } + + ///////////////////////////////////////////////////////////////////////////// + // webkit_glue::ResourceLoaderBridge::Peer implementations. + virtual void OnUploadProgress(uint64 position, uint64 size) {} + virtual void OnReceivedRedirect(const GURL& new_url); + virtual void OnReceivedResponse( + const webkit_glue::ResourceLoaderBridge::ResponseInfo& info, + bool content_filtered); + virtual void OnReceivedData(const char* data, int len); + virtual void OnCompletedRequest(const URLRequestStatus& status, + const std::string& security_info); + std::string GetURLForDebugging() { return url_.spec(); } + + protected: + // An empty constructor so mock classes can be constructed. + BufferedResourceLoader() { + } + + private: + // Defer the resource loading if the buffer is full. + void EnableDeferIfNeeded(); + + // Disable defer loading if we are under-buffered. + void DisableDeferIfNeeded(); + + // Returns true if the current read request can be fulfilled by what is in + // the buffer. + bool CanFulfillRead(); + + // Returns true if the current read request will be fulfilled in the future. + bool WillFulfillRead(); + + // Checks parameters and make sure they are valid. + bool VerifyRead(); + + // Method that does the actual read and calls the |read_callbac_|, assuming + // the request range is in |buffer_|. + void ReadInternal(); + + // Done with read. Invokes the read callback and reset parameters for the + // read request. + void DoneRead(int error); + + // Done with start. Invokes the start callback and reset it. + void DoneStart(int error); + + bool HasPendingRead() { return read_callback_.get() != NULL; } + + // A sliding window of buffer. + scoped_ptr<media::SeekableBuffer> buffer_; + + bool deferred_; + bool completed_; + bool range_requested_; + + webkit_glue::MediaResourceLoaderBridgeFactory* bridge_factory_; + GURL url_; + int64 first_byte_position_; + int64 last_byte_position_; + + // Members used during request start. + scoped_ptr<net::CompletionCallback> start_callback_; + scoped_ptr<webkit_glue::ResourceLoaderBridge> bridge_; + int64 offset_; + int64 content_length_; + + // Members used during a read operation. They should be reset after each + // read has completed or failed. + scoped_ptr<net::CompletionCallback> read_callback_; + int64 read_position_; + int read_size_; + uint8* read_buffer_; + + // Offsets of the requested first byte and last byte in |buffer_|. They are + // written by VerifyRead(). + int first_offset_; + int last_offset_; + + DISALLOW_COPY_AND_ASSIGN(BufferedResourceLoader); +}; + +class BufferedDataSource : public media::DataSource { + public: + // Methods called from pipeline thread + // Static methods for creating this class. + static media::FilterFactory* CreateFactory( + MessageLoop* message_loop, + webkit_glue::MediaResourceLoaderBridgeFactory* bridge_factory) { + return new media::FilterFactoryImpl2< + BufferedDataSource, + MessageLoop*, + webkit_glue::MediaResourceLoaderBridgeFactory*>( + message_loop, bridge_factory); + } + + // media::MediaFilter implementation. + virtual void Initialize(const std::string& url, + media::FilterCallback* callback); + virtual void Stop(); + + // media::DataSource implementation. + // Called from demuxer thread. + virtual void Read(int64 position, size_t size, + uint8* data, + media::DataSource::ReadCallback* read_callback); + virtual bool GetSize(int64* size_out); + virtual bool IsSeekable(); + + const media::MediaFormat& media_format() { + return media_format_; + } + + protected: + BufferedDataSource( + MessageLoop* render_loop, + webkit_glue::MediaResourceLoaderBridgeFactory* bridge_factory); + virtual ~BufferedDataSource(); + + // A factory method to create a BufferedResourceLoader based on the read + // parameters. We can override this file to object a mock + // BufferedResourceLoader for testing. + virtual BufferedResourceLoader* CreateLoader(int64 first_byte_position, + int64 last_byte_position); + + private: + friend class media::FilterFactoryImpl2< + BufferedDataSource, + MessageLoop*, + webkit_glue::MediaResourceLoaderBridgeFactory*>; + + // Posted to perform initialization on render thread. + void InitializeTask(); + + // Task posted to perform resource loading and actual reading on the render + // thread. + void ReadTask(int64 position, int read_size, + uint8* read_buffer, + media::DataSource::ReadCallback* read_callback); + + // Task posted when Stop() is called. + void StopTask(); + + // Reset |loader_| with |loader| and starts it. This task is posted from + // callback method from the current buffered resource loader. + void SwapLoaderTask(BufferedResourceLoader* loader); + + // The method that performs actual read. This method can only be executed on + // the render thread. + void ReadInternal(); + + // Calls |read_callback_| and reset all read parameters. + void DoneRead(int error); + + // Calls |initialize_callback_| and reset it. + void DoneInitialization(); + + // Callback method to perform BufferedResourceLoader::Start() during + // initialization. + void InitializeStartCallback(int error); + + // Callback method to be passed to BufferedResourceLoader during range + // request. Once a resource request has started, this method will be called + // with the error code. This method will be executed on the thread + // BufferedResourceLoader lives, i.e. render thread. + void PartialReadStartCallback(int error); + + // Callback method for making a read request to BufferedResourceLoader. + // If data arrives or the request has failed, this method is called with + // the error code or the number of bytes read. + void ReadCallback(int error); + + media::MediaFormat media_format_; + + // URL of the resource requested. + GURL url_; + + // Members for total bytes of the requested object. It is written once on + // render thread but may be read from any thread. However reading of this + // member is guaranteed to happen after it is first written, so we don't + // need to protect it. + int64 total_bytes_; + + // A factory object to produce ResourceLoaderBridge. + scoped_ptr<webkit_glue::MediaResourceLoaderBridgeFactory> bridge_factory_; + + // A downloader object for loading the media resource. + scoped_ptr<BufferedResourceLoader> loader_; + + // Read parameters received from the Read() method call. + scoped_ptr<media::DataSource::ReadCallback> read_callback_; + int64 read_position_; + int read_size_; + uint8* read_buffer_; + + // This buffer is intermediate, we use it for BufferedResourceLoader to write + // to. And when read in BufferedResourceLoader is done, we copy data from + // this buffer to |read_buffer_|. The reason for an additional copy is that + // we don't own |read_buffer_|. But since the read operation is asynchronous, + // |read_buffer| can be destroyed at any time, so we only copy into + // |read_buffer| in the final step when it is safe. + // Memory is allocated for this member during initialization of this object + // because we want buffer to be passed into BufferedResourceLoader to be + // always non-null. And by initializing this member with a default size we can + // avoid creating zero-sized buffered if the first read has zero size. + scoped_array<uint8> intermediate_read_buffer_; + int intermediate_read_buffer_size_; + + // The message loop of the render thread. + MessageLoop* render_loop_; + + // Filter callbacks. + scoped_ptr<media::FilterCallback> initialize_callback_; + + // Protects |stopped_|. + Lock lock_; + + // Stop signal to suppressing activities. + bool stopped_; + + DISALLOW_COPY_AND_ASSIGN(BufferedDataSource); +}; + +} // namespace webkit_glue + +#endif // WEBKIT_GLUE_MEDIA_BUFFERED_DATA_SOURCE_H_ diff --git a/webkit/glue/media/buffered_data_source_unittest.cc b/webkit/glue/media/buffered_data_source_unittest.cc new file mode 100644 index 0000000..40c0b41 --- /dev/null +++ b/webkit/glue/media/buffered_data_source_unittest.cc @@ -0,0 +1,627 @@ +// Copyright (c) 2009 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 <algorithm> + +#include "base/string_util.h" +#include "media/base/filters.h" +#include "media/base/mock_filter_host.h" +#include "media/base/mock_filters.h" +#include "net/base/net_errors.h" +#include "net/http/http_response_headers.h" +#include "webkit/glue/media/buffered_data_source.h" +#include "webkit/glue/media/mock_media_resource_loader_bridge_factory.h" +#include "webkit/glue/mock_resource_loader_bridge.h" + +using ::testing::_; +using ::testing::Assign; +using ::testing::DoAll; +using ::testing::InSequence; +using ::testing::Invoke; +using ::testing::NotNull; +using ::testing::Return; +using ::testing::SetArgumentPointee; +using ::testing::StrictMock; +using ::testing::WithArgs; + +namespace { + +const char* kHttpUrl = "http://test"; +const int kDataSize = 1024; + +} // namespace + +namespace webkit_glue { + +class BufferedResourceLoaderTest : public testing::Test { + public: + BufferedResourceLoaderTest() { + bridge_.reset(new StrictMock<MockResourceLoaderBridge>()); + + for (int i = 0; i < kDataSize; ++i) + data_[i] = i; + } + + ~BufferedResourceLoaderTest() { + if (bridge_.get()) + EXPECT_CALL(*bridge_, OnDestroy()); + EXPECT_CALL(bridge_factory_, OnDestroy()); + } + + void Initialize(const char* url, int first_position, int last_position) { + gurl_ = GURL(url); + first_position_ = first_position; + last_position_ = last_position; + + loader_.reset(new BufferedResourceLoader(&bridge_factory_, gurl_, + first_position_, last_position_)); + EXPECT_EQ(gurl_.spec(), loader_->GetURLForDebugging()); + } + + void Start() { + InSequence s; + EXPECT_CALL(bridge_factory_, + CreateBridge(gurl_, _, first_position_, last_position_)) + .WillOnce(Return(bridge_.get())); + EXPECT_CALL(*bridge_, Start(loader_.get())); + loader_->Start(NewCallback(this, + &BufferedResourceLoaderTest::StartCallback)); + } + + void FullResponse(int64 content_length) { + EXPECT_CALL(*this, StartCallback(net::OK)); + ResourceLoaderBridge::ResponseInfo info; + std::string header = StringPrintf("HTTP/1.1 200 OK\n" + "Content-Length: %lld", content_length); + replace(header.begin(), header.end(), '\n', '\0'); + info.headers = new net::HttpResponseHeaders(header); + info.content_length = content_length; + loader_->OnReceivedResponse(info, false); + EXPECT_EQ(content_length, loader_->content_length()); + } + + void PartialResponse(int64 content_length) { + EXPECT_CALL(*this, StartCallback(net::OK)); + ResourceLoaderBridge::ResponseInfo info; + std::string header = StringPrintf("HTTP/1.1 206 Partial Content\n" + "Content-Range: bytes %lld-%lld/%lld", + first_position_, + last_position_, + content_length); + replace(header.begin(), header.end(), '\n', '\0'); + info.headers = new net::HttpResponseHeaders(header); + info.content_length = content_length; + loader_->OnReceivedResponse(info, false); + // TODO(hclam): Right now BufferedResourceLoader doesn't care about the + // partial range replied by the server. Do the check here. + } + + void StopWhenLoad() { + InSequence s; + EXPECT_CALL(*bridge_, Cancel()); + EXPECT_CALL(*bridge_, OnDestroy()) + .WillOnce(Invoke(this, &BufferedResourceLoaderTest::ReleaseBridge)); + loader_->Stop(); + } + + void ReleaseBridge() { + bridge_.release(); + } + + // Helper method to write to |loader_| from |data_|. + void WriteLoader(int position, int size) { + loader_->OnReceivedData(reinterpret_cast<char*>(data_ + position), size); + } + + // Helper method to read from |loader_|. + void ReadLoader(int64 position, int size, uint8* buffer) { + loader_->Read(position, size, buffer, + NewCallback(this, &BufferedResourceLoaderTest::ReadCallback)); + } + + // Verifis that data in buffer[0...size] is equal to data_[pos...pos+size]. + void VerifyBuffer(uint8* buffer, int pos, int size) { + EXPECT_EQ(0, memcmp(buffer, data_ + pos, size)); + } + + MOCK_METHOD1(StartCallback, void(int error)); + MOCK_METHOD1(ReadCallback, void(int error)); + + protected: + GURL gurl_; + int64 first_position_; + int64 last_position_; + + scoped_ptr<BufferedResourceLoader> loader_; + StrictMock<MockMediaResourceLoaderBridgeFactory> bridge_factory_; + scoped_ptr<StrictMock<MockResourceLoaderBridge> > bridge_; + + uint8 data_[kDataSize]; + + private: + DISALLOW_COPY_AND_ASSIGN(BufferedResourceLoaderTest); +}; + +TEST_F(BufferedResourceLoaderTest, StartStop) { + Initialize(kHttpUrl, -1, -1); + Start(); + StopWhenLoad(); +} + +// Tests that HTTP header is missing in the response. +TEST_F(BufferedResourceLoaderTest, MissingHttpHeader) { + Initialize(kHttpUrl, -1, -1); + Start(); + + EXPECT_CALL(*this, StartCallback(net::ERR_INVALID_RESPONSE)); + EXPECT_CALL(*bridge_, Cancel()); + EXPECT_CALL(*bridge_, OnDestroy()) + .WillOnce(Invoke(this, &BufferedResourceLoaderTest::ReleaseBridge)); + + ResourceLoaderBridge::ResponseInfo info; + loader_->OnReceivedResponse(info, false); +} + +// Tests that a bad HTTP response is recived, e.g. file not found. +TEST_F(BufferedResourceLoaderTest, BadHttpResponse) { + Initialize(kHttpUrl, -1, -1); + Start(); + + EXPECT_CALL(*this, StartCallback(net::ERR_FAILED)); + EXPECT_CALL(*bridge_, Cancel()); + EXPECT_CALL(*bridge_, OnDestroy()) + .WillOnce(Invoke(this, &BufferedResourceLoaderTest::ReleaseBridge)); + + ResourceLoaderBridge::ResponseInfo info; + info.headers = new net::HttpResponseHeaders("HTTP/1.1 404 Bot Found\n"); + loader_->OnReceivedResponse(info, false); +} + +// Tests that partial content is requested but not fulfilled. +TEST_F(BufferedResourceLoaderTest, NotPartialRange) { + Initialize(kHttpUrl, 100, -1); + Start(); + + EXPECT_CALL(*this, StartCallback(net::ERR_INVALID_RESPONSE)); + EXPECT_CALL(*bridge_, Cancel()); + EXPECT_CALL(*bridge_, OnDestroy()) + .WillOnce(Invoke(this, &BufferedResourceLoaderTest::ReleaseBridge)); + + ResourceLoaderBridge::ResponseInfo info; + info.headers = new net::HttpResponseHeaders("HTTP/1.1 200 OK\n"); + loader_->OnReceivedResponse(info, false); +} + +// Tests that a 200 response is received. +TEST_F(BufferedResourceLoaderTest, FullResponse) { + Initialize(kHttpUrl, -1, -1); + Start(); + FullResponse(1024); + StopWhenLoad(); +} + +// Tests that a partial content response is received. +TEST_F(BufferedResourceLoaderTest, PartialResponse) { + Initialize(kHttpUrl, 100, 200); + Start(); + PartialResponse(1024); + StopWhenLoad(); +} + +// Tests the logic of sliding window for data buffering and reading. +TEST_F(BufferedResourceLoaderTest, BufferAndRead) { + Initialize(kHttpUrl, 10, 29); + Start(); + PartialResponse(30); + + uint8 buffer[10]; + InSequence s; + + // Writes 10 bytes and read them back. + WriteLoader(10, 10); + EXPECT_CALL(*this, ReadCallback(10)); + ReadLoader(10, 10, buffer); + VerifyBuffer(buffer, 10, 10); + + // Writes 10 bytes and read 2 times. + WriteLoader(20, 10); + EXPECT_CALL(*this, ReadCallback(5)); + ReadLoader(20, 5, buffer); + VerifyBuffer(buffer, 20, 5); + EXPECT_CALL(*this, ReadCallback(5)); + ReadLoader(25, 5, buffer); + VerifyBuffer(buffer, 25, 5); + + // Read backward within buffer. + EXPECT_CALL(*this, ReadCallback(10)); + ReadLoader(10, 10, buffer); + VerifyBuffer(buffer, 10, 10); + + // Read backwith outside buffer. + EXPECT_CALL(*this, ReadCallback(net::ERR_CACHE_MISS)); + ReadLoader(9, 10, buffer); + + // Response has completed. + EXPECT_CALL(*bridge_, OnDestroy()) + .WillOnce(Invoke(this, &BufferedResourceLoaderTest::ReleaseBridge)); + URLRequestStatus status; + status.set_status(URLRequestStatus::SUCCESS); + loader_->OnCompletedRequest(status, ""); + + // Try to read 10 from position 25 will just return with 5 bytes. + EXPECT_CALL(*this, ReadCallback(5)); + ReadLoader(25, 10, buffer); + VerifyBuffer(buffer, 25, 5); + + // Try to read outside buffered range after request has completed. + EXPECT_CALL(*this, ReadCallback(net::ERR_CACHE_MISS)); + ReadLoader(5, 10, buffer); + EXPECT_CALL(*this, ReadCallback(net::ERR_CACHE_MISS)); + ReadLoader(30, 10, buffer); +} + +TEST_F(BufferedResourceLoaderTest, ReadOutsideBuffer) { + Initialize(kHttpUrl, 10, 0x00FFFFFF); + Start(); + PartialResponse(0x01000000); + + uint8 buffer[10]; + InSequence s; + + // Read very far aheard will get a cache miss. + EXPECT_CALL(*this, ReadCallback(net::ERR_CACHE_MISS)); + ReadLoader(0x00FFFFFF, 1, buffer); + + // The following call will not call ReadCallback() because it is waiting for + // data to arrive. + ReadLoader(10, 10, buffer); + + // Writing to loader will fulfill the read request. + EXPECT_CALL(*this, ReadCallback(10)); + WriteLoader(10, 20); + VerifyBuffer(buffer, 10, 10); + + // The following call cannot be fulfilled now. + ReadLoader(25, 10, buffer); + + EXPECT_CALL(*bridge_, OnDestroy()) + .WillOnce(Invoke(this, &BufferedResourceLoaderTest::ReleaseBridge)); + EXPECT_CALL(*this, ReadCallback(5)); + URLRequestStatus status; + status.set_status(URLRequestStatus::SUCCESS); + loader_->OnCompletedRequest(status, ""); +} + +TEST_F(BufferedResourceLoaderTest, RequestFailedWhenRead) { + Initialize(kHttpUrl, 10, 29); + Start(); + PartialResponse(30); + + uint8 buffer[10]; + InSequence s; + + ReadLoader(10, 10, buffer); + EXPECT_CALL(*bridge_, OnDestroy()) + .WillOnce(Invoke(this, &BufferedResourceLoaderTest::ReleaseBridge)); + EXPECT_CALL(*this, ReadCallback(net::ERR_FAILED)); + URLRequestStatus status; + status.set_status(URLRequestStatus::FAILED); + loader_->OnCompletedRequest(status, ""); +} + +// TODO(hclam): add unit test for defer loading. + +class MockBufferedResourceLoader : public BufferedResourceLoader { + public: + MockBufferedResourceLoader() : BufferedResourceLoader() { + } + + ~MockBufferedResourceLoader() { + OnDestroy(); + } + + MOCK_METHOD1(Start, void(net::CompletionCallback* read_callback)); + MOCK_METHOD0(Stop, void()); + MOCK_METHOD4(Read, void(int64 position, int read_size, uint8* buffer, + net::CompletionCallback* callback)); + MOCK_METHOD0(content_length, int64()); + MOCK_METHOD0(OnDestroy, void()); + + private: + DISALLOW_COPY_AND_ASSIGN(MockBufferedResourceLoader); +}; + +// A mock BufferedDataSource to inject mock BufferedResourceLoader through +// CreateLoader() method. +class MockBufferedDataSource : public BufferedDataSource { + public: + // Static methods for creating this class. + static media::FilterFactory* CreateFactory( + MessageLoop* message_loop, + MediaResourceLoaderBridgeFactory* bridge_factory) { + return new media::FilterFactoryImpl2< + MockBufferedDataSource, + MessageLoop*, + MediaResourceLoaderBridgeFactory*>(message_loop, + bridge_factory); + } + + MOCK_METHOD2(CreateLoader, BufferedResourceLoader*(int64 first_position, + int64 last_position)); + + protected: + MockBufferedDataSource( + MessageLoop* message_loop, + MediaResourceLoaderBridgeFactory* factory) + : BufferedDataSource(message_loop, factory) { + } + + private: + friend class media::FilterFactoryImpl2< + MockBufferedDataSource, + MessageLoop*, + MediaResourceLoaderBridgeFactory*>; + + DISALLOW_COPY_AND_ASSIGN(MockBufferedDataSource); +}; + +class BufferedDataSourceTest : public testing::Test { + public: + BufferedDataSourceTest() { + message_loop_.reset(MessageLoop::current()); + bridge_factory_.reset( + new StrictMock<MockMediaResourceLoaderBridgeFactory>()); + ReleaseLoader(); + factory_ = MockBufferedDataSource::CreateFactory(message_loop_.get(), + bridge_factory_.get()); + + // Prepare test data. + for (size_t i = 0; i < sizeof(data_); ++i) { + data_[i] = i; + } + } + + ~BufferedDataSourceTest() { + if (data_source_) { + // Expects bridge factory to be destroyed along with data source. + EXPECT_CALL(*bridge_factory_, OnDestroy()) + .WillOnce(Invoke(this, + &BufferedDataSourceTest::ReleaseBridgeFactory)); + } + + // We don't own the message loop so release it. + message_loop_.release(); + } + + void InitializeDataSource(const char* url, int error, int64 content_length) { + // Saves the url first. + gurl_ = GURL(url); + + media::MediaFormat url_format; + url_format.SetAsString(media::MediaFormat::kMimeType, + media::mime_type::kURL); + url_format.SetAsString(media::MediaFormat::kURL, url); + data_source_ = factory_->Create<MockBufferedDataSource>(url_format); + CHECK(data_source_); + + // There is no need to provide a message loop to data source. + data_source_->set_host(&host_); + + // Creates the first mock loader to be injected. + loader_.reset(new StrictMock<MockBufferedResourceLoader>()); + + InSequence s; + StrictMock<media::MockFilterCallback> callback; + EXPECT_CALL(*data_source_, CreateLoader(-1, -1)) + .WillOnce(Return(loader_.get())); + EXPECT_CALL(*loader_, Start(NotNull())) + .WillOnce(DoAll(Assign(&error_, error), + Invoke(this, + &BufferedDataSourceTest::InvokeStartCallback))); + if (error != net::OK) { + EXPECT_CALL(host_, SetError(media::PIPELINE_ERROR_NETWORK)); + EXPECT_CALL(*loader_, Stop()); + } else { + EXPECT_CALL(*loader_, content_length()) + .WillOnce(Return(content_length)); + EXPECT_CALL(host_, SetTotalBytes(content_length)); + EXPECT_CALL(host_, SetBufferedBytes(content_length)); + } + EXPECT_CALL(callback, OnFilterCallback()); + EXPECT_CALL(callback, OnCallbackDestroyed()); + + data_source_->Initialize(url, callback.NewCallback()); + message_loop_->RunAllPending(); + + if (error == net::OK) { + int64 size; + EXPECT_TRUE(data_source_->GetSize(&size)); + EXPECT_EQ(content_length, size); + } + } + + void StopDataSource() { + if (loader_.get()) { + InSequence s; + EXPECT_CALL(*loader_, Stop()); + EXPECT_CALL(*loader_, OnDestroy()) + .WillOnce(Invoke(this, &BufferedDataSourceTest::ReleaseLoader)); + } + + data_source_->Stop(); + message_loop_->RunAllPending(); + } + + void ReleaseBridgeFactory() { + bridge_factory_.release(); + } + + void ReleaseLoader() { + loader_.release(); + } + + void InvokeStartCallback(net::CompletionCallback* callback) { + callback->RunWithParams(Tuple1<int>(error_)); + delete callback; + } + + void InvokeReadCallback(int64 position, int size, uint8* buffer, + net::CompletionCallback* callback) { + if (error_ > 0) + memcpy(buffer, data_ + static_cast<int>(position), error_); + callback->RunWithParams(Tuple1<int>(error_)); + delete callback; + } + + void ReadDataSourceHit(int64 position, int size, int read_size) { + EXPECT_TRUE(loader_.get() != NULL); + + InSequence s; + // Expect the read is delegated to the resource loader. + EXPECT_CALL(*loader_, Read(position, size, NotNull(), NotNull())) + .WillOnce(DoAll(Assign(&error_, read_size), + Invoke(this, + &BufferedDataSourceTest::InvokeReadCallback))); + + // The read has succeeded, so read callback will be called. + EXPECT_CALL(*this, ReadCallback(read_size)); + + data_source_->Read( + position, size, buffer_, + NewCallback(this, &BufferedDataSourceTest::ReadCallback)); + message_loop_->RunAllPending(); + + // Make sure data is correct. + EXPECT_EQ(0, + memcmp(buffer_, data_ + static_cast<int>(position), read_size)); + } + + void ReadDataSourceMiss(int64 position, int size) { + EXPECT_TRUE(loader_.get() != NULL); + + InSequence s; + // 1. Reply with a cache miss for the read. + EXPECT_CALL(*loader_, Read(position, size, NotNull(), NotNull())) + .WillOnce(DoAll(Assign(&error_, net::ERR_CACHE_MISS), + Invoke(this, + &BufferedDataSourceTest::InvokeReadCallback))); + + // 2. Then the current loader will be stop and destroyed. + StrictMock<MockBufferedResourceLoader> *new_loader = + new StrictMock<MockBufferedResourceLoader>(); + EXPECT_CALL(*loader_, Stop()); + EXPECT_CALL(*data_source_, CreateLoader(position, -1)) + .WillOnce(Return(new_loader)); + EXPECT_CALL(*loader_, OnDestroy()) + .WillOnce(Invoke(this, &BufferedDataSourceTest::ReleaseLoader)); + + // 3. Then the new loader will be started. + EXPECT_CALL(*new_loader, Start(NotNull())) + .WillOnce(DoAll(Assign(&error_, net::OK), + Invoke(this, + &BufferedDataSourceTest::InvokeStartCallback))); + + // 4. Then again a read request is made to the new loader. + EXPECT_CALL(*new_loader, Read(position, size, NotNull(), NotNull())) + .WillOnce(DoAll(Assign(&error_, size), + Invoke(this, + &BufferedDataSourceTest::InvokeReadCallback))); + + EXPECT_CALL(*this, ReadCallback(size)); + + data_source_->Read( + position, size, buffer_, + NewCallback(this, &BufferedDataSourceTest::ReadCallback)); + message_loop_->RunAllPending(); + + // Make sure data is correct. + EXPECT_EQ(0, memcmp(buffer_, data_ + static_cast<int>(position), size)); + + EXPECT_TRUE(loader_.get() == NULL); + loader_.reset(new_loader); + } + + void ReadDataSourceFailed(int64 position, int size, int error) { + EXPECT_TRUE(loader_.get() != NULL); + + InSequence s; + // 1. Expect the read is delegated to the resource loader. + EXPECT_CALL(*loader_, Read(position, size, NotNull(), NotNull())) + .WillOnce(DoAll(Assign(&error_, error), + Invoke(this, + &BufferedDataSourceTest::InvokeReadCallback))); + + // 2. The read has failed, so read callback will be called. + EXPECT_CALL(*this, ReadCallback(media::DataSource::kReadError)); + + // 3. Host will then receive an error. + EXPECT_CALL(host_, SetError(media::PIPELINE_ERROR_NETWORK)); + + // 4. The the loader is destroyed. + EXPECT_CALL(*loader_, Stop()); + + data_source_->Read( + position, size, buffer_, + NewCallback(this, &BufferedDataSourceTest::ReadCallback)); + + message_loop_->RunAllPending(); + } + + MOCK_METHOD1(ReadCallback, void(size_t size)); + + scoped_ptr<StrictMock<MockMediaResourceLoaderBridgeFactory> > + bridge_factory_; + scoped_ptr<StrictMock<MockBufferedResourceLoader> > loader_; + scoped_refptr<MockBufferedDataSource > data_source_; + scoped_refptr<media::FilterFactory> factory_; + + StrictMock<media::MockFilterHost> host_; + GURL gurl_; + scoped_ptr<MessageLoop> message_loop_; + + int error_; + uint8 buffer_[1024]; + uint8 data_[1024]; + + private: + DISALLOW_COPY_AND_ASSIGN(BufferedDataSourceTest); +}; + +TEST_F(BufferedDataSourceTest, InitializationSuccess) { + InitializeDataSource(kHttpUrl, net::OK, 1024); + StopDataSource(); +} + +TEST_F(BufferedDataSourceTest, InitiailizationFailed) { + InitializeDataSource(kHttpUrl, net::ERR_FILE_NOT_FOUND, 0); + StopDataSource(); +} + +TEST_F(BufferedDataSourceTest, ReadCacheHit) { + InitializeDataSource(kHttpUrl, net::OK, 25); + + // Performs read with cache hit. + ReadDataSourceHit(10, 10, 10); + + // Performs read with cache hit but partially filled. + ReadDataSourceHit(20, 10, 5); + + StopDataSource(); +} + +TEST_F(BufferedDataSourceTest, ReadCacheMiss) { + InitializeDataSource(kHttpUrl, net::OK, 1024); + ReadDataSourceMiss(1000, 10); + ReadDataSourceMiss(20, 10); + StopDataSource(); +} + +TEST_F(BufferedDataSourceTest, ReadFailed) { + InitializeDataSource(kHttpUrl, net::OK, 1024); + ReadDataSourceHit(10, 10, 10); + ReadDataSourceFailed(10, 10, net::ERR_CONNECTION_RESET); + StopDataSource(); +} + +} // namespace webkit_glue diff --git a/webkit/glue/media/media_resource_loader_bridge_factory.h b/webkit/glue/media/media_resource_loader_bridge_factory.h index 80c0ed2..453f655 100644 --- a/webkit/glue/media/media_resource_loader_bridge_factory.h +++ b/webkit/glue/media/media_resource_loader_bridge_factory.h @@ -38,6 +38,11 @@ class MediaResourceLoaderBridgeFactory { int64 first_byte_position, int64 last_byte_position); + protected: + // An empty constructor only used by inherited classes. + MediaResourceLoaderBridgeFactory() { + } + private: FRIEND_TEST(MediaResourceLoaderBridgeFactoryTest, GenerateHeaders); diff --git a/webkit/glue/media/mock_media_resource_loader_bridge_factory.h b/webkit/glue/media/mock_media_resource_loader_bridge_factory.h new file mode 100644 index 0000000..7bb27fe --- /dev/null +++ b/webkit/glue/media/mock_media_resource_loader_bridge_factory.h @@ -0,0 +1,36 @@ +// Copyright (c) 2009 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. + +#ifndef WEBKIT_GLUE_MOCK_RESOURCE_LOADER_BRIDGE_H_ +#define WEBKIT_GLUE_MOCK_RESOURCE_LOADER_BRIDGE_H_ + +#include "testing/gmock/include/gmock/gmock.h" +#include "webkit/glue/media/media_resource_loader_bridge_factory.h" + +namespace webkit_glue { + +class MockMediaResourceLoaderBridgeFactory + : public webkit_glue::MediaResourceLoaderBridgeFactory { + public: + MockMediaResourceLoaderBridgeFactory() { + } + + virtual ~MockMediaResourceLoaderBridgeFactory() { + OnDestroy(); + } + + MOCK_METHOD4(CreateBridge, + webkit_glue::ResourceLoaderBridge*(const GURL& url, + int load_flags, + int64 first_byte_position, + int64 last_byte_position)); + MOCK_METHOD0(OnDestroy, void()); + + private: + DISALLOW_COPY_AND_ASSIGN(MockMediaResourceLoaderBridgeFactory); +}; + +} // namespace webkit_glue + +#endif // WEBKIT_GLUE_MEDIA_MOCK_RESOURCE_LOADER_BRIDGE_H_ diff --git a/webkit/glue/media/simple_data_source.cc b/webkit/glue/media/simple_data_source.cc index f426a14..27d286e 100644 --- a/webkit/glue/media/simple_data_source.cc +++ b/webkit/glue/media/simple_data_source.cc @@ -36,7 +36,6 @@ SimpleDataSource::SimpleDataSource( : render_loop_(render_loop), bridge_factory_(bridge_factory), size_(-1), - position_(0), state_(UNINITIALIZED) { DCHECK(render_loop); } @@ -81,24 +80,20 @@ const media::MediaFormat& SimpleDataSource::media_format() { return media_format_; } -size_t SimpleDataSource::Read(uint8* data, size_t size) { +void SimpleDataSource::Read(int64 position, + size_t size, + uint8* data, + ReadCallback* read_callback) { DCHECK_GE(size_, 0); - size_t copied = std::min(size, static_cast<size_t>(size_ - position_)); - memcpy(data, data_.c_str() + position_, copied); - position_ += copied; - return copied; -} - -bool SimpleDataSource::GetPosition(int64* position_out) { - *position_out = position_; - return true; -} - -bool SimpleDataSource::SetPosition(int64 position) { - if (position < 0 || position > size_) - return false; - position_ = position; - return true; + if (position >= size_) { + read_callback->RunWithParams(Tuple1<size_t>(0)); + delete read_callback; + } else { + size_t copied = std::min(size, static_cast<size_t>(size_ - position)); + memcpy(data, data_.c_str() + position, copied); + read_callback->RunWithParams(Tuple1<size_t>(copied)); + delete read_callback; + } } bool SimpleDataSource::GetSize(int64* size_out) { diff --git a/webkit/glue/media/simple_data_source.h b/webkit/glue/media/simple_data_source.h index 8a63df4..1d15389 100644 --- a/webkit/glue/media/simple_data_source.h +++ b/webkit/glue/media/simple_data_source.h @@ -41,9 +41,8 @@ class SimpleDataSource : public media::DataSource, virtual void Initialize(const std::string& url, media::FilterCallback* callback); virtual const media::MediaFormat& media_format(); - virtual size_t Read(uint8* data, size_t size); - virtual bool GetPosition(int64* position_out); - virtual bool SetPosition(int64 position); + virtual void Read(int64 position, size_t size, + uint8* data, ReadCallback* read_callback); virtual bool GetSize(int64* size_out); virtual bool IsSeekable(); @@ -91,7 +90,6 @@ class SimpleDataSource : public media::DataSource, GURL url_; std::string data_; int64 size_; - int64 position_; // Simple state tracking variable. enum State { diff --git a/webkit/glue/media/simple_data_source_unittest.cc b/webkit/glue/media/simple_data_source_unittest.cc new file mode 100644 index 0000000..9d367aa --- /dev/null +++ b/webkit/glue/media/simple_data_source_unittest.cc @@ -0,0 +1,246 @@ +// Copyright (c) 2009 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/base/filters.h" +#include "media/base/mock_filter_host.h" +#include "media/base/mock_filters.h" +#include "webkit/glue/media/mock_media_resource_loader_bridge_factory.h" +#include "webkit/glue/media/simple_data_source.h" +#include "webkit/glue/mock_resource_loader_bridge.h" + +using ::testing::_; +using ::testing::DoAll; +using ::testing::InSequence; +using ::testing::Invoke; +using ::testing::NotNull; +using ::testing::Return; +using ::testing::SetArgumentPointee; +using ::testing::StrictMock; +using ::testing::WithArgs; + +namespace { + +const int kDataSize = 1024; +const char kHttpUrl[] = "http://test"; +const char kFtpUrl[] = "ftp://test"; +const char kHttpsUrl[] = "https://test"; +const char kFileUrl[] = "file://test"; +const char kInvalidUrl[] = "whatever://test"; + +} // namespace + +namespace webkit_glue { + +class SimpleDataSourceTest : public testing::Test { + public: + SimpleDataSourceTest() { + bridge_factory_.reset( + new StrictMock<MockMediaResourceLoaderBridgeFactory>()); + bridge_.reset(new StrictMock<MockResourceLoaderBridge>()); + factory_ = SimpleDataSource::CreateFactory(MessageLoop::current(), + bridge_factory_.get()); + + for (int i = 0; i < kDataSize; ++i) { + data_[i] = i; + } + } + + virtual ~SimpleDataSourceTest() { + if (bridge_.get()) { + EXPECT_CALL(*bridge_, OnDestroy()); + } + } + + void InitializeDataSource(const char* url) { + // Saves the url first. + gurl_ = GURL(url); + + media::MediaFormat url_format; + url_format.SetAsString(media::MediaFormat::kMimeType, + media::mime_type::kURL); + url_format.SetAsString(media::MediaFormat::kURL, url); + data_source_ = factory_->Create<SimpleDataSource>(url_format); + CHECK(data_source_); + + // There is no need to provide a message loop to data source. + data_source_->set_host(&host_); + + // First a bridge is created. + InSequence s; + EXPECT_CALL(*bridge_factory_, CreateBridge(gurl_, _, -1, -1)) + .WillOnce(Return(bridge_.get())); + EXPECT_CALL(*bridge_, Start(data_source_.get())) + .WillOnce(Return(true)); + + // TODO(hclam): need to add expectations to initialization callback. + data_source_->Initialize(url, callback_.NewCallback()); + + MessageLoop::current()->RunAllPending(); + } + + void RequestSucceeded() { + ResourceLoaderBridge::ResponseInfo info; + info.content_length = kDataSize; + + data_source_->OnReceivedResponse(info, false); + int64 size; + EXPECT_TRUE(data_source_->GetSize(&size)); + EXPECT_EQ(kDataSize, size); + + for (int i = 0; i < kDataSize; ++i) + data_source_->OnReceivedData(data_ + i, 1); + + InSequence s; + EXPECT_CALL(*bridge_, OnDestroy()) + .WillOnce(Invoke(this, &SimpleDataSourceTest::ReleaseBridge)); + EXPECT_CALL(host_, SetTotalBytes(kDataSize)); + EXPECT_CALL(host_, SetBufferedBytes(kDataSize)); + EXPECT_CALL(callback_, OnFilterCallback()); + EXPECT_CALL(callback_, OnCallbackDestroyed()); + + URLRequestStatus status; + status.set_status(URLRequestStatus::SUCCESS); + status.set_os_error(0); + data_source_->OnCompletedRequest(status, ""); + + // Let the tasks to be executed. + MessageLoop::current()->RunAllPending(); + } + + void RequestFailed() { + InSequence s; + EXPECT_CALL(*bridge_, OnDestroy()) + .WillOnce(Invoke(this, &SimpleDataSourceTest::ReleaseBridge)); + EXPECT_CALL(host_, SetError(media::PIPELINE_ERROR_NETWORK)); + EXPECT_CALL(callback_, OnFilterCallback()); + EXPECT_CALL(callback_, OnCallbackDestroyed()); + + URLRequestStatus status; + status.set_status(URLRequestStatus::FAILED); + status.set_os_error(100); + data_source_->OnCompletedRequest(status, ""); + + // Let the tasks to be executed. + MessageLoop::current()->RunAllPending(); + } + + void DestroyDataSource() { + EXPECT_CALL(*bridge_factory_, OnDestroy()) + .WillOnce(Invoke(this, &SimpleDataSourceTest::ReleaseBridgeFactory)); + + data_source_->Stop(); + MessageLoop::current()->RunAllPending(); + + data_source_ = NULL; + } + + void AsyncRead() { + for (int i = 0; i < kDataSize; ++i) { + uint8 buffer[1]; + + EXPECT_CALL(*this, ReadCallback(1)); + data_source_->Read( + i, 1, buffer, + NewCallback(this, &SimpleDataSourceTest::ReadCallback)); + EXPECT_EQ(static_cast<uint8>(data_[i]), buffer[0]); + } + } + + void ReleaseBridge() { + bridge_.release(); + } + + void ReleaseBridgeFactory() { + bridge_factory_.release(); + } + + MOCK_METHOD1(ReadCallback, void(size_t size)); + + protected: + scoped_ptr<MessageLoop> message_loop_; + scoped_ptr<StrictMock<MockMediaResourceLoaderBridgeFactory> > bridge_factory_; + scoped_ptr<StrictMock<MockResourceLoaderBridge> > bridge_; + scoped_refptr<media::FilterFactory> factory_; + scoped_refptr<SimpleDataSource> data_source_; + StrictMock<media::MockFilterHost> host_; + StrictMock<media::MockFilterCallback> callback_; + GURL gurl_; + char data_[kDataSize]; + + DISALLOW_COPY_AND_ASSIGN(SimpleDataSourceTest); +}; + +TEST_F(SimpleDataSourceTest, InitializeHTTP) { + InitializeDataSource(kHttpUrl); + RequestSucceeded(); + DestroyDataSource(); +} + +TEST_F(SimpleDataSourceTest, InitializeHTTPS) { + InitializeDataSource(kHttpsUrl); + RequestSucceeded(); + DestroyDataSource(); +} + +TEST_F(SimpleDataSourceTest, InitializeFTP) { + InitializeDataSource(kFtpUrl); + RequestSucceeded(); + DestroyDataSource(); +} + +TEST_F(SimpleDataSourceTest, InitializeFile) { + InitializeDataSource(kFileUrl); + RequestSucceeded(); + DestroyDataSource(); +} + +TEST_F(SimpleDataSourceTest, InitializeInvalid) { + StrictMock<media::MockFilterCallback> callback; + media::MediaFormat url_format; + url_format.SetAsString(media::MediaFormat::kMimeType, + media::mime_type::kURL); + url_format.SetAsString(media::MediaFormat::kURL, kInvalidUrl); + data_source_ = factory_->Create<SimpleDataSource>(url_format); + CHECK(data_source_); + + // There is no need to provide a message loop to data source. + data_source_->set_host(&host_); + + EXPECT_CALL(host_, SetError(media::PIPELINE_ERROR_NETWORK)); + EXPECT_CALL(callback, OnFilterCallback()); + EXPECT_CALL(callback, OnCallbackDestroyed()); + + data_source_->Initialize(kInvalidUrl, callback.NewCallback()); + data_source_->Stop(); + MessageLoop::current()->RunAllPending(); + + EXPECT_CALL(*bridge_factory_, OnDestroy()) + .WillOnce(Invoke(this, &SimpleDataSourceTest::ReleaseBridgeFactory)); + data_source_ = NULL; +} + +TEST_F(SimpleDataSourceTest, RequestFailed) { + InitializeDataSource(kHttpUrl); + RequestFailed(); + DestroyDataSource(); +} + +TEST_F(SimpleDataSourceTest, StopWhenDownloading) { + InitializeDataSource(kHttpUrl); + + EXPECT_CALL(*bridge_, Cancel()); + EXPECT_CALL(*bridge_, OnDestroy()) + .WillOnce(Invoke(this, &SimpleDataSourceTest::ReleaseBridge)); + EXPECT_CALL(callback_, OnCallbackDestroyed()); + DestroyDataSource(); +} + +TEST_F(SimpleDataSourceTest, AsyncRead) { + InitializeDataSource(kFileUrl); + RequestSucceeded(); + AsyncRead(); + DestroyDataSource(); +} + +} // namespace webkit_glue |