| // Copyright 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "media/blink/buffered_data_source.h" |
| |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/callback_helpers.h" |
| #include "base/location.h" |
| #include "base/macros.h" |
| #include "base/single_thread_task_runner.h" |
| #include "build/build_config.h" |
| #include "media/base/media_log.h" |
| #include "net/base/net_errors.h" |
| |
| using blink::WebFrame; |
| |
| namespace { |
| |
| // 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; |
| |
| // The number of milliseconds to wait before retrying a failed load. |
| const int kLoaderFailedRetryDelayMs = 250; |
| |
| // Each retry, add this many MS to the delay. |
| // total delay is: |
| // (kLoaderPartialRetryDelayMs + |
| // kAdditionalDelayPerRetryMs * (kMaxRetries - 1) / 2) * kLoaderRetries |
| // = 29250 ms |
| const int kAdditionalDelayPerRetryMs = 50; |
| |
| } // namespace |
| |
| namespace media { |
| |
| class BufferedDataSource::ReadOperation { |
| public: |
| ReadOperation(int64_t position, |
| int size, |
| uint8_t* data, |
| const DataSource::ReadCB& callback); |
| ~ReadOperation(); |
| |
| // Runs |callback_| with the given |result|, deleting the operation |
| // afterwards. |
| static void Run(std::unique_ptr<ReadOperation> read_op, int result); |
| |
| // State for the number of times this read operation has been retried. |
| int retries() { return retries_; } |
| void IncrementRetries() { ++retries_; } |
| |
| int64_t position() { return position_; } |
| int size() { return size_; } |
| uint8_t* data() { return data_; } |
| |
| private: |
| int retries_; |
| |
| const int64_t position_; |
| const int size_; |
| uint8_t* data_; |
| DataSource::ReadCB callback_; |
| |
| DISALLOW_IMPLICIT_CONSTRUCTORS(ReadOperation); |
| }; |
| |
| BufferedDataSource::ReadOperation::ReadOperation( |
| int64_t position, |
| int size, |
| uint8_t* data, |
| const DataSource::ReadCB& callback) |
| : retries_(0), |
| position_(position), |
| size_(size), |
| data_(data), |
| callback_(callback) { |
| DCHECK(!callback_.is_null()); |
| } |
| |
| BufferedDataSource::ReadOperation::~ReadOperation() { |
| DCHECK(callback_.is_null()); |
| } |
| |
| // static |
| void BufferedDataSource::ReadOperation::Run( |
| std::unique_ptr<ReadOperation> read_op, |
| int result) { |
| base::ResetAndReturn(&read_op->callback_).Run(result); |
| } |
| |
| BufferedDataSource::BufferedDataSource( |
| const GURL& url, |
| BufferedResourceLoader::CORSMode cors_mode, |
| const scoped_refptr<base::SingleThreadTaskRunner>& task_runner, |
| WebFrame* frame, |
| MediaLog* media_log, |
| BufferedDataSourceHost* host, |
| const DownloadingCB& downloading_cb) |
| : url_(url), |
| cors_mode_(cors_mode), |
| total_bytes_(kPositionNotSpecified), |
| streaming_(false), |
| frame_(frame), |
| intermediate_read_buffer_(kInitialReadBufferSize), |
| render_task_runner_(task_runner), |
| stop_signal_received_(false), |
| media_has_played_(false), |
| buffering_strategy_(BUFFERING_STRATEGY_NORMAL), |
| preload_(AUTO), |
| bitrate_(0), |
| playback_rate_(0.0), |
| media_log_(media_log), |
| host_(host), |
| downloading_cb_(downloading_cb), |
| weak_factory_(this) { |
| weak_ptr_ = weak_factory_.GetWeakPtr(); |
| DCHECK(host_); |
| DCHECK(!downloading_cb_.is_null()); |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| } |
| |
| BufferedDataSource::~BufferedDataSource() { |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| } |
| |
| bool BufferedDataSource::media_has_played() const { |
| return media_has_played_; |
| } |
| |
| bool BufferedDataSource::assume_fully_buffered() { |
| return !url_.SchemeIsHTTPOrHTTPS(); |
| } |
| |
| // A factory method to create BufferedResourceLoader using the read parameters. |
| // This method can be overridden to inject mock BufferedResourceLoader object |
| // for testing purpose. |
| BufferedResourceLoader* BufferedDataSource::CreateResourceLoader( |
| int64_t first_byte_position, |
| int64_t last_byte_position) { |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| |
| BufferedResourceLoader::DeferStrategy strategy = preload_ == METADATA ? |
| BufferedResourceLoader::kReadThenDefer : |
| BufferedResourceLoader::kCapacityDefer; |
| |
| return new BufferedResourceLoader(url_, |
| cors_mode_, |
| first_byte_position, |
| last_byte_position, |
| strategy, |
| bitrate_, |
| playback_rate_, |
| media_log_.get()); |
| } |
| |
| void BufferedDataSource::Initialize(const InitializeCB& init_cb) { |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| DCHECK(!init_cb.is_null()); |
| DCHECK(!loader_.get()); |
| |
| init_cb_ = init_cb; |
| |
| if (url_.SchemeIsHTTPOrHTTPS()) { |
| // Do an unbounded range request starting at the beginning. If the server |
| // responds with 200 instead of 206 we'll fall back into a streaming mode. |
| loader_.reset(CreateResourceLoader(0, kPositionNotSpecified)); |
| } 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_.reset(CreateResourceLoader(kPositionNotSpecified, |
| kPositionNotSpecified)); |
| } |
| |
| base::WeakPtr<BufferedDataSource> weak_this = weak_factory_.GetWeakPtr(); |
| loader_->Start( |
| base::Bind(&BufferedDataSource::StartCallback, weak_this), |
| base::Bind(&BufferedDataSource::LoadingStateChangedCallback, weak_this), |
| base::Bind(&BufferedDataSource::ProgressCallback, weak_this), |
| frame_); |
| } |
| |
| void BufferedDataSource::SetPreload(Preload preload) { |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| preload_ = preload; |
| } |
| |
| void BufferedDataSource::SetBufferingStrategy( |
| BufferingStrategy buffering_strategy) { |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| buffering_strategy_ = buffering_strategy; |
| UpdateDeferStrategy(); |
| } |
| |
| bool BufferedDataSource::HasSingleOrigin() { |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| DCHECK(init_cb_.is_null() && loader_.get()) |
| << "Initialize() must complete before calling HasSingleOrigin()"; |
| return loader_->HasSingleOrigin(); |
| } |
| |
| bool BufferedDataSource::DidPassCORSAccessCheck() const { |
| return loader_.get() && loader_->DidPassCORSAccessCheck(); |
| } |
| |
| void BufferedDataSource::Abort() { |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| { |
| base::AutoLock auto_lock(lock_); |
| StopInternal_Locked(); |
| } |
| StopLoader(); |
| frame_ = NULL; |
| } |
| |
| void BufferedDataSource::MediaPlaybackRateChanged(double playback_rate) { |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| DCHECK(loader_.get()); |
| |
| if (playback_rate < 0.0) |
| return; |
| |
| playback_rate_ = playback_rate; |
| loader_->SetPlaybackRate(playback_rate); |
| } |
| |
| void BufferedDataSource::MediaIsPlaying() { |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| media_has_played_ = true; |
| UpdateDeferStrategy(); |
| } |
| |
| ///////////////////////////////////////////////////////////////////////////// |
| // DataSource implementation. |
| void BufferedDataSource::Stop() { |
| { |
| base::AutoLock auto_lock(lock_); |
| StopInternal_Locked(); |
| } |
| |
| render_task_runner_->PostTask( |
| FROM_HERE, |
| base::Bind(&BufferedDataSource::StopLoader, weak_factory_.GetWeakPtr())); |
| } |
| |
| void BufferedDataSource::SetBitrate(int bitrate) { |
| render_task_runner_->PostTask(FROM_HERE, |
| base::Bind(&BufferedDataSource::SetBitrateTask, |
| weak_factory_.GetWeakPtr(), |
| bitrate)); |
| } |
| |
| void BufferedDataSource::OnBufferingHaveEnough(bool always_cancel) { |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| if (loader_ && (always_cancel || (preload_ == METADATA && |
| !media_has_played_ && !IsStreaming()))) { |
| loader_->CancelUponDeferral(); |
| } |
| } |
| |
| int64_t BufferedDataSource::GetMemoryUsage() const { |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| return loader_ ? loader_->GetMemoryUsage() : 0; |
| } |
| |
| void BufferedDataSource::Read(int64_t position, |
| int size, |
| uint8_t* data, |
| const DataSource::ReadCB& read_cb) { |
| DVLOG(1) << "Read: " << position << " offset, " << size << " bytes"; |
| DCHECK(!read_cb.is_null()); |
| |
| { |
| base::AutoLock auto_lock(lock_); |
| DCHECK(!read_op_); |
| |
| if (stop_signal_received_) { |
| read_cb.Run(kReadError); |
| return; |
| } |
| |
| read_op_.reset(new ReadOperation(position, size, data, read_cb)); |
| } |
| |
| render_task_runner_->PostTask( |
| FROM_HERE, |
| base::Bind(&BufferedDataSource::ReadTask, weak_factory_.GetWeakPtr())); |
| } |
| |
| bool BufferedDataSource::GetSize(int64_t* size_out) { |
| if (total_bytes_ != kPositionNotSpecified) { |
| *size_out = total_bytes_; |
| return true; |
| } |
| *size_out = 0; |
| return false; |
| } |
| |
| bool BufferedDataSource::IsStreaming() { |
| return streaming_; |
| } |
| |
| ///////////////////////////////////////////////////////////////////////////// |
| // Render thread tasks. |
| void BufferedDataSource::ReadTask() { |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| ReadInternal(); |
| } |
| |
| void BufferedDataSource::StopInternal_Locked() { |
| lock_.AssertAcquired(); |
| if (stop_signal_received_) |
| return; |
| |
| stop_signal_received_ = true; |
| |
| // Initialize() isn't part of the DataSource interface so don't call it in |
| // response to Stop(). |
| init_cb_.Reset(); |
| |
| if (read_op_) |
| ReadOperation::Run(std::move(read_op_), kReadError); |
| } |
| |
| void BufferedDataSource::StopLoader() { |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| |
| if (loader_) |
| loader_->Stop(); |
| } |
| |
| void BufferedDataSource::SetBitrateTask(int bitrate) { |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| DCHECK(loader_.get()); |
| |
| bitrate_ = bitrate; |
| loader_->SetBitrate(bitrate); |
| } |
| |
| // This method is the place where actual read happens, |loader_| must be valid |
| // prior to make this method call. |
| void BufferedDataSource::ReadInternal() { |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| int64_t position = 0; |
| int size = 0; |
| { |
| base::AutoLock auto_lock(lock_); |
| if (stop_signal_received_) |
| return; |
| |
| position = read_op_->position(); |
| size = read_op_->size(); |
| } |
| |
| // First we prepare the intermediate read buffer for BufferedResourceLoader |
| // to write to. |
| if (static_cast<int>(intermediate_read_buffer_.size()) < size) |
| intermediate_read_buffer_.resize(size); |
| |
| // Perform the actual read with BufferedResourceLoader. |
| DCHECK(!intermediate_read_buffer_.empty()); |
| loader_->Read(position, |
| size, |
| &intermediate_read_buffer_[0], |
| base::Bind(&BufferedDataSource::ReadCallback, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| |
| ///////////////////////////////////////////////////////////////////////////// |
| // BufferedResourceLoader callback methods. |
| void BufferedDataSource::StartCallback( |
| BufferedResourceLoader::Status status) { |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| DCHECK(loader_.get()); |
| |
| bool init_cb_is_null = false; |
| { |
| base::AutoLock auto_lock(lock_); |
| init_cb_is_null = init_cb_.is_null(); |
| } |
| if (init_cb_is_null) { |
| loader_->Stop(); |
| return; |
| } |
| |
| response_original_url_ = loader_->response_original_url(); |
| #if defined(OS_ANDROID) |
| // The response original url is the URL of this resource after following |
| // redirects. Update |url_| to this so that we only follow redirects once. |
| // We do this on Android only to preserve the behavior we had before the |
| // unified media pipeline. This behavior will soon exist on all platforms |
| // as we switch to MultiBufferDataSource (http://crbug.com/514719). |
| // If the response URL is empty (which happens when it's from a Service |
| // Worker), keep the original one. |
| if (!response_original_url_.is_empty()) |
| url_ = response_original_url_; |
| #endif // defined(OS_ANDROID) |
| |
| // All responses must be successful. Resources that are assumed to be fully |
| // buffered must have a known content length. |
| bool success = status == BufferedResourceLoader::kOk && |
| (!assume_fully_buffered() || |
| loader_->instance_size() != kPositionNotSpecified); |
| |
| if (success) { |
| total_bytes_ = loader_->instance_size(); |
| streaming_ = |
| !assume_fully_buffered() && |
| (total_bytes_ == kPositionNotSpecified || !loader_->range_supported()); |
| |
| media_log_->SetDoubleProperty("total_bytes", |
| static_cast<double>(total_bytes_)); |
| media_log_->SetBooleanProperty("streaming", streaming_); |
| } else { |
| loader_->Stop(); |
| } |
| |
| // TODO(scherkus): we shouldn't have to lock to signal host(), see |
| // http://crbug.com/113712 for details. |
| base::AutoLock auto_lock(lock_); |
| if (stop_signal_received_) |
| return; |
| |
| if (success) { |
| if (total_bytes_ != kPositionNotSpecified) { |
| host_->SetTotalBytes(total_bytes_); |
| if (assume_fully_buffered()) |
| host_->AddBufferedByteRange(0, total_bytes_); |
| } |
| |
| media_log_->SetBooleanProperty("single_origin", loader_->HasSingleOrigin()); |
| media_log_->SetBooleanProperty("passed_cors_access_check", |
| loader_->DidPassCORSAccessCheck()); |
| media_log_->SetBooleanProperty("range_header_supported", |
| loader_->range_supported()); |
| } |
| |
| render_task_runner_->PostTask( |
| FROM_HERE, base::Bind(base::ResetAndReturn(&init_cb_), success)); |
| } |
| |
| void BufferedDataSource::PartialReadStartCallback( |
| BufferedResourceLoader::Status status) { |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| DCHECK(loader_.get()); |
| if (status == BufferedResourceLoader::kOk && |
| CheckPartialResponseURL(loader_->response_original_url())) { |
| // Once the 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(); |
| |
| // TODO(scherkus): we shouldn't have to lock to signal host(), see |
| // http://crbug.com/113712 for details. |
| base::AutoLock auto_lock(lock_); |
| if (stop_signal_received_) |
| return; |
| ReadOperation::Run(std::move(read_op_), kReadError); |
| } |
| |
| bool BufferedDataSource::CheckPartialResponseURL( |
| const GURL& partial_response_original_url) const { |
| // We check the redirected URL of partial responses in case malicious |
| // attackers scan the bytes of other origin resources by mixing their |
| // generated bytes and the target response. See http://crbug.com/489060#c32 |
| // for details. |
| // If the origin of the new response is different from the first response we |
| // deny the redirected response unless the crossorigin attribute has been set. |
| if ((response_original_url_.GetOrigin() == |
| partial_response_original_url.GetOrigin()) || |
| DidPassCORSAccessCheck()) { |
| return true; |
| } |
| |
| MEDIA_LOG(ERROR, media_log_) << "BufferedDataSource: origin has changed"; |
| return false; |
| } |
| |
| void BufferedDataSource::ReadCallback( |
| BufferedResourceLoader::Status status, |
| int bytes_read) { |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| |
| // TODO(scherkus): we shouldn't have to lock to signal host(), see |
| // http://crbug.com/113712 for details. |
| base::AutoLock auto_lock(lock_); |
| if (stop_signal_received_) |
| return; |
| |
| if (status != BufferedResourceLoader::kOk) { |
| // Stop the resource load if it failed. |
| loader_->Stop(); |
| |
| if (read_op_->retries() < kLoaderRetries) { |
| // Allow some resiliency against sporadic network failures or intentional |
| // cancellations due to a system suspend / resume. Here we treat failed |
| // reads as a cache miss so long as we haven't exceeded max retries. |
| if (status == BufferedResourceLoader::kFailed) { |
| render_task_runner_->PostDelayedTask( |
| FROM_HERE, base::Bind(&BufferedDataSource::ReadCallback, |
| weak_factory_.GetWeakPtr(), |
| BufferedResourceLoader::kCacheMiss, 0), |
| base::TimeDelta::FromMilliseconds(kLoaderFailedRetryDelayMs + |
| read_op_->retries() * |
| kAdditionalDelayPerRetryMs)); |
| return; |
| } |
| |
| read_op_->IncrementRetries(); |
| |
| // Recreate a loader starting from where we last left off until the |
| // end of the resource. |
| loader_.reset(CreateResourceLoader( |
| read_op_->position(), kPositionNotSpecified)); |
| |
| base::WeakPtr<BufferedDataSource> weak_this = weak_factory_.GetWeakPtr(); |
| loader_->Start( |
| base::Bind(&BufferedDataSource::PartialReadStartCallback, weak_this), |
| base::Bind(&BufferedDataSource::LoadingStateChangedCallback, |
| weak_this), |
| base::Bind(&BufferedDataSource::ProgressCallback, weak_this), |
| frame_); |
| return; |
| } |
| |
| ReadOperation::Run(std::move(read_op_), kReadError); |
| return; |
| } |
| |
| if (bytes_read > 0) { |
| DCHECK(!intermediate_read_buffer_.empty()); |
| memcpy(read_op_->data(), &intermediate_read_buffer_[0], bytes_read); |
| } else if (bytes_read == 0 && total_bytes_ == kPositionNotSpecified) { |
| // We've reached the end of the file and we didn't know the total size |
| // before. Update the total size so Read()s past the end of the file will |
| // fail like they would if we had known the file size at the beginning. |
| total_bytes_ = loader_->instance_size(); |
| |
| if (total_bytes_ != kPositionNotSpecified) { |
| host_->SetTotalBytes(total_bytes_); |
| host_->AddBufferedByteRange(loader_->first_byte_position(), |
| total_bytes_); |
| } |
| } |
| ReadOperation::Run(std::move(read_op_), bytes_read); |
| } |
| |
| void BufferedDataSource::LoadingStateChangedCallback( |
| BufferedResourceLoader::LoadingState state) { |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| |
| if (assume_fully_buffered()) |
| return; |
| |
| bool is_downloading_data = false; |
| switch (state) { |
| case BufferedResourceLoader::kLoading: |
| is_downloading_data = true; |
| break; |
| case BufferedResourceLoader::kLoadingDeferred: |
| case BufferedResourceLoader::kLoadingFinished: |
| is_downloading_data = false; |
| break; |
| |
| // TODO(scherkus): we don't signal network activity changes when loads |
| // fail to preserve existing behaviour when deferring is toggled, however |
| // we should consider changing DownloadingCB to also propagate loading |
| // state. For example there isn't any signal today to notify the client that |
| // loading has failed (we only get errors on subsequent reads). |
| case BufferedResourceLoader::kLoadingFailed: |
| return; |
| } |
| |
| downloading_cb_.Run(is_downloading_data); |
| } |
| |
| void BufferedDataSource::ProgressCallback(int64_t position) { |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| |
| if (assume_fully_buffered()) |
| return; |
| |
| // TODO(scherkus): we shouldn't have to lock to signal host(), see |
| // http://crbug.com/113712 for details. |
| base::AutoLock auto_lock(lock_); |
| if (stop_signal_received_) |
| return; |
| |
| host_->AddBufferedByteRange(loader_->first_byte_position(), position); |
| } |
| |
| void BufferedDataSource::UpdateDeferStrategy() { |
| if (!loader_) |
| return; |
| |
| // No need to aggressively buffer when we are assuming the resource is fully |
| // buffered. |
| if (assume_fully_buffered()) { |
| loader_->UpdateDeferStrategy(BufferedResourceLoader::kCapacityDefer); |
| return; |
| } |
| |
| // If the playback has started (at which point the preload value is ignored) |
| // and the strategy is aggressive, then try to load as much as possible (the |
| // loader will fall back to kCapacityDefer if it knows the current response |
| // won't be useful from the cache in the future). |
| bool aggressive = (buffering_strategy_ == BUFFERING_STRATEGY_AGGRESSIVE); |
| if (media_has_played_ && aggressive && loader_->range_supported()) { |
| loader_->UpdateDeferStrategy(BufferedResourceLoader::kNeverDefer); |
| return; |
| } |
| |
| // If media is currently playing or the page indicated preload=auto or the |
| // the server does not support the byte range request or we do not want to go |
| // too far ahead of the read head, use threshold strategy to enable/disable |
| // deferring when the buffer is full/depleted. |
| loader_->UpdateDeferStrategy(BufferedResourceLoader::kCapacityDefer); |
| } |
| |
| } // namespace media |