diff options
author | Ben Murdoch <benm@google.com> | 2010-07-29 17:14:53 +0100 |
---|---|---|
committer | Ben Murdoch <benm@google.com> | 2010-08-04 14:29:45 +0100 |
commit | c407dc5cd9bdc5668497f21b26b09d988ab439de (patch) | |
tree | 7eaf8707c0309516bdb042ad976feedaf72b0bb1 /webkit/glue/media | |
parent | 0998b1cdac5733f299c12d88bc31ef9c8035b8fa (diff) | |
download | external_chromium-c407dc5cd9bdc5668497f21b26b09d988ab439de.zip external_chromium-c407dc5cd9bdc5668497f21b26b09d988ab439de.tar.gz external_chromium-c407dc5cd9bdc5668497f21b26b09d988ab439de.tar.bz2 |
Merge Chromium src@r53293
Change-Id: Ia79acf8670f385cee48c45b0a75371d8e950af34
Diffstat (limited to 'webkit/glue/media')
-rw-r--r-- | webkit/glue/media/buffered_data_source.cc | 1052 | ||||
-rw-r--r-- | webkit/glue/media/buffered_data_source.h | 413 | ||||
-rw-r--r-- | webkit/glue/media/buffered_data_source_unittest.cc | 934 | ||||
-rw-r--r-- | webkit/glue/media/media_resource_loader_bridge_factory.cc | 76 | ||||
-rw-r--r-- | webkit/glue/media/media_resource_loader_bridge_factory.h | 76 | ||||
-rw-r--r-- | webkit/glue/media/media_resource_loader_bridge_factory_unittest.cc | 44 | ||||
-rw-r--r-- | webkit/glue/media/mock_media_resource_loader_bridge_factory.h | 36 | ||||
-rw-r--r-- | webkit/glue/media/simple_data_source.cc | 236 | ||||
-rw-r--r-- | webkit/glue/media/simple_data_source.h | 125 | ||||
-rw-r--r-- | webkit/glue/media/simple_data_source_unittest.cc | 252 | ||||
-rw-r--r-- | webkit/glue/media/video_renderer_impl.cc | 321 | ||||
-rw-r--r-- | webkit/glue/media/video_renderer_impl.h | 122 | ||||
-rw-r--r-- | webkit/glue/media/web_video_renderer.h | 39 |
13 files changed, 3726 insertions, 0 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..dfac588 --- /dev/null +++ b/webkit/glue/media/buffered_data_source.cc @@ -0,0 +1,1052 @@ +// Copyright (c) 2010 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/callback.h" +#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 "media/base/filter_host.h" +#include "media/base/media_format.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/webkit_glue.h" + +namespace { + +const char kHttpScheme[] = "http"; +const char kHttpsScheme[] = "https"; +const char kDataScheme[] = "data"; +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 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): Set it to 5s, calibrate this value later. +const int kTimeoutMilliseconds = 5000; + +// 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. +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; + +// Returns true if |url| operates on HTTP protocol. +bool IsHttpProtocol(const GURL& url) { + return url.SchemeIs(kHttpScheme) || url.SchemeIs(kHttpsScheme); +} + +bool IsDataProtocol(const GURL& url) { + return url.SchemeIs(kDataScheme); +} + +} // 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), + defer_allowed_(true), + completed_(false), + range_requested_(false), + partial_response_(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), + instance_size_(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, + NetworkEventCallback* event_callback) { + // Make sure we have not started. + DCHECK(!bridge_.get()); + DCHECK(!start_callback_.get()); + DCHECK(!event_callback_.get()); + DCHECK(start_callback); + DCHECK(event_callback); + + start_callback_.reset(start_callback); + event_callback_.reset(event_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_, + IsMediaCacheEnabled() ? net::LOAD_NORMAL : net::LOAD_BYPASS_CACHE, + first_byte_position_, + last_byte_position_)); + + // Increment the reference count right before we start the request. This + // reference will be release when this request has ended. + AddRef(); + + // And start the resource loading. + bridge_->Start(this); +} + +void BufferedResourceLoader::Stop() { + // Reset callbacks. + start_callback_.reset(); + event_callback_.reset(); + read_callback_.reset(); + + // Use the internal buffer to signal that we have been stopped. + // TODO(hclam): Not so pretty to do this. + if (!buffer_.get()) + return; + + // Destroy internal buffer. + buffer_.reset(); + + if (bridge_.get()) { + // Cancel the request. This method call will cancel the request + // asynchronously. We may still get data or messages until we receive + // a response completed message. + if (deferred_) + bridge_->SetDefersLoading(false); + deferred_ = false; + bridge_->Cancel(); + } +} + +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); + + // Save the parameter of reading. + read_callback_.reset(read_callback); + read_position_ = position; + read_size_ = read_size; + read_buffer_ = buffer; + + // If read position is beyond the instance size, we cannot read there. + if (instance_size_ != kPositionNotSpecified && + instance_size_ <= read_position_) { + DoneRead(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(net::ERR_CACHE_MISS); + return; + } + + // Prepare the parameters. + first_offset_ = static_cast<int>(read_position_ - offset_); + last_offset_ = first_offset_ + read_size_; + + // 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); +} + +int64 BufferedResourceLoader::GetBufferedFirstBytePosition() { + if (buffer_.get()) + return offset_ - static_cast<int>(buffer_->backward_bytes()); + return kPositionNotSpecified; +} + +int64 BufferedResourceLoader::GetBufferedLastBytePosition() { + if (buffer_.get()) + return offset_ + static_cast<int>(buffer_->forward_bytes()) - 1; + return kPositionNotSpecified; +} + +void BufferedResourceLoader::SetAllowDefer(bool is_allowed) { + defer_allowed_ = is_allowed; + DisableDeferIfNeeded(); +} + +///////////////////////////////////////////////////////////////////////////// +// BufferedResourceLoader, +// webkit_glue::ResourceLoaderBridge::Peer implementations +bool BufferedResourceLoader::OnReceivedRedirect( + const GURL& new_url, + const webkit_glue::ResourceLoaderBridge::ResponseInfo& info, + bool* has_new_first_party_for_cookies, + GURL* new_first_party_for_cookies) { + DCHECK(bridge_.get()); + + // Save the new URL. + url_ = new_url; + // TODO(wtc): should we return a new first party for cookies URL? + *has_new_first_party_for_cookies = false; + + // The load may have been stopped and |start_callback| is destroyed. + // In this case we shouldn't do anything. + if (!start_callback_.get()) + return true; + + if (!IsProtocolSupportedForMedia(new_url)) { + DoneStart(net::ERR_ADDRESS_INVALID); + Stop(); + return false; + } + return true; +} + +void BufferedResourceLoader::OnReceivedResponse( + const webkit_glue::ResourceLoaderBridge::ResponseInfo& info, + bool content_filtered) { + DCHECK(bridge_.get()); + + // The loader may have been stopped and |start_callback| is destroyed. + // In this case we shouldn't do anything. + if (!start_callback_.get()) + return; + + // 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 (IsHttpProtocol(url_)) { + 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 (info.headers->response_code() == kHttpPartialContent) + partial_response_ = true; + + if (range_requested_ && partial_response_) { + // If we have verified the partial response and it is correct, we will + // return net::OK. + if (!VerifyPartialResponse(info)) + 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; + } + } else { + // For any protocol other than HTTP and HTTPS, assume range request is + // always fulfilled. + partial_response_ = range_requested_; + } + + // |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; + + // If we have not requested a range, then the size of the instance is equal + // to the content length. + if (!partial_response_) + instance_size_ = content_length_; + + // Calls with a successful response. + DoneStart(net::OK); +} + +void BufferedResourceLoader::OnReceivedData(const char* data, int len) { + DCHECK(bridge_.get()); + + // If this loader has been stopped, |buffer_| would be destroyed. + // In this case we shouldn't do anything. + if (!buffer_.get()) + return; + + // Writes more data to |buffer_|. + buffer_->Append(reinterpret_cast<const uint8*>(data), len); + + // If there is an active read request, try to fulfill the request. + if (HasPendingRead() && CanFulfillRead()) { + ReadInternal(); + } else if (!defer_allowed_) { + // If we're not allowed to defer, slide the buffer window forward instead + // of deferring. + if (buffer_->forward_bytes() > buffer_->forward_capacity()) { + size_t excess = buffer_->forward_bytes() - buffer_->forward_capacity(); + bool success = buffer_->Seek(excess); + DCHECK(success); + offset_ += first_offset_ + excess; + } + } + + // At last see if the buffer is full and we need to defer the downloading. + EnableDeferIfNeeded(); + + // Notify that we have received some data. + NotifyNetworkEvent(); +} + +void BufferedResourceLoader::OnCompletedRequest( + const URLRequestStatus& status, const std::string& security_info) { + DCHECK(bridge_.get()); + + // Saves the information that the request has completed. + completed_ = true; + + // 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()) { + // Make sure we have a valid buffer before we satisfy a read request. + DCHECK(buffer_.get()); + + if (status.is_success()) { + // Try to fulfill with what is in the buffer. + if (CanFulfillRead()) + ReadInternal(); + else + DoneRead(net::ERR_CACHE_MISS); + } else { + // If the request has failed, then fail the read. + DoneRead(net::ERR_FAILED); + } + } + + // There must not be any outstanding read request. + DCHECK(!HasPendingRead()); + + // Notify that network response is completed. + NotifyNetworkEvent(); + + // We incremented the reference count when the loader was started. We balance + // that reference here so that we get destroyed. This is also the only safe + // place to destroy the ResourceLoaderBridge. + bridge_.reset(); + Release(); +} + +///////////////////////////////////////////////////////////////////////////// +// BufferedResourceLoader, private +void BufferedResourceLoader::EnableDeferIfNeeded() { + if (!defer_allowed_) + return; + + if (!deferred_ && + buffer_->forward_bytes() >= buffer_->forward_capacity()) { + deferred_ = true; + + if (bridge_.get()) + bridge_->SetDefersLoading(true); + + NotifyNetworkEvent(); + } +} + +void BufferedResourceLoader::DisableDeferIfNeeded() { + if (deferred_ && + (!defer_allowed_ || + buffer_->forward_bytes() < buffer_->forward_capacity() / 2)) { + deferred_ = false; + + if (bridge_.get()) + bridge_->SetDefersLoading(false); + + NotifyNetworkEvent(); + } +} + +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; +} + +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_buffer_, read_size_)); + offset_ += first_offset_ + read; + + // And report with what we have read. + DoneRead(read); +} + +bool BufferedResourceLoader::VerifyPartialResponse( + const ResourceLoaderBridge::ResponseInfo& info) { + int64 first_byte_position, last_byte_position, instance_size; + if (!info.headers->GetContentRange(&first_byte_position, + &last_byte_position, + &instance_size)) { + return false; + } + + if (instance_size != kPositionNotSpecified) + instance_size_ = instance_size; + + if (first_byte_position_ != -1 && + 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(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(); +} + +void BufferedResourceLoader::NotifyNetworkEvent() { + if (event_callback_.get()) + event_callback_->Run(); +} + +///////////////////////////////////////////////////////////////////////////// +// BufferedDataSource, static methods +bool BufferedDataSource::IsMediaFormatSupported( + const media::MediaFormat& media_format) { + std::string mime_type; + std::string url; + if (media_format.GetAsString(media::MediaFormat::kMimeType, &mime_type) && + media_format.GetAsString(media::MediaFormat::kURL, &url)) { + GURL gurl(url); + + // This data source doesn't support data:// protocol, so reject it + // explicitly. + if (IsProtocolSupportedForMedia(gurl) && !IsDataProtocol(gurl)) + return true; + } + return false; +} + +///////////////////////////////////////////////////////////////////////////// +// BufferedDataSource, protected +BufferedDataSource::BufferedDataSource( + MessageLoop* render_loop, + webkit_glue::MediaResourceLoaderBridgeFactory* bridge_factory) + : total_bytes_(kPositionNotSpecified), + loaded_(false), + streaming_(false), + bridge_factory_(bridge_factory), + loader_(NULL), + network_activity_(false), + initialize_callback_(NULL), + read_callback_(NULL), + read_position_(0), + read_size_(0), + read_buffer_(NULL), + read_attempts_(0), + intermediate_read_buffer_(new uint8[kInitialReadBufferSize]), + intermediate_read_buffer_size_(kInitialReadBufferSize), + render_loop_(render_loop), + stop_signal_received_(false), + stopped_on_render_loop_(false), + media_is_paused_(true) { +} + +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::CreateResourceLoader( + 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); +} + +// This method simply returns kTimeoutMilliseconds. The purpose of this +// method is to be overidded so as to provide a different timeout value +// for testing purpose. +base::TimeDelta BufferedDataSource::GetTimeoutMilliseconds() { + return base::TimeDelta::FromMilliseconds(kTimeoutMilliseconds); +} + +///////////////////////////////////////////////////////////////////////////// +// BufferedDataSource, media::MediaFilter implementation +void BufferedDataSource::Initialize(const std::string& url, + media::FilterCallback* callback) { + // Saves the url. + url_ = GURL(url); + + if (!IsProtocolSupportedForMedia(url_)) { + // This method is called on the thread where host() lives so it is safe + // to make this call. + host()->SetError(media::PIPELINE_ERROR_NETWORK); + callback->Run(); + delete callback; + return; + } + + DCHECK(callback); + initialize_callback_.reset(callback); + + 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(media::FilterCallback* callback) { + { + AutoLock auto_lock(lock_); + stop_signal_received_ = true; + } + if (callback) { + callback->Run(); + delete callback; + } + render_loop_->PostTask(FROM_HERE, + NewRunnableMethod(this, &BufferedDataSource::CleanupTask)); +} + +void BufferedDataSource::SetPlaybackRate(float playback_rate) { + render_loop_->PostTask(FROM_HERE, + NewRunnableMethod(this, &BufferedDataSource::SetPlaybackRateTask, + playback_rate)); +} + +///////////////////////////////////////////////////////////////////////////// +// 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::IsStreaming() { + return streaming_; +} + +///////////////////////////////////////////////////////////////////////////// +// BufferedDataSource, render thread tasks +void BufferedDataSource::InitializeTask() { + DCHECK(MessageLoop::current() == render_loop_); + DCHECK(!loader_.get()); + DCHECK(!stopped_on_render_loop_); + + // Kick starts the watch dog task that will handle connection timeout. + // We run the watch dog 2 times faster the actual timeout so as to catch + // the timeout more accurately. + watch_dog_timer_.Start( + GetTimeoutMilliseconds() / 2, + this, + &BufferedDataSource::WatchDogTask); + + if (IsHttpProtocol(url_)) { + // Fetch only first 1024 bytes as this usually covers the header portion + // of a media file that gives enough information about the codecs, etc. + // This also serve as a probe to determine server capability to serve + // range request. + // TODO(hclam): Do some experiments for the best approach. + loader_ = CreateResourceLoader(0, 1024); + loader_->Start( + NewCallback(this, &BufferedDataSource::HttpInitialStartCallback), + NewCallback(this, &BufferedDataSource::NetworkEventCallback)); + } else { + // For all other protocols, assume they support range request. We fetch + // the full range of the resource to obtain the instance size because + // we won't be served HTTP headers. + loader_ = CreateResourceLoader(-1, -1); + loader_->Start( + NewCallback(this, &BufferedDataSource::NonHttpInitialStartCallback), + NewCallback(this, &BufferedDataSource::NetworkEventCallback)); + } +} + +void BufferedDataSource::ReadTask( + int64 position, int read_size, uint8* buffer, + media::DataSource::ReadCallback* read_callback) { + DCHECK(MessageLoop::current() == render_loop_); + + // If CleanupTask() was executed we should return immediately. We check this + // variable to prevent doing any actual work after clean up was done. We do + // not check |stop_signal_received_| because anything use of it has to be + // within |lock_| which is not desirable. + if (stopped_on_render_loop_) + return; + + 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; + read_submitted_time_ = base::Time::Now(); + read_attempts_ = 0; + + // Call to read internal to perform the actual read. + ReadInternal(); +} + +void BufferedDataSource::CleanupTask() { + DCHECK(MessageLoop::current() == render_loop_); + DCHECK(!stopped_on_render_loop_); + + // Stop the watch dog. + watch_dog_timer_.Stop(); + + // We just need to stop the loader, so it stops activity. + if (loader_.get()) + loader_->Stop(); + + // Reset the parameters of the current read request. + read_callback_.reset(); + read_position_ = 0; + read_size_ = 0; + read_buffer_ = 0; + read_submitted_time_ = base::Time(); + read_attempts_ = 0; + + // Signal that stop task has finished execution. + stopped_on_render_loop_ = true; +} + +void BufferedDataSource::RestartLoadingTask() { + DCHECK(MessageLoop::current() == render_loop_); + + // This variable is set in CleanupTask(). We check this and do an early + // return. The sequence of actions which enable this conditions is: + // 1. Stop() is called from the pipeline. + // 2. ReadCallback() is called from the resource loader. + // 3. CleanupTask() is executed. + // 4. RestartLoadingTask() is executed. + if (stopped_on_render_loop_) + return; + + // If there's no outstanding read then return early. + if (!read_callback_.get()) + return; + + loader_ = CreateResourceLoader(read_position_, -1); + loader_->SetAllowDefer(!media_is_paused_); + loader_->Start( + NewCallback(this, &BufferedDataSource::PartialReadStartCallback), + NewCallback(this, &BufferedDataSource::NetworkEventCallback)); +} + +void BufferedDataSource::WatchDogTask() { + DCHECK(MessageLoop::current() == render_loop_); + DCHECK(!stopped_on_render_loop_); + + // We only care if there is an active read request. + if (!read_callback_.get()) + return; + + DCHECK(loader_.get()); + base::TimeDelta delta = base::Time::Now() - read_submitted_time_; + if (delta < GetTimeoutMilliseconds()) + return; + + // TODO(hclam): Maybe raise an error here. But if an error is reported + // the whole pipeline may get destroyed... + if (read_attempts_ >= kReadTrials) + return; + + ++read_attempts_; + read_submitted_time_ = base::Time::Now(); + + // Stops the current loader and creates a new resource loader and + // retry the request. + loader_->Stop(); + loader_ = CreateResourceLoader(read_position_, -1); + loader_->SetAllowDefer(!media_is_paused_); + loader_->Start( + NewCallback(this, &BufferedDataSource::PartialReadStartCallback), + NewCallback(this, &BufferedDataSource::NetworkEventCallback)); +} + +void BufferedDataSource::SetPlaybackRateTask(float playback_rate) { + DCHECK(MessageLoop::current() == render_loop_); + DCHECK(loader_.get()); + + bool previously_paused = media_is_paused_; + media_is_paused_ = (playback_rate == 0.0); + + // Disallow deferring data when we are pausing, allow deferring data + // when we resume playing. + if (previously_paused && !media_is_paused_) { + loader_->SetAllowDefer(true); + } else if (!previously_paused && media_is_paused_) { + loader_->SetAllowDefer(false); + } +} + +// 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_Locked(int error) { + DCHECK(MessageLoop::current() == render_loop_); + DCHECK(read_callback_.get()); + 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_Locked() { + DCHECK(MessageLoop::current() == render_loop_); + 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::HttpInitialStartCallback(int error) { + DCHECK(MessageLoop::current() == render_loop_); + DCHECK(loader_.get()); + + int64 instance_size = loader_->instance_size(); + bool partial_response = loader_->partial_response(); + bool success = error == net::OK; + + if (success) { + // TODO(hclam): Needs more thinking about supporting servers without range + // request or their partial response is not complete. + total_bytes_ = instance_size; + loaded_ = false; + streaming_ = (instance_size == kPositionNotSpecified) || !partial_response; + } else { + // TODO(hclam): In case of failure, we can retry several times. + loader_->Stop(); + } + + // 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 (stop_signal_received_) + return; + + if (!success) { + host()->SetError(media::PIPELINE_ERROR_NETWORK); + DoneInitialization_Locked(); + return; + } + + if (streaming_) { + // If the server didn't reply with an instance size, it is likely this + // is a streaming response. + host()->SetStreaming(true); + } else { + // This value governs the range that we can seek to. + // TODO(hclam): Report the correct value of buffered bytes. + host()->SetTotalBytes(total_bytes_); + host()->SetBufferedBytes(0); + } + + // Currently, only files can be used reliably w/o a network. + host()->SetLoaded(false); + DoneInitialization_Locked(); +} + +void BufferedDataSource::NonHttpInitialStartCallback(int error) { + DCHECK(MessageLoop::current() == render_loop_); + DCHECK(loader_.get()); + + int64 instance_size = loader_->instance_size(); + bool success = error == net::OK && instance_size != kPositionNotSpecified; + + if (success) { + total_bytes_ = instance_size; + loaded_ = true; + } else { + loader_->Stop(); + } + + // 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 (stop_signal_received_) + return; + + if (success) { + host()->SetTotalBytes(total_bytes_); + host()->SetBufferedBytes(total_bytes_); + host()->SetLoaded(loaded_); + } else { + host()->SetError(media::PIPELINE_ERROR_NETWORK); + } + DoneInitialization_Locked(); +} + +void BufferedDataSource::PartialReadStartCallback(int error) { + DCHECK(MessageLoop::current() == render_loop_); + DCHECK(loader_.get()); + + // This callback method is invoked after we have verified the server has + // range request capability, so as a safety guard verify again the response + // is partial. + if (error == net::OK && loader_->partial_response()) { + // Once the range request has started successfully, we can proceed with + // reading from it. + ReadInternal(); + return; + } + + // Stop the resource loader since we have received an error. + loader_->Stop(); + + // 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 (stop_signal_received_) + return; + DoneRead_Locked(net::ERR_INVALID_RESPONSE); +} + +void BufferedDataSource::ReadCallback(int error) { + DCHECK(MessageLoop::current() == render_loop_); + + if (error < 0) { + DCHECK(loader_.get()); + + // Stop the resource load if it failed. + loader_->Stop(); + + if (error == net::ERR_CACHE_MISS) { + render_loop_->PostTask(FROM_HERE, + NewRunnableMethod(this, &BufferedDataSource::RestartLoadingTask)); + return; + } + } + + // 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 (stop_signal_received_) + return; + + 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_Locked(error); +} + +void BufferedDataSource::NetworkEventCallback() { + DCHECK(MessageLoop::current() == render_loop_); + DCHECK(loader_.get()); + + // In case of non-HTTP request we don't need to report network events, + // so return immediately. + if (loaded_) + return; + + bool network_activity = loader_->network_activity(); + int64 buffered_last_byte_position = loader_->GetBufferedLastBytePosition(); + + // If we get an unspecified value, return immediately. + if (buffered_last_byte_position == kPositionNotSpecified) + return; + + // 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 (stop_signal_received_) + return; + + if (network_activity != network_activity_) { + network_activity_ = network_activity; + host()->SetNetworkActivity(network_activity); + } + host()->SetBufferedBytes(buffered_last_byte_position + 1); +} + +} // 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..0dc2115 --- /dev/null +++ b/webkit/glue/media/buffered_data_source.h @@ -0,0 +1,413 @@ +// Copyright (c) 2010 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 <algorithm> +#include <string> +#include <vector> + +#include "base/callback.h" +#include "base/lock.h" +#include "base/scoped_ptr.h" +#include "base/timer.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 base::RefCountedThreadSafe<BufferedResourceLoader>, + public webkit_glue::ResourceLoaderBridge::Peer { + public: + typedef Callback0::Type NetworkEventCallback; + + // |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); + + // 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 |callback| is called with the result. + // |callback| is called with the following values: + // - net::OK + // The request has started successfully. + // - net::ERR_FAILED + // The request has failed because of an error with the network. + // - net::ERR_INVALID_RESPONSE + // An invalid response is received from the server. + // - (Anything else) + // An error code that indicates the request has failed. + // |event_callback| is called when the response is completed, data is + // received, the request is suspended or resumed. + virtual void Start(net::CompletionCallback* callback, + NetworkEventCallback* event_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. + // |callback| is called with the following values: + // - (Anything greater than or equal 0) + // Read was successful with the indicated number of bytes read. + // - net::ERR_FAILED + // The read has failed because of an error with the network. + // - net::ERR_CACHE_MISS + // The read was made too far away from the current buffered position. + virtual void Read(int64 position, int read_size, + uint8* buffer, net::CompletionCallback* callback); + + // Returns the position of the first byte buffered. Returns -1 if such value + // is not available. + virtual int64 GetBufferedFirstBytePosition(); + + // Returns the position of the last byte buffered. Returns -1 if such value + // is not available. + virtual int64 GetBufferedLastBytePosition(); + + // Sets whether deferring data is allowed or disallowed. + virtual void SetAllowDefer(bool is_allowed); + + // Gets the content length in bytes of the instance after this loader has been + // started. If this value is -1, then content length is unknown. + virtual int64 content_length() { return content_length_; } + + // Gets the original size of the file requested. If this value is -1, then + // the size is unknown. + virtual int64 instance_size() { return instance_size_; } + + // Returns true if the response for this loader is a partial response. + // It means a 206 response in HTTP/HTTPS protocol. + virtual bool partial_response() { return partial_response_; } + + // Returns true if network is currently active. + virtual bool network_activity() { return !completed_ && !deferred_; } + + ///////////////////////////////////////////////////////////////////////////// + // webkit_glue::ResourceLoaderBridge::Peer implementations. + virtual void OnUploadProgress(uint64 position, uint64 size) {} + virtual bool OnReceivedRedirect( + const GURL& new_url, + const webkit_glue::ResourceLoaderBridge::ResponseInfo& info, + bool* has_new_first_party_for_cookies, + GURL* new_first_party_for_cookies); + 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); + GURL GetURLForDebugging() const { return url_; } + + protected: + friend class base::RefCountedThreadSafe<BufferedResourceLoader>; + + virtual ~BufferedResourceLoader(); + + private: + friend class BufferedResourceLoaderTest; + + // 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(); + + // Method that does the actual read and calls the |read_callbac_|, assuming + // the request range is in |buffer_|. + void ReadInternal(); + + // If we have made a range request, verify the response from the server. + bool VerifyPartialResponse(const ResourceLoaderBridge::ResponseInfo& info); + + // 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); + + // Calls |event_callback_| in terms of a network event. + void NotifyNetworkEvent(); + + bool HasPendingRead() { return read_callback_.get() != NULL; } + + // A sliding window of buffer. + scoped_ptr<media::SeekableBuffer> buffer_; + + // True if resource loading was deferred. + bool deferred_; + + // True if resource loader is allowed to defer, false otherwise. + bool defer_allowed_; + + // True if resource loading has completed. + bool completed_; + + // True if a range request was made. + bool range_requested_; + + // True if response data received is a partial range. + bool partial_response_; + + webkit_glue::MediaResourceLoaderBridgeFactory* bridge_factory_; + GURL url_; + int64 first_byte_position_; + int64 last_byte_position_; + + // Callback method that listens to network events. + scoped_ptr<NetworkEventCallback> event_callback_; + + // Members used during request start. + scoped_ptr<net::CompletionCallback> start_callback_; + scoped_ptr<webkit_glue::ResourceLoaderBridge> bridge_; + int64 offset_; + int64 content_length_; + int64 instance_size_; + + // 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::FilterFactoryImpl2 implementation. + static bool IsMediaFormatSupported( + const media::MediaFormat& media_format); + + // media::MediaFilter implementation. + virtual void Initialize(const std::string& url, + media::FilterCallback* callback); + virtual void Stop(media::FilterCallback* callback); + virtual void SetPlaybackRate(float playback_rate); + + // 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 IsStreaming(); + + 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* CreateResourceLoader( + int64 first_byte_position, int64 last_byte_position); + + // Gets the number of milliseconds to declare a request timeout since + // the request was made. This method is made virtual so as to inject a + // different number for testing purpose. + virtual base::TimeDelta GetTimeoutMilliseconds(); + + private: + friend class media::FilterFactoryImpl2< + BufferedDataSource, + MessageLoop*, + webkit_glue::MediaResourceLoaderBridgeFactory*>; + + // Posted to perform initialization on render thread and start resource + // loading. + void InitializeTask(); + + // Task posted to perform 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. Stops |watch_dog_timer_| and + // |loader_|, reset Read() variables, and set |stopped_on_render_loop_| + // to signal any remaining tasks to stop. + void CleanupTask(); + + // Restart resource loading on render thread. + void RestartLoadingTask(); + + // This task monitors the current active read request. If the current read + // request has timed out, this task will destroy the current loader and + // creates a new one to accommodate the read request. + void WatchDogTask(); + + // This task uses the current playback rate with the previous playback rate + // to determine whether we are going from pause to play and play to pause, + // and signals the buffered resource loader accordingly. + void SetPlaybackRateTask(float playback_rate); + + // 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_Locked(int error); + + // Calls |initialize_callback_| and reset it. + void DoneInitialization_Locked(); + + // Callback method for |loader_| if URL for the resource requested is using + // HTTP protocol. This method is called when response for initial request is + // received. + void HttpInitialStartCallback(int error); + + // Callback method for |loader_| if URL for the resource requested is using + // a non-HTTP protocol, e.g. local files. This method is called when response + // for initial request is received. + void NonHttpInitialStartCallback(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); + + // Callback method when a network event is received. + void NetworkEventCallback(); + + 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_; + + // True if this data source is considered loaded. + bool loaded_; + + // This value will be true if this data source can only support streaming. + // i.e. range request is not supported. + bool streaming_; + + // A factory object to produce ResourceLoaderBridge. + scoped_ptr<webkit_glue::MediaResourceLoaderBridgeFactory> bridge_factory_; + + // A resource loader for the media resource. + scoped_refptr<BufferedResourceLoader> loader_; + + // True if network is active. + bool network_activity_; + + // Callback method from the pipeline for initialization. + scoped_ptr<media::FilterCallback> initialize_callback_; + + // Read parameters received from the Read() method call. + scoped_ptr<media::DataSource::ReadCallback> read_callback_; + int64 read_position_; + int read_size_; + uint8* read_buffer_; + base::Time read_submitted_time_; + int read_attempts_; + + // 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_; + + // Protects |stopped_|. + Lock lock_; + + // Stop signal to suppressing activities. This variable is set on the pipeline + // thread and read from the render thread. + bool stop_signal_received_; + + // This variable is set by CleanupTask() that indicates this object is stopped + // on the render thread. + bool stopped_on_render_loop_; + + // This variable is true when we are in a paused state and false when we + // are in a playing state. + bool media_is_paused_; + + // This timer is to run the WatchDogTask repeatedly. We use a timer instead + // of doing PostDelayedTask() reduce the extra reference held by the message + // loop. The RepeatingTimer does PostDelayedTask() internally, by using it + // the message loop doesn't hold a reference for the watch dog task. + base::RepeatingTimer<BufferedDataSource> watch_dog_timer_; + + 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..ce42437 --- /dev/null +++ b/webkit/glue/media/buffered_data_source_unittest.cc @@ -0,0 +1,934 @@ +// Copyright (c) 2010 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/callback.h" +#include "base/format_macros.h" +#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::DeleteArg; +using ::testing::DoAll; +using ::testing::InSequence; +using ::testing::Invoke; +using ::testing::InvokeWithoutArgs; +using ::testing::NotNull; +using ::testing::Return; +using ::testing::SetArgumentPointee; +using ::testing::StrictMock; +using ::testing::WithArgs; + +namespace { + +const char* kHttpUrl = "http://test"; +const char* kFileUrl = "file://test"; +const int kDataSize = 1024; + +enum NetworkState { + NONE, + LOADED, + LOADING +}; + +} // namespace + +namespace webkit_glue { + +// Submit a request completed event to the resource loader due to request +// being canceled. Pretending the event is from external. +ACTION_P(RequestCanceled, loader) { + URLRequestStatus status; + status.set_status(URLRequestStatus::CANCELED); + status.set_os_error(net::ERR_ABORTED); + loader->OnCompletedRequest(status, ""); +} + +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_ = new BufferedResourceLoader(&bridge_factory_, gurl_, + first_position_, last_position_); + EXPECT_EQ(gurl_.spec(), loader_->GetURLForDebugging().spec()); + } + + void SetLoaderBuffer(size_t forward_capacity, size_t backward_capacity) { + loader_->buffer_.reset( + new media::SeekableBuffer(backward_capacity, forward_capacity)); + } + + 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), + NewCallback(this, &BufferedResourceLoaderTest::NetworkCallback)); + } + + void FullResponse(int64 instance_size) { + EXPECT_CALL(*this, StartCallback(net::OK)); + ResourceLoaderBridge::ResponseInfo info; + std::string header = StringPrintf("HTTP/1.1 200 OK\n" + "Content-Length: %" PRId64, + instance_size); + replace(header.begin(), header.end(), '\n', '\0'); + info.headers = new net::HttpResponseHeaders(header); + info.content_length = instance_size; + loader_->OnReceivedResponse(info, false); + EXPECT_EQ(instance_size, loader_->content_length()); + EXPECT_EQ(instance_size, loader_->instance_size()); + EXPECT_FALSE(loader_->partial_response()); + } + + void PartialResponse(int64 first_position, int64 last_position, + int64 instance_size) { + EXPECT_CALL(*this, StartCallback(net::OK)); + int64 content_length = last_position - first_position + 1; + ResourceLoaderBridge::ResponseInfo info; + std::string header = StringPrintf("HTTP/1.1 206 Partial Content\n" + "Content-Range: bytes " + "%" PRId64 "-%" PRId64 "/%" PRId64, + first_position, + last_position, + instance_size); + 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()); + EXPECT_EQ(instance_size, loader_->instance_size()); + EXPECT_TRUE(loader_->partial_response()); + } + + void StopWhenLoad() { + InSequence s; + EXPECT_CALL(*bridge_, Cancel()) + .WillOnce(RequestCanceled(loader_)); + EXPECT_CALL(*bridge_, OnDestroy()) + .WillOnce(Invoke(this, &BufferedResourceLoaderTest::ReleaseBridge)); + loader_->Stop(); + } + + void ReleaseBridge() { + ignore_result(bridge_.release()); + } + + // Helper method to write to |loader_| from |data_|. + void WriteLoader(int position, int size) { + EXPECT_CALL(*this, NetworkCallback()) + .RetiresOnSaturation(); + 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)); + } + + // Helper method to disallow deferring in |loader_|. + void DisallowLoaderDefer() { + if (loader_->deferred_) { + EXPECT_CALL(*bridge_, SetDefersLoading(false)); + EXPECT_CALL(*this, NetworkCallback()); + } + loader_->SetAllowDefer(false); + } + + // Helper method to allow deferring in |loader_|. + void AllowLoaderDefer() { + loader_->SetAllowDefer(true); + } + + MOCK_METHOD1(StartCallback, void(int error)); + MOCK_METHOD1(ReadCallback, void(int error)); + MOCK_METHOD0(NetworkCallback, void()); + + protected: + GURL gurl_; + int64 first_position_; + int64 last_position_; + + scoped_refptr<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()) + .WillOnce(RequestCanceled(loader_)); + 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()) + .WillOnce(RequestCanceled(loader_)); + EXPECT_CALL(*bridge_, OnDestroy()) + .WillOnce(Invoke(this, &BufferedResourceLoaderTest::ReleaseBridge)); + + ResourceLoaderBridge::ResponseInfo info; + info.headers = new net::HttpResponseHeaders("HTTP/1.1 404 Not Found\n"); + loader_->OnReceivedResponse(info, false); +} + +// Tests that partial content is requested but not fulfilled. +TEST_F(BufferedResourceLoaderTest, NotPartialResponse) { + Initialize(kHttpUrl, 100, -1); + Start(); + FullResponse(1024); + StopWhenLoad(); +} + +// 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(100, 200, 1024); + StopWhenLoad(); +} + +// Tests that an invalid partial response is received. +TEST_F(BufferedResourceLoaderTest, InvalidPartialResponse) { + Initialize(kHttpUrl, 0, 10); + Start(); + + EXPECT_CALL(*this, StartCallback(net::ERR_INVALID_RESPONSE)); + EXPECT_CALL(*bridge_, Cancel()) + .WillOnce(RequestCanceled(loader_)); + EXPECT_CALL(*bridge_, OnDestroy()) + .WillOnce(Invoke(this, &BufferedResourceLoaderTest::ReleaseBridge)); + + ResourceLoaderBridge::ResponseInfo info; + std::string header = StringPrintf("HTTP/1.1 206 Partial Content\n" + "Content-Range: bytes %d-%d/%d", + 1, 10, 1024); + replace(header.begin(), header.end(), '\n', '\0'); + info.headers = new net::HttpResponseHeaders(header); + info.content_length = 10; + loader_->OnReceivedResponse(info, false); +} + +// Tests the logic of sliding window for data buffering and reading. +TEST_F(BufferedResourceLoaderTest, BufferAndRead) { + Initialize(kHttpUrl, 10, 29); + Start(); + PartialResponse(10, 29, 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 backward outside buffer. + EXPECT_CALL(*this, ReadCallback(net::ERR_CACHE_MISS)); + ReadLoader(9, 10, buffer); + + // Response has completed. + EXPECT_CALL(*this, NetworkCallback()); + 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); + + // Try to read beyond the instance size. + EXPECT_CALL(*this, ReadCallback(0)); + ReadLoader(30, 10, buffer); +} + +TEST_F(BufferedResourceLoaderTest, ReadOutsideBuffer) { + Initialize(kHttpUrl, 10, 0x00FFFFFF); + Start(); + PartialResponse(10, 0x00FFFFFF, 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(*this, ReadCallback(5)); + EXPECT_CALL(*this, NetworkCallback()); + EXPECT_CALL(*bridge_, OnDestroy()) + .WillOnce(Invoke(this, &BufferedResourceLoaderTest::ReleaseBridge)); + URLRequestStatus status; + status.set_status(URLRequestStatus::SUCCESS); + loader_->OnCompletedRequest(status, ""); +} + +TEST_F(BufferedResourceLoaderTest, RequestFailedWhenRead) { + Initialize(kHttpUrl, 10, 29); + Start(); + PartialResponse(10, 29, 30); + + uint8 buffer[10]; + InSequence s; + + ReadLoader(10, 10, buffer); + EXPECT_CALL(*this, ReadCallback(net::ERR_FAILED)); + EXPECT_CALL(*this, NetworkCallback()); + EXPECT_CALL(*bridge_, OnDestroy()) + .WillOnce(Invoke(this, &BufferedResourceLoaderTest::ReleaseBridge)); + URLRequestStatus status; + status.set_status(URLRequestStatus::FAILED); + loader_->OnCompletedRequest(status, ""); +} + +// Tests the logic of caching data to disk when media is paused. +TEST_F(BufferedResourceLoaderTest, AllowDefer_NoDataReceived) { + Initialize(kHttpUrl, 10, 99); + SetLoaderBuffer(10, 20); + Start(); + PartialResponse(10, 99, 100); + + // Start in undeferred state, then disallow defer, then allow defer + // without receiving data in between. + DisallowLoaderDefer(); + AllowLoaderDefer(); + StopWhenLoad(); +} + +TEST_F(BufferedResourceLoaderTest, AllowDefer_ReadSameWindow) { + Initialize(kHttpUrl, 10, 99); + SetLoaderBuffer(10, 20); + Start(); + PartialResponse(10, 99, 100); + + uint8 buffer[10]; + + // Start in undeferred state, disallow defer, receive data but don't shift + // buffer window, then allow defer and read. + DisallowLoaderDefer(); + WriteLoader(10, 10); + AllowLoaderDefer(); + + EXPECT_CALL(*this, ReadCallback(10)); + ReadLoader(10, 10, buffer); + VerifyBuffer(buffer, 10, 10); + StopWhenLoad(); +} + +TEST_F(BufferedResourceLoaderTest, AllowDefer_ReadPastWindow) { + Initialize(kHttpUrl, 10, 99); + SetLoaderBuffer(10, 20); + Start(); + PartialResponse(10, 99, 100); + + uint8 buffer[10]; + + // Not deferred, disallow defer, received data and shift buffer window, + // allow defer, then read in area outside of buffer window. + DisallowLoaderDefer(); + WriteLoader(10, 10); + WriteLoader(20, 50); + AllowLoaderDefer(); + + EXPECT_CALL(*this, ReadCallback(net::ERR_CACHE_MISS)); + ReadLoader(10, 10, buffer); + StopWhenLoad(); +} + +TEST_F(BufferedResourceLoaderTest, AllowDefer_DeferredNoDataReceived) { + Initialize(kHttpUrl, 10, 99); + SetLoaderBuffer(10, 20); + Start(); + PartialResponse(10, 99, 100); + + uint8 buffer[10]; + + // Start in deferred state, then disallow defer, receive no data, and + // allow defer and read. + EXPECT_CALL(*bridge_, SetDefersLoading(true)); + EXPECT_CALL(*this, NetworkCallback()); + WriteLoader(10, 40); + + DisallowLoaderDefer(); + AllowLoaderDefer(); + + EXPECT_CALL(*this, ReadCallback(10)); + ReadLoader(20, 10, buffer); + VerifyBuffer(buffer, 20, 10); + StopWhenLoad(); +} + +TEST_F(BufferedResourceLoaderTest, AllowDefer_DeferredReadSameWindow) { + Initialize(kHttpUrl, 10, 99); + SetLoaderBuffer(10, 20); + Start(); + PartialResponse(10, 99, 100); + + uint8 buffer[10]; + + // Start in deferred state, disallow defer, receive data and shift buffer + // window, allow defer, and read in a place that's still in the window. + EXPECT_CALL(*bridge_, SetDefersLoading(true)); + EXPECT_CALL(*this, NetworkCallback()); + WriteLoader(10, 30); + + DisallowLoaderDefer(); + WriteLoader(40, 5); + AllowLoaderDefer(); + + EXPECT_CALL(*this, ReadCallback(10)); + ReadLoader(20, 10, buffer); + VerifyBuffer(buffer, 20, 10); + StopWhenLoad(); +} + +TEST_F(BufferedResourceLoaderTest, AllowDefer_DeferredReadPastWindow) { + Initialize(kHttpUrl, 10, 99); + SetLoaderBuffer(10, 20); + Start(); + PartialResponse(10, 99, 100); + + uint8 buffer[10]; + + // Start in deferred state, disallow defer, receive data and shift buffer + // window, allow defer, and read outside of the buffer window. + EXPECT_CALL(*bridge_, SetDefersLoading(true)); + EXPECT_CALL(*this, NetworkCallback()); + WriteLoader(10, 40); + + DisallowLoaderDefer(); + WriteLoader(50, 20); + WriteLoader(70, 40); + AllowLoaderDefer(); + + EXPECT_CALL(*this, ReadCallback(net::ERR_CACHE_MISS)); + ReadLoader(20, 5, buffer); + StopWhenLoad(); +} + +// TODO(hclam): add unit test for defer loading. + +class MockBufferedResourceLoader : public BufferedResourceLoader { + public: + MockBufferedResourceLoader() : BufferedResourceLoader(NULL, GURL(), 0, 0) { + } + + MOCK_METHOD2(Start, void(net::CompletionCallback* read_callback, + NetworkEventCallback* network_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(instance_size, int64()); + MOCK_METHOD0(partial_response, bool()); + MOCK_METHOD0(network_activity, bool()); + MOCK_METHOD0(GetBufferedFirstBytePosition, int64()); + MOCK_METHOD0(GetBufferedLastBytePosition, int64()); + + protected: + ~MockBufferedResourceLoader() {} + + DISALLOW_COPY_AND_ASSIGN(MockBufferedResourceLoader); +}; + +// A mock BufferedDataSource to inject mock BufferedResourceLoader through +// CreateResourceLoader() 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); + } + + virtual base::TimeDelta GetTimeoutMilliseconds() { + // It is 100 ms because we don't want the test to run too long. + return base::TimeDelta::FromMilliseconds(100); + } + + MOCK_METHOD2(CreateResourceLoader, 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_ = MessageLoop::current(); + bridge_factory_.reset( + new StrictMock<MockMediaResourceLoaderBridgeFactory>()); + factory_ = MockBufferedDataSource::CreateFactory(message_loop_, + bridge_factory_.get()); + + // Prepare test data. + for (size_t i = 0; i < sizeof(data_); ++i) { + data_[i] = i; + } + } + + virtual ~BufferedDataSourceTest() { + if (data_source_) { + // Release the bridge factory because we don't own it. + // Expects bridge factory to be destroyed along with data source. + EXPECT_CALL(*bridge_factory_, OnDestroy()) + .WillOnce(Invoke(this, + &BufferedDataSourceTest::ReleaseBridgeFactory)); + } + } + + void InitializeDataSource(const char* url, int error, + bool partial_response, int64 instance_size, + NetworkState networkState) { + // 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 mock loader to be injected. + loader_ = new StrictMock<MockBufferedResourceLoader>(); + + bool loaded = networkState == LOADED; + { + InSequence s; + EXPECT_CALL(*data_source_, CreateResourceLoader(_, _)) + .WillOnce(Return(loader_.get())); + + // The initial response loader will be started. + EXPECT_CALL(*loader_, Start(NotNull(), NotNull())) + .WillOnce( + DoAll(Assign(&error_, error), + Invoke(this, + &BufferedDataSourceTest::InvokeStartCallback))); + } + + StrictMock<media::MockFilterCallback> callback; + EXPECT_CALL(*loader_, instance_size()) + .WillRepeatedly(Return(instance_size)); + EXPECT_CALL(*loader_, partial_response()) + .WillRepeatedly(Return(partial_response)); + if (error == net::OK) { + // Expected loaded or not. + EXPECT_CALL(host_, SetLoaded(loaded)); + + // TODO(hclam): The condition for streaming needs to be adjusted. + if (instance_size != -1 && (loaded || partial_response)) { + EXPECT_CALL(host_, SetTotalBytes(instance_size)); + if (loaded) + EXPECT_CALL(host_, SetBufferedBytes(instance_size)); + else + EXPECT_CALL(host_, SetBufferedBytes(0)); + } else { + EXPECT_CALL(host_, SetStreaming(true)); + } + + EXPECT_CALL(callback, OnFilterCallback()); + EXPECT_CALL(callback, OnCallbackDestroyed()); + } else { + EXPECT_CALL(host_, SetError(media::PIPELINE_ERROR_NETWORK)); + EXPECT_CALL(*loader_, Stop()); + EXPECT_CALL(callback, OnFilterCallback()); + EXPECT_CALL(callback, OnCallbackDestroyed()); + } + + // Actual initialization of the data source. + data_source_->Initialize(url, callback.NewCallback()); + message_loop_->RunAllPending(); + + if (error == net::OK) { + // Verify the size of the data source. + int64 size; + if (instance_size != -1 && (loaded || partial_response)) { + EXPECT_TRUE(data_source_->GetSize(&size)); + EXPECT_EQ(instance_size, size); + } else { + EXPECT_TRUE(data_source_->IsStreaming()); + } + } + } + + void StopDataSource() { + if (loader_) { + InSequence s; + EXPECT_CALL(*loader_, Stop()); + } + + StrictMock<media::MockFilterCallback> callback; + EXPECT_CALL(callback, OnFilterCallback()); + EXPECT_CALL(callback, OnCallbackDestroyed()); + data_source_->Stop(callback.NewCallback()); + message_loop_->RunAllPending(); + } + + void ReleaseBridgeFactory() { + ignore_result(bridge_factory_.release()); + } + + void InvokeStartCallback( + net::CompletionCallback* callback, + BufferedResourceLoader::NetworkEventCallback* network_callback) { + callback->RunWithParams(Tuple1<int>(error_)); + delete callback; + // TODO(hclam): Save this callback. + delete network_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_); + + 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_); + + // 1. Reply with a cache miss for the read. + { + InSequence s; + EXPECT_CALL(*loader_, Read(position, size, NotNull(), NotNull())) + .WillOnce(DoAll(Assign(&error_, net::ERR_CACHE_MISS), + Invoke(this, + &BufferedDataSourceTest::InvokeReadCallback))); + EXPECT_CALL(*loader_, Stop()); + } + + // 2. Then the current loader will be stop and destroyed. + StrictMock<MockBufferedResourceLoader> *new_loader = + new StrictMock<MockBufferedResourceLoader>(); + EXPECT_CALL(*data_source_, CreateResourceLoader(position, -1)) + .WillOnce(Return(new_loader)); + + // 3. Then the new loader will be started. + EXPECT_CALL(*new_loader, Start(NotNull(), NotNull())) + .WillOnce(DoAll(Assign(&error_, net::OK), + Invoke(this, + &BufferedDataSourceTest::InvokeStartCallback))); + EXPECT_CALL(*new_loader, partial_response()) + .WillRepeatedly(Return(loader_->partial_response())); + + // 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)); + + loader_ = new_loader; + } + + void ReadDataSourceFailed(int64 position, int size, int error) { + EXPECT_TRUE(loader_); + + // 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. Host will then receive an error. + EXPECT_CALL(*loader_, Stop()); + + // 3. The read has failed, so read callback will be called. + EXPECT_CALL(*this, ReadCallback(media::DataSource::kReadError)); + + data_source_->Read( + position, size, buffer_, + NewCallback(this, &BufferedDataSourceTest::ReadCallback)); + + message_loop_->RunAllPending(); + } + + void ReadDataSourceTimesOut(int64 position, int size) { + // 1. Drop the request and let it times out. + { + InSequence s; + EXPECT_CALL(*loader_, Read(position, size, NotNull(), NotNull())) + .WillOnce(DeleteArg<3>()); + EXPECT_CALL(*loader_, Stop()); + } + + // 2. Then the current loader will be stop and destroyed. + StrictMock<MockBufferedResourceLoader> *new_loader = + new StrictMock<MockBufferedResourceLoader>(); + EXPECT_CALL(*data_source_, CreateResourceLoader(position, -1)) + .WillOnce(Return(new_loader)); + + // 3. Then the new loader will be started and respond to queries about + // whether this is a partial response using the value of the previous + // loader. + EXPECT_CALL(*new_loader, Start(NotNull(), NotNull())) + .WillOnce(DoAll(Assign(&error_, net::OK), + Invoke(this, + &BufferedDataSourceTest::InvokeStartCallback))); + EXPECT_CALL(*new_loader, partial_response()) + .WillRepeatedly(Return(loader_->partial_response())); + + // 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), + InvokeWithoutArgs(message_loop_, + &MessageLoop::Quit))); + + EXPECT_CALL(*this, ReadCallback(size)); + + data_source_->Read( + position, size, buffer_, + NewCallback(this, &BufferedDataSourceTest::ReadCallback)); + + // This blocks the current thread until the watch task is executed and + // triggers a read callback to quit this message loop. + message_loop_->Run(); + + // Make sure data is correct. + EXPECT_EQ(0, memcmp(buffer_, data_ + static_cast<int>(position), size)); + + loader_ = new_loader; + } + + MOCK_METHOD1(ReadCallback, void(size_t size)); + + scoped_ptr<StrictMock<MockMediaResourceLoaderBridgeFactory> > + bridge_factory_; + scoped_refptr<StrictMock<MockBufferedResourceLoader> > loader_; + scoped_refptr<MockBufferedDataSource> data_source_; + scoped_refptr<media::FilterFactory> factory_; + + StrictMock<media::MockFilterHost> host_; + GURL gurl_; + 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, true, 1024, LOADING); + StopDataSource(); +} + +TEST_F(BufferedDataSourceTest, InitiailizationFailed) { + InitializeDataSource(kHttpUrl, net::ERR_FILE_NOT_FOUND, false, 0, NONE); + StopDataSource(); +} + +TEST_F(BufferedDataSourceTest, MissingContentLength) { + InitializeDataSource(kHttpUrl, net::OK, true, -1, LOADING); + StopDataSource(); +} + +TEST_F(BufferedDataSourceTest, RangeRequestNotSupported) { + InitializeDataSource(kHttpUrl, net::OK, false, 1024, LOADING); + StopDataSource(); +} + +TEST_F(BufferedDataSourceTest, + MissingContentLengthAndRangeRequestNotSupported) { + InitializeDataSource(kHttpUrl, net::OK, false, -1, LOADING); + StopDataSource(); +} + +TEST_F(BufferedDataSourceTest, ReadCacheHit) { + InitializeDataSource(kHttpUrl, net::OK, true, 25, LOADING); + + // 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, true, 1024, LOADING); + ReadDataSourceMiss(1000, 10); + ReadDataSourceMiss(20, 10); + StopDataSource(); +} + +TEST_F(BufferedDataSourceTest, ReadFailed) { + InitializeDataSource(kHttpUrl, net::OK, true, 1024, LOADING); + ReadDataSourceHit(10, 10, 10); + ReadDataSourceFailed(10, 10, net::ERR_CONNECTION_RESET); + StopDataSource(); +} + +TEST_F(BufferedDataSourceTest, ReadTimesOut) { + InitializeDataSource(kHttpUrl, net::OK, true, 1024, LOADING); + ReadDataSourceTimesOut(20, 10); + StopDataSource(); +} + +TEST_F(BufferedDataSourceTest, FileHasLoadedState) { + InitializeDataSource(kFileUrl, net::OK, true, 1024, LOADED); + ReadDataSourceTimesOut(20, 10); + StopDataSource(); +} + +} // namespace webkit_glue diff --git a/webkit/glue/media/media_resource_loader_bridge_factory.cc b/webkit/glue/media/media_resource_loader_bridge_factory.cc new file mode 100644 index 0000000..1961bcc --- /dev/null +++ b/webkit/glue/media/media_resource_loader_bridge_factory.cc @@ -0,0 +1,76 @@ +// 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 "webkit/glue/media/media_resource_loader_bridge_factory.h" + +#include "base/format_macros.h" +#include "base/string_util.h" + +namespace { + +// A constant for an unknown position. +const int64 kPositionNotSpecified = -1; + +} // namespace + +namespace webkit_glue { + +MediaResourceLoaderBridgeFactory::MediaResourceLoaderBridgeFactory( + const GURL& referrer, + const std::string& frame_origin, + const std::string& main_frame_origin, + int origin_pid, + int appcache_host_id, + int32 routing_id) + : referrer_(referrer), + frame_origin_(frame_origin), + main_frame_origin_(main_frame_origin), + origin_pid_(origin_pid), + appcache_host_id_(appcache_host_id), + routing_id_(routing_id) { +} + +ResourceLoaderBridge* MediaResourceLoaderBridgeFactory::CreateBridge( + const GURL& url, + int load_flags, + int64 first_byte_position, + int64 last_byte_position) { + webkit_glue::ResourceLoaderBridge::RequestInfo request_info; + request_info.method = "GET"; + request_info.url = url; + request_info.first_party_for_cookies = url; + request_info.referrer = referrer_; + request_info.frame_origin = frame_origin_; + request_info.main_frame_origin = main_frame_origin_; + request_info.headers = GenerateHeaders(first_byte_position, + last_byte_position); + request_info.load_flags = load_flags; + request_info.requestor_pid = origin_pid_; + request_info.request_type = ResourceType::MEDIA; + request_info.appcache_host_id = appcache_host_id_; + request_info.routing_id = routing_id_; + return webkit_glue::ResourceLoaderBridge::Create(request_info); +} + +// static +const std::string MediaResourceLoaderBridgeFactory::GenerateHeaders ( + int64 first_byte_position, int64 last_byte_position) { + // Construct the range header. + std::string header; + if (first_byte_position > kPositionNotSpecified && + last_byte_position > kPositionNotSpecified) { + if (first_byte_position <= last_byte_position) { + header = StringPrintf("Range: bytes=%" PRId64 "-%" PRId64, + first_byte_position, + last_byte_position); + } + } else if (first_byte_position > kPositionNotSpecified) { + header = StringPrintf("Range: bytes=%" PRId64 "-", first_byte_position); + } else if (last_byte_position > kPositionNotSpecified) { + NOTIMPLEMENTED() << "Suffix range not implemented"; + } + return header; +} + +} // 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 new file mode 100644 index 0000000..6408949 --- /dev/null +++ b/webkit/glue/media/media_resource_loader_bridge_factory.h @@ -0,0 +1,76 @@ +// 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_MEDIA_RESOURCE_LOADER_BRIDGE_FACTORY_H_ +#define WEBKIT_GLUE_MEDIA_MEDIA_RESOURCE_LOADER_BRIDGE_FACTORY_H_ + +#include "testing/gtest/include/gtest/gtest_prod.h" +#include "webkit/glue/resource_loader_bridge.h" + +namespace webkit_glue { + +// A factory used to create a ResourceLoaderBridge for the media player. +// This factory is used also for testing. Testing code can use this class and +// override CreateBridge() to inject a mock ResourceLoaderBridge for code that +// interacts with it, e.g. BufferedDataSource. +class MediaResourceLoaderBridgeFactory { + public: + MediaResourceLoaderBridgeFactory( + const GURL& referrer, + const std::string& frame_origin, + const std::string& main_frame_origin, + int origin_pid, + int appcache_host_id, + int32 routing_id); + + virtual ~MediaResourceLoaderBridgeFactory() {} + + // Factory method to create a ResourceLoaderBridge with the following + // parameters: + // |url| - URL of the resource to be loaded. + // |load_flags| - Load flags for this loading. + // |first_byte_position| - First byte position for a range request, -1 if not. + // |last_byte_position| - Last byte position for a range request, -1 if not. + virtual ResourceLoaderBridge* CreateBridge( + const GURL& url, + int load_flags, + int64 first_byte_position, + int64 last_byte_position); + + protected: + // An empty constructor only used by inherited classes. + MediaResourceLoaderBridgeFactory() { + } + + private: + FRIEND_TEST(MediaResourceLoaderBridgeFactoryTest, GenerateHeaders); + + // Returns a range request header using parameters |first_byte_position| and + // |last_byte_position|. + // Negative numbers other than -1 are not allowed for |first_byte_position| + // and |last_byte_position|. |first_byte_position| should always be less than + // or equal to |last_byte_position| if they are both not -1. + // Here's a list of valid parameters: + // |first_byte_position| |last_byte_position| + // 0 1000 + // 4096 4096 + // 0 -1 + // -1 -1 + // Empty string is returned on invalid parameters. + static const std::string GenerateHeaders(int64 first_byte_position, + int64 last_byte_position); + + GURL first_party_for_cookies_; + GURL referrer_; + std::string frame_origin_; + std::string main_frame_origin_; + std::string headers_; + int origin_pid_; + int appcache_host_id_; + int32 routing_id_; +}; + +} // namespace webkit_glue + +#endif // WEBKIT_GLUE_MEDIA_MEDIA_RESOURCE_LOADER_BRIDGE_FACTORY_H_ diff --git a/webkit/glue/media/media_resource_loader_bridge_factory_unittest.cc b/webkit/glue/media/media_resource_loader_bridge_factory_unittest.cc new file mode 100644 index 0000000..4c0126b --- /dev/null +++ b/webkit/glue/media/media_resource_loader_bridge_factory_unittest.cc @@ -0,0 +1,44 @@ +// 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 "net/http/http_util.h" +#include "webkit/glue/media/media_resource_loader_bridge_factory.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace webkit_glue { + +TEST(MediaResourceLoaderBridgeFactoryTest, GenerateHeaders) { + static const struct { + const bool success; + const int64 first_byte_position; + const int64 last_byte_position; + } data[] = { + { false, -1, -1 }, + { false, -5, 0 }, + { false, 100, 0 }, + { true, 0, -1 }, + { true, 0, 0 }, + { true, 100, 100 }, + { true, 50, -1 }, + { true, 10000, -1 }, + { true, 50, 100 }, + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(data); ++i) { + std::string headers = MediaResourceLoaderBridgeFactory::GenerateHeaders( + data[i].first_byte_position, data[i].last_byte_position); + std::vector<net::HttpByteRange> ranges; + bool ret = net::HttpUtil::ParseRanges(headers, &ranges); + EXPECT_EQ(data[i].success, ret); + if (ret) { + EXPECT_EQ(1u, ranges.size()); + EXPECT_EQ(data[i].first_byte_position, + ranges[0].first_byte_position()); + EXPECT_EQ(data[i].last_byte_position, + ranges[0].last_byte_position()); + } + } +} + +} // namespace webkit_glue 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 new file mode 100644 index 0000000..20bf0af --- /dev/null +++ b/webkit/glue/media/simple_data_source.cc @@ -0,0 +1,236 @@ +// Copyright (c) 2010 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/message_loop.h" +#include "base/process_util.h" +#include "media/base/filter_host.h" +#include "net/base/load_flags.h" +#include "net/base/data_url.h" +#include "net/http/http_response_headers.h" +#include "net/url_request/url_request_status.h" +#include "webkit/glue/media/simple_data_source.h" +#include "webkit/glue/resource_loader_bridge.h" +#include "webkit/glue/webkit_glue.h" + +namespace { + +const char kHttpScheme[] = "http"; +const char kHttpsScheme[] = "https"; +const char kDataScheme[] = "data"; + +// A helper method that accepts only HTTP, HTTPS and FILE protocol. +bool IsDataProtocol(const GURL& url) { + return url.SchemeIs(kDataScheme); +} + +} // namespace + +namespace webkit_glue { + +bool SimpleDataSource::IsMediaFormatSupported( + const media::MediaFormat& media_format) { + std::string mime_type; + std::string url; + if (media_format.GetAsString(media::MediaFormat::kMimeType, &mime_type) && + media_format.GetAsString(media::MediaFormat::kURL, &url)) { + GURL gurl(url); + if (IsProtocolSupportedForMedia(gurl)) + return true; + } + return false; +} + +SimpleDataSource::SimpleDataSource( + MessageLoop* render_loop, + webkit_glue::MediaResourceLoaderBridgeFactory* bridge_factory) + : render_loop_(render_loop), + bridge_factory_(bridge_factory), + size_(-1), + state_(UNINITIALIZED) { + DCHECK(render_loop); +} + +SimpleDataSource::~SimpleDataSource() { + AutoLock auto_lock(lock_); + DCHECK(state_ == UNINITIALIZED || state_ == STOPPED); +} + +void SimpleDataSource::Stop(media::FilterCallback* callback) { + AutoLock auto_lock(lock_); + state_ = STOPPED; + if (callback) { + callback->Run(); + delete callback; + } + + // Post a task to the render thread to cancel loading the resource. + render_loop_->PostTask(FROM_HERE, + NewRunnableMethod(this, &SimpleDataSource::CancelTask)); +} + +void SimpleDataSource::Initialize(const std::string& url, + media::FilterCallback* callback) { + AutoLock auto_lock(lock_); + DCHECK_EQ(state_, UNINITIALIZED); + DCHECK(callback); + state_ = INITIALIZING; + initialize_callback_.reset(callback); + + // Validate the URL. + SetURL(GURL(url)); + if (!url_.is_valid() || !IsProtocolSupportedForMedia(url_)) { + host()->SetError(media::PIPELINE_ERROR_NETWORK); + initialize_callback_->Run(); + initialize_callback_.reset(); + return; + } + + // Post a task to the render thread to start loading the resource. + render_loop_->PostTask(FROM_HERE, + NewRunnableMethod(this, &SimpleDataSource::StartTask)); +} + +const media::MediaFormat& SimpleDataSource::media_format() { + return media_format_; +} + +void SimpleDataSource::Read(int64 position, + size_t size, + uint8* data, + ReadCallback* read_callback) { + DCHECK_GE(size_, 0); + 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) { + *size_out = size_; + return true; +} + +bool SimpleDataSource::IsStreaming() { + return false; +} + +void SimpleDataSource::OnDownloadProgress(uint64 position, uint64 size) {} + +void SimpleDataSource::OnUploadProgress(uint64 position, uint64 size) {} + +bool SimpleDataSource::OnReceivedRedirect( + const GURL& new_url, + const webkit_glue::ResourceLoaderBridge::ResponseInfo& info, + bool* has_new_first_party_for_cookies, + GURL* new_first_party_for_cookies) { + SetURL(new_url); + // TODO(wtc): should we return a new first party for cookies URL? + *has_new_first_party_for_cookies = false; + return true; +} + +void SimpleDataSource::OnReceivedResponse( + const webkit_glue::ResourceLoaderBridge::ResponseInfo& info, + bool content_filtered) { + size_ = info.content_length; +} + +void SimpleDataSource::OnReceivedData(const char* data, int len) { + data_.append(data, len); +} + +void SimpleDataSource::OnCompletedRequest(const URLRequestStatus& status, + const std::string& security_info) { + AutoLock auto_lock(lock_); + // It's possible this gets called after Stop(), in which case |host_| is no + // longer valid. + if (state_ == STOPPED) { + return; + } + + // Otherwise we should be initializing and have created a bridge. + DCHECK_EQ(state_, INITIALIZING); + DCHECK(bridge_.get()); + bridge_.reset(); + + // If we don't get a content length or the request has failed, report it + // as a network error. + DCHECK(size_ == -1 || static_cast<size_t>(size_) == data_.length()); + if (size_ == -1) { + size_ = data_.length(); + } + + DoneInitialization_Locked(status.is_success()); +} + +GURL SimpleDataSource::GetURLForDebugging() const { + return url_; +} + +void SimpleDataSource::SetURL(const GURL& url) { + url_ = url; + media_format_.Clear(); + media_format_.SetAsString(media::MediaFormat::kMimeType, + media::mime_type::kApplicationOctetStream); + media_format_.SetAsString(media::MediaFormat::kURL, url.spec()); +} + +void SimpleDataSource::StartTask() { + AutoLock auto_lock(lock_); + DCHECK(MessageLoop::current() == render_loop_); + + // We may have stopped. + if (state_ == STOPPED) + return; + + DCHECK_EQ(state_, INITIALIZING); + + if (IsDataProtocol(url_)) { + // If this using data protocol, we just need to decode it. + std::string mime_type, charset; + bool success = net::DataURL::Parse(url_, &mime_type, &charset, &data_); + + // Don't care about the mime-type just proceed if decoding was successful. + size_ = data_.length(); + DoneInitialization_Locked(success); + } else { + // Create our bridge and start loading the resource. + bridge_.reset(bridge_factory_->CreateBridge( + url_, net::LOAD_BYPASS_CACHE, -1, -1)); + bridge_->Start(this); + } +} + +void SimpleDataSource::CancelTask() { + AutoLock auto_lock(lock_); + DCHECK_EQ(state_, STOPPED); + + // Cancel any pending requests. + if (bridge_.get()) { + bridge_->Cancel(); + bridge_.reset(); + } +} + +void SimpleDataSource::DoneInitialization_Locked(bool success) { + lock_.AssertAcquired(); + if (success) { + state_ = INITIALIZED; + host()->SetTotalBytes(size_); + host()->SetBufferedBytes(size_); + // If scheme is file or data, say we are loaded. + host()->SetLoaded(url_.SchemeIsFile() || IsDataProtocol(url_)); + } else { + host()->SetError(media::PIPELINE_ERROR_NETWORK); + } + initialize_callback_->Run(); + initialize_callback_.reset(); +} + +} // namespace webkit_glue diff --git a/webkit/glue/media/simple_data_source.h b/webkit/glue/media/simple_data_source.h new file mode 100644 index 0000000..577d973 --- /dev/null +++ b/webkit/glue/media/simple_data_source.h @@ -0,0 +1,125 @@ +// Copyright (c) 2010 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. + +// An extremely simple implementation of DataSource that downloads the entire +// media resource into memory before signaling that initialization has finished. +// Primarily used to test <audio> and <video> with buffering/caching removed +// from the equation. + +#ifndef WEBKIT_GLUE_MEDIA_SIMPLE_DATA_SOURCE_H_ +#define WEBKIT_GLUE_MEDIA_SIMPLE_DATA_SOURCE_H_ + +#include "base/message_loop.h" +#include "base/scoped_ptr.h" +#include "media/base/factory.h" +#include "media/base/filters.h" +#include "webkit/glue/media/media_resource_loader_bridge_factory.h" + +class MessageLoop; +class WebMediaPlayerDelegateImpl; + +namespace webkit_glue { + +class SimpleDataSource : public media::DataSource, + public webkit_glue::ResourceLoaderBridge::Peer { + public: + static media::FilterFactory* CreateFactory( + MessageLoop* message_loop, + webkit_glue::MediaResourceLoaderBridgeFactory* bridge_factory) { + return new media::FilterFactoryImpl2< + SimpleDataSource, + MessageLoop*, + webkit_glue::MediaResourceLoaderBridgeFactory*>(message_loop, + bridge_factory); + } + + // media::FilterFactoryImpl2 implementation. + static bool IsMediaFormatSupported( + const media::MediaFormat& media_format); + + // MediaFilter implementation. + virtual void Stop(media::FilterCallback* callback); + + // DataSource implementation. + virtual void Initialize(const std::string& url, + media::FilterCallback* callback); + virtual const media::MediaFormat& media_format(); + virtual void Read(int64 position, size_t size, + uint8* data, ReadCallback* read_callback); + virtual bool GetSize(int64* size_out); + virtual bool IsStreaming(); + + // webkit_glue::ResourceLoaderBridge::Peer implementation. + virtual void OnDownloadProgress(uint64 position, uint64 size); + virtual void OnUploadProgress(uint64 position, uint64 size); + virtual bool OnReceivedRedirect( + const GURL& new_url, + const webkit_glue::ResourceLoaderBridge::ResponseInfo& info, + bool* has_new_first_party_for_cookies, + GURL* new_first_party_for_cookies); + 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); + virtual GURL GetURLForDebugging() const; + + private: + friend class media::FilterFactoryImpl2< + SimpleDataSource, + MessageLoop*, + webkit_glue::MediaResourceLoaderBridgeFactory*>; + SimpleDataSource( + MessageLoop* render_loop, + webkit_glue::MediaResourceLoaderBridgeFactory* bridge_factory); + virtual ~SimpleDataSource(); + + // Updates |url_| and |media_format_| with the given URL. + void SetURL(const GURL& url); + + // Creates and starts the resource loading on the render thread. + void StartTask(); + + // Cancels and deletes the resource loading on the render thread. + void CancelTask(); + + // Perform initialization completion tasks under a lock. + void DoneInitialization_Locked(bool success); + + // Primarily used for asserting the bridge is loading on the render thread. + MessageLoop* render_loop_; + + // Factory to create a bridge. + scoped_ptr<webkit_glue::MediaResourceLoaderBridgeFactory> bridge_factory_; + + // Bridge used to load the media resource. + scoped_ptr<webkit_glue::ResourceLoaderBridge> bridge_; + + media::MediaFormat media_format_; + GURL url_; + std::string data_; + int64 size_; + + // Simple state tracking variable. + enum State { + UNINITIALIZED, + INITIALIZING, + INITIALIZED, + STOPPED, + }; + State state_; + + // Used for accessing |state_|. + Lock lock_; + + // Filter callbacks. + scoped_ptr<media::FilterCallback> initialize_callback_; + + DISALLOW_COPY_AND_ASSIGN(SimpleDataSource); +}; + +} // namespace webkit_glue + +#endif // WEBKIT_GLUE_MEDIA_SIMPLE_DATA_SOURCE_H_ 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..e6acba9 --- /dev/null +++ b/webkit/glue/media/simple_data_source_unittest.cc @@ -0,0 +1,252 @@ +// Copyright (c) 2010 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/callback.h" +#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::NiceMock; +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 kHttpsUrl[] = "https://test"; +const char kFileUrl[] = "file://test"; +const char kDataUrl[] = + "data:text/plain;base64,YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoK"; +const char kDataUrlDecoded[] = "abcdefghijklmnopqrstuvwxyz"; +const char kInvalidUrl[] = "whatever://test"; + +} // namespace + +namespace webkit_glue { + +class SimpleDataSourceTest : public testing::Test { + public: + SimpleDataSourceTest() { + bridge_factory_.reset( + new NiceMock<MockMediaResourceLoaderBridgeFactory>()); + bridge_.reset(new NiceMock<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()); + if (bridge_factory_.get()) + EXPECT_CALL(*bridge_factory_, OnDestroy()); + } + + void InitializeDataSource(const char* 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(url), _, -1, -1)) + .WillOnce(Return(bridge_.get())); + EXPECT_CALL(*bridge_, Start(data_source_.get())) + .WillOnce(Return(true)); + + data_source_->Initialize(url, callback_.NewCallback()); + + MessageLoop::current()->RunAllPending(); + } + + void RequestSucceeded(bool is_loaded) { + 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); + + EXPECT_CALL(host_, SetLoaded(is_loaded)); + + 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)); + + StrictMock<media::MockFilterCallback> callback; + EXPECT_CALL(callback, OnFilterCallback()); + EXPECT_CALL(callback, OnCallbackDestroyed()); + data_source_->Stop(callback.NewCallback()); + 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() { + ignore_result(bridge_.release()); + } + + void ReleaseBridgeFactory() { + ignore_result(bridge_factory_.release()); + } + + MOCK_METHOD1(ReadCallback, void(size_t size)); + + protected: + scoped_ptr<MessageLoop> message_loop_; + scoped_ptr<NiceMock<MockMediaResourceLoaderBridgeFactory> > bridge_factory_; + scoped_ptr<NiceMock<MockResourceLoaderBridge> > bridge_; + scoped_refptr<media::FilterFactory> factory_; + scoped_refptr<SimpleDataSource> data_source_; + StrictMock<media::MockFilterHost> host_; + StrictMock<media::MockFilterCallback> callback_; + char data_[kDataSize]; + + DISALLOW_COPY_AND_ASSIGN(SimpleDataSourceTest); +}; + +TEST_F(SimpleDataSourceTest, InitializeHTTP) { + InitializeDataSource(kHttpUrl); + RequestSucceeded(false); + DestroyDataSource(); +} + +TEST_F(SimpleDataSourceTest, InitializeHTTPS) { + InitializeDataSource(kHttpsUrl); + RequestSucceeded(false); + DestroyDataSource(); +} + +TEST_F(SimpleDataSourceTest, InitializeFile) { + InitializeDataSource(kFileUrl); + RequestSucceeded(true); + DestroyDataSource(); +} + +TEST_F(SimpleDataSourceTest, InitializeData) { + media::MediaFormat url_format; + url_format.SetAsString(media::MediaFormat::kMimeType, + media::mime_type::kURL); + url_format.SetAsString(media::MediaFormat::kURL, kDataUrl); + 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_, SetLoaded(true)); + EXPECT_CALL(host_, SetTotalBytes(sizeof(kDataUrlDecoded))); + EXPECT_CALL(host_, SetBufferedBytes(sizeof(kDataUrlDecoded))); + EXPECT_CALL(callback_, OnFilterCallback()); + EXPECT_CALL(callback_, OnCallbackDestroyed()); + + data_source_->Initialize(kDataUrl, callback_.NewCallback()); + MessageLoop::current()->RunAllPending(); + + DestroyDataSource(); +} + +TEST_F(SimpleDataSourceTest, InitializeInvalid) { + 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); + EXPECT_FALSE(data_source_); +} + +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(true); + AsyncRead(); + DestroyDataSource(); +} + +} // namespace webkit_glue diff --git a/webkit/glue/media/video_renderer_impl.cc b/webkit/glue/media/video_renderer_impl.cc new file mode 100644 index 0000000..796d07f --- /dev/null +++ b/webkit/glue/media/video_renderer_impl.cc @@ -0,0 +1,321 @@ +// Copyright (c) 2010 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 "webkit/glue/media/video_renderer_impl.h" + +#include "media/base/video_frame.h" +#include "media/base/yuv_convert.h" +#include "webkit/glue/webmediaplayer_impl.h" + +namespace webkit_glue { + +VideoRendererImpl::VideoRendererImpl(WebMediaPlayerImpl::Proxy* proxy, + bool pts_logging) + : proxy_(proxy), + last_converted_frame_(NULL), + pts_logging_(pts_logging) { + // TODO(hclam): decide whether to do the following line in this thread or + // in the render thread. + proxy_->SetVideoRenderer(this); +} + +// static +media::FilterFactory* VideoRendererImpl::CreateFactory( + WebMediaPlayerImpl::Proxy* proxy, + bool pts_logging) { + return new media::FilterFactoryImpl2<VideoRendererImpl, + WebMediaPlayerImpl::Proxy*, + bool>(proxy, pts_logging); +} + +// static +bool VideoRendererImpl::IsMediaFormatSupported( + const media::MediaFormat& media_format) { + return ParseMediaFormat(media_format, NULL, NULL, NULL, NULL); +} + +bool VideoRendererImpl::OnInitialize(media::VideoDecoder* decoder) { + video_size_.SetSize(width(), height()); + bitmap_.setConfig(SkBitmap::kARGB_8888_Config, width(), height()); + if (bitmap_.allocPixels(NULL, NULL)) { + bitmap_.eraseRGB(0x00, 0x00, 0x00); + return true; + } + + NOTREACHED(); + return false; +} + +void VideoRendererImpl::OnStop(media::FilterCallback* callback) { + if (callback) { + callback->Run(); + delete callback; + } +} + +void VideoRendererImpl::OnFrameAvailable() { + proxy_->Repaint(); +} + +void VideoRendererImpl::SetRect(const gfx::Rect& rect) { +} + +// This method is always called on the renderer's thread. +void VideoRendererImpl::Paint(skia::PlatformCanvas* canvas, + const gfx::Rect& dest_rect) { + scoped_refptr<media::VideoFrame> video_frame; + GetCurrentFrame(&video_frame); + if (!video_frame) { + SkPaint paint; + paint.setColor(SK_ColorBLACK); + canvas->drawRectCoords( + static_cast<float>(dest_rect.x()), + static_cast<float>(dest_rect.y()), + static_cast<float>(dest_rect.right()), + static_cast<float>(dest_rect.bottom()), + paint); + } else { + if (CanFastPaint(canvas, dest_rect)) { + FastPaint(video_frame, canvas, dest_rect); + } else { + SlowPaint(video_frame, canvas, dest_rect); + } + + // Presentation timestamp logging is primarily used to measure performance + // on low-end devices. When profiled on an Intel Atom N280 @ 1.66GHz this + // code had a ~63 microsecond perf hit when logging to a file (not stdout), + // which is neglible enough for measuring playback performance. + if (pts_logging_) { + LOG(INFO) << "pts=" + << video_frame->GetTimestamp().InMicroseconds(); + } + } + + PutCurrentFrame(video_frame); +} + +// CanFastPaint is a helper method to determine the conditions for fast +// painting. The conditions are: +// 1. No skew in canvas matrix. +// 2. No flipping nor mirroring. +// 3. Canvas has pixel format ARGB8888. +// 4. Canvas is opaque. +// TODO(hclam): The fast paint method should support flipping and mirroring. +// Disable the flipping and mirroring checks once we have it. +bool VideoRendererImpl::CanFastPaint(skia::PlatformCanvas* canvas, + const gfx::Rect& dest_rect) { + // Fast paint does not handle opacity value other than 1.0. Hence use slow + // paint if opacity is not 1.0. Since alpha = opacity * 0xFF, we check that + // alpha != 0xFF. + // + // Additonal notes: If opacity = 0.0, the chrome display engine does not try + // to render the video. So, this method is never called. However, if the + // opacity = 0.0001, alpha is again 0, but the display engine tries to render + // the video. If we use Fast paint, the video shows up with opacity = 1.0. + // Hence we use slow paint also in the case where alpha = 0. It would be ideal + // if rendering was never called even for cases where alpha is 0. Created + // bug 48090 for this. + SkCanvas::LayerIter layer_iter(canvas, false); + SkColor sk_color = layer_iter.paint().getColor(); + SkAlpha sk_alpha = SkColorGetA(sk_color); + if (sk_alpha != 0xFF) { + return false; + } + + const SkMatrix& total_matrix = canvas->getTotalMatrix(); + // Perform the following checks here: + // 1. Check for skewing factors of the transformation matrix. They should be + // zero. + // 2. Check for mirroring and flipping. Make sure they are greater than zero. + if (SkScalarNearlyZero(total_matrix.getSkewX()) && + SkScalarNearlyZero(total_matrix.getSkewY()) && + total_matrix.getScaleX() > 0 && + total_matrix.getScaleY() > 0) { + // Get the properties of the SkDevice and the clip rect. + SkDevice* device = canvas->getDevice(); + + // Get the boundary of the device. + SkIRect device_rect; + device->getBounds(&device_rect); + + // Get the pixel config of the device. + const SkBitmap::Config config = device->config(); + // Get the total clip rect associated with the canvas. + const SkRegion& total_clip = canvas->getTotalClip(); + + SkIRect dest_irect; + TransformToSkIRect(canvas->getTotalMatrix(), dest_rect, &dest_irect); + + if (config == SkBitmap::kARGB_8888_Config && device->isOpaque() && + device_rect.contains(total_clip.getBounds())) { + return true; + } + } + + return false; +} + +void VideoRendererImpl::SlowPaint(media::VideoFrame* video_frame, + skia::PlatformCanvas* canvas, + const gfx::Rect& dest_rect) { + // 1. Convert YUV frame to RGB. + base::TimeDelta timestamp = video_frame->GetTimestamp(); + if (video_frame != last_converted_frame_ || + timestamp != last_converted_timestamp_) { + last_converted_frame_ = video_frame; + last_converted_timestamp_ = timestamp; + DCHECK(video_frame->format() == media::VideoFrame::YV12 || + video_frame->format() == media::VideoFrame::YV16); + DCHECK(video_frame->stride(media::VideoFrame::kUPlane) == + video_frame->stride(media::VideoFrame::kVPlane)); + DCHECK(video_frame->planes() == media::VideoFrame::kNumYUVPlanes); + bitmap_.lockPixels(); + media::YUVType yuv_type = + (video_frame->format() == media::VideoFrame::YV12) ? + media::YV12 : media::YV16; + media::ConvertYUVToRGB32(video_frame->data(media::VideoFrame::kYPlane), + video_frame->data(media::VideoFrame::kUPlane), + video_frame->data(media::VideoFrame::kVPlane), + static_cast<uint8*>(bitmap_.getPixels()), + video_frame->width(), + video_frame->height(), + video_frame->stride(media::VideoFrame::kYPlane), + video_frame->stride(media::VideoFrame::kUPlane), + bitmap_.rowBytes(), + yuv_type); + bitmap_.unlockPixels(); + } + + // 2. Paint the bitmap to canvas. + SkMatrix matrix; + matrix.setTranslate(static_cast<SkScalar>(dest_rect.x()), + static_cast<SkScalar>(dest_rect.y())); + if (dest_rect.width() != video_size_.width() || + dest_rect.height() != video_size_.height()) { + matrix.preScale(SkIntToScalar(dest_rect.width()) / + SkIntToScalar(video_size_.width()), + SkIntToScalar(dest_rect.height()) / + SkIntToScalar(video_size_.height())); + } + SkPaint paint; + paint.setFlags(SkPaint::kFilterBitmap_Flag); + canvas->drawBitmapMatrix(bitmap_, matrix, &paint); +} + +void VideoRendererImpl::FastPaint(media::VideoFrame* video_frame, + skia::PlatformCanvas* canvas, + const gfx::Rect& dest_rect) { + DCHECK(video_frame->format() == media::VideoFrame::YV12 || + video_frame->format() == media::VideoFrame::YV16); + DCHECK(video_frame->stride(media::VideoFrame::kUPlane) == + video_frame->stride(media::VideoFrame::kVPlane)); + DCHECK(video_frame->planes() == media::VideoFrame::kNumYUVPlanes); + const SkBitmap& bitmap = canvas->getDevice()->accessBitmap(true); + media::YUVType yuv_type = (video_frame->format() == media::VideoFrame::YV12) ? + media::YV12 : media::YV16; + int y_shift = yuv_type; // 1 for YV12, 0 for YV16. + + // Create a rectangle backed by SkScalar. + SkRect scalar_dest_rect; + scalar_dest_rect.iset(dest_rect.x(), dest_rect.y(), + dest_rect.right(), dest_rect.bottom()); + + // Transform the destination rectangle to local coordinates. + const SkMatrix& local_matrix = canvas->getTotalMatrix(); + SkRect local_dest_rect; + local_matrix.mapRect(&local_dest_rect, scalar_dest_rect); + + // After projecting the destination rectangle to local coordinates, round + // the projected rectangle to integer values, this will give us pixel values + // of the rectangle. + SkIRect local_dest_irect, local_dest_irect_saved; + local_dest_rect.round(&local_dest_irect); + local_dest_rect.round(&local_dest_irect_saved); + + // Only does the paint if the destination rect intersects with the clip + // rect. + if (local_dest_irect.intersect(canvas->getTotalClip().getBounds())) { + // At this point |local_dest_irect| contains the rect that we should draw + // to within the clipping rect. + + // Calculate the address for the top left corner of destination rect in + // the canvas that we will draw to. The address is obtained by the base + // address of the canvas shifted by "left" and "top" of the rect. + uint8* dest_rect_pointer = static_cast<uint8*>(bitmap.getPixels()) + + local_dest_irect.fTop * bitmap.rowBytes() + + local_dest_irect.fLeft * 4; + + // Project the clip rect to the original video frame, obtains the + // dimensions of the projected clip rect, "left" and "top" of the rect. + // The math here are all integer math so we won't have rounding error and + // write outside of the canvas. + // We have the assumptions of dest_rect.width() and dest_rect.height() + // being non-zero, these are valid assumptions since finding intersection + // above rejects empty rectangle so we just do a DCHECK here. + DCHECK_NE(0, dest_rect.width()); + DCHECK_NE(0, dest_rect.height()); + size_t frame_clip_width = local_dest_irect.width() * + video_frame->width() / local_dest_irect_saved.width(); + size_t frame_clip_height = local_dest_irect.height() * + video_frame->height() / local_dest_irect_saved.height(); + + // Project the "left" and "top" of the final destination rect to local + // coordinates of the video frame, use these values to find the offsets + // in the video frame to start reading. + size_t frame_clip_left = + (local_dest_irect.fLeft - local_dest_irect_saved.fLeft) * + video_frame->width() / local_dest_irect_saved.width(); + size_t frame_clip_top = + (local_dest_irect.fTop - local_dest_irect_saved.fTop) * + video_frame->height() / local_dest_irect_saved.height(); + + // Use the "left" and "top" of the destination rect to locate the offset + // in Y, U and V planes. + size_t y_offset = video_frame->stride(media::VideoFrame::kYPlane) * + frame_clip_top + frame_clip_left; + // For format YV12, there is one U, V value per 2x2 block. + // For format YV16, there is one u, V value per 2x1 block. + size_t uv_offset = (video_frame->stride(media::VideoFrame::kUPlane) * + (frame_clip_top >> y_shift)) + (frame_clip_left >> 1); + uint8* frame_clip_y = + video_frame->data(media::VideoFrame::kYPlane) + y_offset; + uint8* frame_clip_u = + video_frame->data(media::VideoFrame::kUPlane) + uv_offset; + uint8* frame_clip_v = + video_frame->data(media::VideoFrame::kVPlane) + uv_offset; + bitmap.lockPixels(); + + // TODO(hclam): do rotation and mirroring here. + // TODO(fbarchard): switch filtering based on performance. + media::ScaleYUVToRGB32(frame_clip_y, + frame_clip_u, + frame_clip_v, + dest_rect_pointer, + frame_clip_width, + frame_clip_height, + local_dest_irect.width(), + local_dest_irect.height(), + video_frame->stride(media::VideoFrame::kYPlane), + video_frame->stride(media::VideoFrame::kUPlane), + bitmap.rowBytes(), + yuv_type, + media::ROTATE_0, + media::FILTER_BILINEAR); + bitmap.unlockPixels(); + } +} + +void VideoRendererImpl::TransformToSkIRect(const SkMatrix& matrix, + const gfx::Rect& src_rect, + SkIRect* dest_rect) { + // Transform destination rect to local coordinates. + SkRect transformed_rect; + SkRect skia_dest_rect; + skia_dest_rect.iset(src_rect.x(), src_rect.y(), + src_rect.right(), src_rect.bottom()); + matrix.mapRect(&transformed_rect, skia_dest_rect); + transformed_rect.round(dest_rect); +} + +} // namespace webkit_glue diff --git a/webkit/glue/media/video_renderer_impl.h b/webkit/glue/media/video_renderer_impl.h new file mode 100644 index 0000000..30f2e38 --- /dev/null +++ b/webkit/glue/media/video_renderer_impl.h @@ -0,0 +1,122 @@ +// Copyright (c) 2010 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. +// +// The video renderer implementation to be use by the media pipeline. It lives +// inside video renderer thread and also WebKit's main thread. We need to be +// extra careful about members shared by two different threads, especially +// video frame buffers. + +#ifndef WEBKIT_GLUE_MEDIA_VIDEO_RENDERER_IMPL_H_ +#define WEBKIT_GLUE_MEDIA_VIDEO_RENDERER_IMPL_H_ + +#include "gfx/rect.h" +#include "gfx/size.h" +#include "media/base/buffers.h" +#include "media/base/filters.h" +#include "media/filters/video_renderer_base.h" +#include "skia/ext/platform_canvas.h" +#include "third_party/WebKit/WebKit/chromium/public/WebMediaPlayer.h" +#include "webkit/glue/media/web_video_renderer.h" +#include "webkit/glue/webmediaplayer_impl.h" + +namespace webkit_glue { + +class VideoRendererImpl : public WebVideoRenderer { + public: + // WebVideoRenderer implementation. + virtual void SetRect(const gfx::Rect& rect); + virtual void Paint(skia::PlatformCanvas* canvas, const gfx::Rect& dest_rect); + + // Static method for creating factory for this object. + static media::FilterFactory* CreateFactory(WebMediaPlayerImpl::Proxy* proxy, + bool pts_logging); + + // FilterFactoryImpl2 implementation. + static bool IsMediaFormatSupported(const media::MediaFormat& media_format); + + // TODO(scherkus): remove this mega-hack, see http://crbug.com/28207 + class FactoryFactory : public webkit_glue::WebVideoRendererFactoryFactory { + public: + FactoryFactory(bool pts_logging) + : webkit_glue::WebVideoRendererFactoryFactory(), + pts_logging_(pts_logging) { + } + + virtual media::FilterFactory* CreateFactory( + webkit_glue::WebMediaPlayerImpl::Proxy* proxy) { + return VideoRendererImpl::CreateFactory(proxy, pts_logging_); + } + + private: + // Whether we're logging video presentation timestamps (PTS). + bool pts_logging_; + + DISALLOW_COPY_AND_ASSIGN(FactoryFactory); + }; + + protected: + // Method called by VideoRendererBase during initialization. + virtual bool OnInitialize(media::VideoDecoder* decoder); + + // Method called by the VideoRendererBase when stopping. + virtual void OnStop(media::FilterCallback* callback); + + // Method called by the VideoRendererBase when a frame is available. + virtual void OnFrameAvailable(); + + private: + // Only the filter factories can create instances. + friend class media::FilterFactoryImpl2<VideoRendererImpl, + WebMediaPlayerImpl::Proxy*, + bool>; + VideoRendererImpl(WebMediaPlayerImpl::Proxy* proxy, bool pts_logging); + virtual ~VideoRendererImpl() {} + + // Determine the conditions to perform fast paint. Returns true if we can do + // fast paint otherwise false. + bool CanFastPaint(skia::PlatformCanvas* canvas, const gfx::Rect& dest_rect); + + // Slow paint does a YUV => RGB, and scaled blit in two separate operations. + void SlowPaint(media::VideoFrame* video_frame, + skia::PlatformCanvas* canvas, + const gfx::Rect& dest_rect); + + // Fast paint does YUV => RGB, scaling, blitting all in one step into the + // canvas. It's not always safe and appropriate to perform fast paint. + // CanFastPaint() is used to determine the conditions. + void FastPaint(media::VideoFrame* video_frame, + skia::PlatformCanvas* canvas, + const gfx::Rect& dest_rect); + + void TransformToSkIRect(const SkMatrix& matrix, const gfx::Rect& src_rect, + SkIRect* dest_rect); + + // Pointer to our parent object that is called to request repaints. + scoped_refptr<WebMediaPlayerImpl::Proxy> proxy_; + + // An RGB bitmap used to convert the video frames. + SkBitmap bitmap_; + + // These two members are used to determine if the |bitmap_| contains + // an already converted image of the current frame. IMPORTANT NOTE: The + // value of |last_converted_frame_| must only be used for comparison purposes, + // and it should be assumed that the value of the pointer is INVALID unless + // it matches the pointer returned from GetCurrentFrame(). Even then, just + // to make sure, we compare the timestamp to be sure the bits in the + // |current_frame_bitmap_| are valid. + media::VideoFrame* last_converted_frame_; + base::TimeDelta last_converted_timestamp_; + + // The size of the video. + gfx::Size video_size_; + + // Whether we're logging video presentation timestamps (PTS). + bool pts_logging_; + + DISALLOW_COPY_AND_ASSIGN(VideoRendererImpl); +}; + +} // namespace webkit_glue + +#endif // WEBKIT_GLUE_MEDIA_VIDEO_RENDERER_IMPL_H_ diff --git a/webkit/glue/media/web_video_renderer.h b/webkit/glue/media/web_video_renderer.h new file mode 100644 index 0000000..8bafb1a --- /dev/null +++ b/webkit/glue/media/web_video_renderer.h @@ -0,0 +1,39 @@ +// Copyright (c) 2010 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_WEB_VIDEO_RENDERER_H_ +#define WEBKIT_GLUE_MEDIA_WEB_VIDEO_RENDERER_H_ + +#include "media/filters/video_renderer_base.h" + +namespace webkit_glue { + +// A specialized version of a VideoRenderer designed to be used inside WebKit. +class WebVideoRenderer : public media::VideoRendererBase { + public: + WebVideoRenderer() : media::VideoRendererBase() {} + virtual ~WebVideoRenderer() {} + + // This method is called with the same rect as the Paint() method and could + // be used by future implementations to implement an improved color space + + // scale code on a separate thread. Since we always do the stretch on the + // same thread as the Paint method, we just ignore the call for now. + // + // Method called on the render thread. + virtual void SetRect(const gfx::Rect& rect) = 0; + + // Paint the current front frame on the |canvas| stretching it to fit the + // |dest_rect|. + // + // Method called on the render thread. + virtual void Paint(skia::PlatformCanvas* canvas, + const gfx::Rect& dest_rect) = 0; + + private: + DISALLOW_COPY_AND_ASSIGN(WebVideoRenderer); +}; + +} // namespace webkit_glue + +#endif // WEBKIT_GLUE_MEDIA_WEB_VIDEO_RENDERER_H_ |