| // 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/multibuffer_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 "media/base/media_log.h" |
| #include "media/blink/buffered_data_source_host_impl.h" |
| #include "media/blink/multibuffer_reader.h" |
| #include "net/base/net_errors.h" |
| |
| using blink::WebFrame; |
| |
| namespace { |
| |
| // Minimum preload buffer. |
| const int64_t kMinBufferPreload = 2 << 20; // 2 Mb |
| // Maxmimum preload buffer. |
| const int64_t kMaxBufferPreload = 50 << 20; // 50 Mb |
| |
| // Preload this much extra, then stop preloading until we fall below the |
| // kTargetSecondsBufferedAhead. |
| const int64_t kPreloadHighExtra = 1 << 20; // 1 Mb |
| |
| // Default pin region size. |
| // Note that we go over this if preload is calculated high enough. |
| const int64_t kDefaultPinSize = 25 << 20; // 25 Mb |
| |
| // If bitrate is not known, use this. |
| const int64_t kDefaultBitrate = 200 * 8 << 10; // 200 Kbps. |
| |
| // Maximum bitrate for buffer calculations. |
| const int64_t kMaxBitrate = 20 * 8 << 20; // 20 Mbps. |
| |
| // Maximum playback rate for buffer calculations. |
| const double kMaxPlaybackRate = 25.0; |
| |
| // Preload this many seconds of data by default. |
| const int64_t kTargetSecondsBufferedAhead = 10; |
| |
| // Keep this many seconds of data for going back by default. |
| const int64_t kTargetSecondsBufferedBehind = 2; |
| |
| } // namespace |
| |
| namespace media { |
| |
| template <typename T> |
| T clamp(T value, T min, T max) { |
| return std::max(std::min(value, max), min); |
| } |
| |
| class MultibufferDataSource::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); |
| |
| int64_t position() { return position_; } |
| int size() { return size_; } |
| uint8_t* data() { return data_; } |
| |
| private: |
| const int64_t position_; |
| const int size_; |
| uint8_t* data_; |
| DataSource::ReadCB callback_; |
| |
| DISALLOW_IMPLICIT_CONSTRUCTORS(ReadOperation); |
| }; |
| |
| MultibufferDataSource::ReadOperation::ReadOperation( |
| int64_t position, |
| int size, |
| uint8_t* data, |
| const DataSource::ReadCB& callback) |
| : position_(position), size_(size), data_(data), callback_(callback) { |
| DCHECK(!callback_.is_null()); |
| } |
| |
| MultibufferDataSource::ReadOperation::~ReadOperation() { |
| DCHECK(callback_.is_null()); |
| } |
| |
| // static |
| void MultibufferDataSource::ReadOperation::Run( |
| std::unique_ptr<ReadOperation> read_op, |
| int result) { |
| base::ResetAndReturn(&read_op->callback_).Run(result); |
| } |
| |
| MultibufferDataSource::MultibufferDataSource( |
| const GURL& url, |
| UrlData::CORSMode cors_mode, |
| const scoped_refptr<base::SingleThreadTaskRunner>& task_runner, |
| linked_ptr<UrlIndex> url_index, |
| WebFrame* frame, |
| MediaLog* media_log, |
| BufferedDataSourceHost* host, |
| const DownloadingCB& downloading_cb) |
| : cors_mode_(cors_mode), |
| total_bytes_(kPositionNotSpecified), |
| streaming_(false), |
| loading_(false), |
| failed_(false), |
| render_task_runner_(task_runner), |
| url_index_(url_index), |
| frame_(frame), |
| stop_signal_received_(false), |
| media_has_played_(false), |
| buffering_strategy_(BUFFERING_STRATEGY_NORMAL), |
| single_origin_(true), |
| cancel_on_defer_(false), |
| 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()); |
| url_data_ = url_index_->GetByUrl(url, cors_mode_); |
| url_data_->Use(); |
| DCHECK(url_data_); |
| url_data_->OnRedirect( |
| base::Bind(&MultibufferDataSource::OnRedirect, weak_ptr_)); |
| } |
| |
| MultibufferDataSource::~MultibufferDataSource() { |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| } |
| |
| bool MultibufferDataSource::media_has_played() const { |
| return media_has_played_; |
| } |
| |
| bool MultibufferDataSource::assume_fully_buffered() { |
| return !url_data_->url().SchemeIsHTTPOrHTTPS(); |
| } |
| |
| void MultibufferDataSource::CreateResourceLoader(int64_t first_byte_position, |
| int64_t last_byte_position) { |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| |
| reader_.reset(new MultiBufferReader( |
| url_data_->multibuffer(), first_byte_position, last_byte_position, |
| base::Bind(&MultibufferDataSource::ProgressCallback, weak_ptr_))); |
| UpdateBufferSizes(); |
| } |
| |
| void MultibufferDataSource::Initialize(const InitializeCB& init_cb) { |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| DCHECK(!init_cb.is_null()); |
| DCHECK(!reader_.get()); |
| |
| init_cb_ = init_cb; |
| |
| CreateResourceLoader(0, kPositionNotSpecified); |
| |
| // We're not allowed to call Wait() if data is already available. |
| if (reader_->Available()) { |
| render_task_runner_->PostTask( |
| FROM_HERE, |
| base::Bind(&MultibufferDataSource::StartCallback, weak_ptr_)); |
| |
| // When the entire file is already in the cache, we won't get any more |
| // progress callbacks, which breaks some expectations. Post a task to |
| // make sure that the client gets at least one call each for the progress |
| // and loading callbacks. |
| render_task_runner_->PostTask( |
| FROM_HERE, base::Bind(&MultibufferDataSource::UpdateProgress, |
| weak_factory_.GetWeakPtr())); |
| } else { |
| reader_->Wait(1, |
| base::Bind(&MultibufferDataSource::StartCallback, weak_ptr_)); |
| } |
| } |
| |
| void MultibufferDataSource::OnRedirect( |
| const scoped_refptr<UrlData>& destination) { |
| if (!destination) { |
| // A failure occured. |
| failed_ = true; |
| if (!init_cb_.is_null()) { |
| render_task_runner_->PostTask( |
| FROM_HERE, |
| base::Bind(&MultibufferDataSource::StartCallback, weak_ptr_)); |
| } else { |
| base::AutoLock auto_lock(lock_); |
| StopInternal_Locked(); |
| } |
| StopLoader(); |
| return; |
| } |
| if (url_data_->url().GetOrigin() != destination->url().GetOrigin()) { |
| single_origin_ = false; |
| } |
| reader_.reset(nullptr); |
| url_data_ = destination; |
| |
| if (url_data_) { |
| url_data_->OnRedirect( |
| base::Bind(&MultibufferDataSource::OnRedirect, weak_ptr_)); |
| |
| if (!init_cb_.is_null()) { |
| CreateResourceLoader(0, kPositionNotSpecified); |
| if (reader_->Available()) { |
| render_task_runner_->PostTask( |
| FROM_HERE, |
| base::Bind(&MultibufferDataSource::StartCallback, weak_ptr_)); |
| } else { |
| reader_->Wait( |
| 1, base::Bind(&MultibufferDataSource::StartCallback, weak_ptr_)); |
| } |
| } else if (read_op_) { |
| CreateResourceLoader(read_op_->position(), kPositionNotSpecified); |
| if (reader_->Available()) { |
| render_task_runner_->PostTask( |
| FROM_HERE, base::Bind(&MultibufferDataSource::ReadTask, weak_ptr_)); |
| } else { |
| reader_->Wait(1, |
| base::Bind(&MultibufferDataSource::ReadTask, weak_ptr_)); |
| } |
| } |
| } |
| } |
| |
| void MultibufferDataSource::SetPreload(Preload preload) { |
| DVLOG(1) << __func__ << "(" << preload << ")"; |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| preload_ = preload; |
| UpdateBufferSizes(); |
| } |
| |
| void MultibufferDataSource::SetBufferingStrategy( |
| BufferingStrategy buffering_strategy) { |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| buffering_strategy_ = buffering_strategy; |
| UpdateBufferSizes(); |
| } |
| |
| bool MultibufferDataSource::HasSingleOrigin() { |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| // Before initialization completes there is no risk of leaking data. Callers |
| // are required to order checks such that this isn't a race. |
| return single_origin_; |
| } |
| |
| bool MultibufferDataSource::DidPassCORSAccessCheck() const { |
| if (cors_mode_ == UrlData::CORS_UNSPECIFIED) |
| return false; |
| // If init_cb is set, we initialization is not finished yet. |
| if (!init_cb_.is_null()) |
| return false; |
| if (failed_) |
| return false; |
| return true; |
| } |
| |
| void MultibufferDataSource::MediaPlaybackRateChanged(double playback_rate) { |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| |
| if (playback_rate < 0.0) |
| return; |
| |
| playback_rate_ = playback_rate; |
| cancel_on_defer_ = false; |
| UpdateBufferSizes(); |
| } |
| |
| void MultibufferDataSource::MediaIsPlaying() { |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| media_has_played_ = true; |
| cancel_on_defer_ = false; |
| // Once we start playing, we need preloading. |
| preload_ = AUTO; |
| UpdateBufferSizes(); |
| } |
| |
| ///////////////////////////////////////////////////////////////////////////// |
| // DataSource implementation. |
| void MultibufferDataSource::Stop() { |
| { |
| base::AutoLock auto_lock(lock_); |
| StopInternal_Locked(); |
| } |
| |
| render_task_runner_->PostTask(FROM_HERE, |
| base::Bind(&MultibufferDataSource::StopLoader, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void MultibufferDataSource::Abort() { |
| base::AutoLock auto_lock(lock_); |
| DCHECK(init_cb_.is_null()); |
| if (read_op_) |
| ReadOperation::Run(std::move(read_op_), kAborted); |
| |
| // Abort does not call StopLoader() since it is typically called prior to a |
| // seek or suspend. Let the loader logic make the decision about whether a new |
| // loader is necessary upon the seek or resume. |
| } |
| |
| void MultibufferDataSource::SetBitrate(int bitrate) { |
| render_task_runner_->PostTask( |
| FROM_HERE, base::Bind(&MultibufferDataSource::SetBitrateTask, |
| weak_factory_.GetWeakPtr(), bitrate)); |
| } |
| |
| void MultibufferDataSource::OnBufferingHaveEnough(bool always_cancel) { |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| if (reader_ && (always_cancel || (preload_ == METADATA && |
| !media_has_played_ && !IsStreaming()))) { |
| cancel_on_defer_ = true; |
| if (!loading_) |
| reader_.reset(nullptr); |
| } |
| } |
| |
| int64_t MultibufferDataSource::GetMemoryUsage() const { |
| // TODO(hubbe): Make more accurate when url_data_ is shared. |
| return url_data_->CachedSize() |
| << url_data_->multibuffer()->block_size_shift(); |
| } |
| |
| GURL MultibufferDataSource::GetUrlAfterRedirects() const { |
| return url_data_->url(); |
| } |
| |
| void MultibufferDataSource::Read(int64_t position, |
| int size, |
| uint8_t* data, |
| const DataSource::ReadCB& read_cb) { |
| DVLOG(1) << "Read: " << position << " offset, " << size << " bytes"; |
| // Reading is not allowed until after initialization. |
| DCHECK(init_cb_.is_null()); |
| 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(&MultibufferDataSource::ReadTask, weak_factory_.GetWeakPtr())); |
| } |
| |
| bool MultibufferDataSource::GetSize(int64_t* size_out) { |
| base::AutoLock auto_lock(lock_); |
| if (total_bytes_ != kPositionNotSpecified) { |
| *size_out = total_bytes_; |
| return true; |
| } |
| *size_out = 0; |
| return false; |
| } |
| |
| bool MultibufferDataSource::IsStreaming() { |
| return streaming_; |
| } |
| |
| ///////////////////////////////////////////////////////////////////////////// |
| // This method is the place where actual read happens, |
| void MultibufferDataSource::ReadTask() { |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| |
| base::AutoLock auto_lock(lock_); |
| int bytes_read = 0; |
| if (stop_signal_received_ || !read_op_) |
| return; |
| DCHECK(read_op_->size()); |
| |
| if (!reader_) { |
| CreateResourceLoader(read_op_->position(), kPositionNotSpecified); |
| } else { |
| reader_->Seek(read_op_->position()); |
| } |
| |
| int64_t available = reader_->Available(); |
| if (available < 0) { |
| // A failure has occured. |
| ReadOperation::Run(std::move(read_op_), kReadError); |
| return; |
| } |
| if (available) { |
| bytes_read = |
| static_cast<int>(std::min<int64_t>(available, read_op_->size())); |
| bytes_read = reader_->TryRead(read_op_->data(), bytes_read); |
| |
| 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_ = reader_->Tell(); |
| if (total_bytes_ != kPositionNotSpecified) |
| host_->SetTotalBytes(total_bytes_); |
| } |
| |
| ReadOperation::Run(std::move(read_op_), bytes_read); |
| } else { |
| reader_->Wait(1, base::Bind(&MultibufferDataSource::ReadTask, |
| weak_factory_.GetWeakPtr())); |
| } |
| UpdateLoadingState_Locked(false); |
| } |
| |
| void MultibufferDataSource::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 MultibufferDataSource::StopLoader() { |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| reader_.reset(nullptr); |
| } |
| |
| void MultibufferDataSource::SetBitrateTask(int bitrate) { |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| DCHECK(reader_.get()); |
| |
| bitrate_ = bitrate; |
| UpdateBufferSizes(); |
| } |
| |
| ///////////////////////////////////////////////////////////////////////////// |
| // BufferedResourceLoader callback methods. |
| void MultibufferDataSource::StartCallback() { |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| |
| if (init_cb_.is_null()) { |
| reader_.reset(); |
| return; |
| } |
| |
| // All responses must be successful. Resources that are assumed to be fully |
| // buffered must have a known content length. |
| bool success = reader_ && reader_->Available() > 0 && url_data_ && |
| (!assume_fully_buffered() || |
| url_data_->length() != kPositionNotSpecified); |
| |
| if (success) { |
| { |
| base::AutoLock auto_lock(lock_); |
| total_bytes_ = url_data_->length(); |
| } |
| streaming_ = |
| !assume_fully_buffered() && (total_bytes_ == kPositionNotSpecified || |
| !url_data_->range_supported()); |
| |
| media_log_->SetDoubleProperty("total_bytes", |
| static_cast<double>(total_bytes_)); |
| media_log_->SetBooleanProperty("streaming", streaming_); |
| } else { |
| reader_.reset(nullptr); |
| } |
| |
| // 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_); |
| } |
| |
| // Progress callback might be called after the start callback, |
| // make sure that we update single_origin_ now. |
| media_log_->SetBooleanProperty("single_origin", single_origin_); |
| media_log_->SetBooleanProperty("passed_cors_access_check", |
| DidPassCORSAccessCheck()); |
| media_log_->SetBooleanProperty("range_header_supported", |
| url_data_->range_supported()); |
| } |
| |
| render_task_runner_->PostTask( |
| FROM_HERE, base::Bind(base::ResetAndReturn(&init_cb_), success)); |
| |
| // Even if data is cached, say that we're loading at this point for |
| // compatibility. |
| UpdateLoadingState_Locked(true); |
| } |
| |
| void MultibufferDataSource::ProgressCallback(int64_t begin, int64_t end) { |
| DVLOG(1) << __func__ << "(" << begin << ", " << end << ")"; |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| |
| if (assume_fully_buffered()) |
| return; |
| |
| base::AutoLock auto_lock(lock_); |
| |
| if (end > begin) { |
| // TODO(scherkus): we shouldn't have to lock to signal host(), see |
| // http://crbug.com/113712 for details. |
| if (stop_signal_received_) |
| return; |
| |
| host_->AddBufferedByteRange(begin, end); |
| } |
| |
| UpdateLoadingState_Locked(false); |
| } |
| |
| void MultibufferDataSource::UpdateLoadingState_Locked(bool force_loading) { |
| DVLOG(1) << __func__; |
| lock_.AssertAcquired(); |
| if (assume_fully_buffered()) |
| return; |
| // Update loading state. |
| bool is_loading = !!reader_ && reader_->IsLoading(); |
| if (force_loading || is_loading != loading_) { |
| bool loading = is_loading || force_loading; |
| |
| if (!loading && cancel_on_defer_) { |
| if (read_op_) { |
| // We can't destroy the reader if a read operation is pending. |
| // UpdateLoadingState_Locked will be called again when the read |
| // operation is done. |
| return; |
| } |
| reader_.reset(nullptr); |
| } |
| |
| loading_ = loading; |
| |
| // Callback could kill us, be sure to call it last. |
| downloading_cb_.Run(loading_); |
| } |
| } |
| |
| void MultibufferDataSource::UpdateProgress() { |
| DCHECK(render_task_runner_->BelongsToCurrentThread()); |
| if (reader_) { |
| uint64_t available = reader_->Available(); |
| uint64_t pos = reader_->Tell(); |
| ProgressCallback(pos, pos + available); |
| } |
| } |
| |
| void MultibufferDataSource::UpdateBufferSizes() { |
| DVLOG(1) << __func__; |
| if (!reader_) |
| return; |
| |
| if (!assume_fully_buffered()) { |
| // If the playback has started and the strategy is aggressive, then try to |
| // load as much as possible, assuming that the file is cacheable. (If not, |
| // why bother?) |
| bool aggressive = (buffering_strategy_ == BUFFERING_STRATEGY_AGGRESSIVE); |
| if (media_has_played_ && aggressive && url_data_ && |
| url_data_->range_supported() && url_data_->cacheable()) { |
| reader_->SetPreload(1LL << 40, 1LL << 40); // 1 Tb |
| return; |
| } |
| } |
| |
| // Use a default bit rate if unknown and clamp to prevent overflow. |
| int64_t bitrate = clamp<int64_t>(bitrate_, 0, kMaxBitrate); |
| if (bitrate == 0) |
| bitrate = kDefaultBitrate; |
| |
| // Only scale the buffer window for playback rates greater than 1.0 in |
| // magnitude and clamp to prevent overflow. |
| double playback_rate = playback_rate_; |
| |
| playback_rate = std::max(playback_rate, 1.0); |
| playback_rate = std::min(playback_rate, kMaxPlaybackRate); |
| |
| int64_t bytes_per_second = (bitrate / 8.0) * playback_rate; |
| |
| // Preload 10 seconds of data, clamped to some min/max value. |
| int64_t preload = clamp(kTargetSecondsBufferedAhead * bytes_per_second, |
| kMinBufferPreload, kMaxBufferPreload); |
| // We preload this much, then we stop unil we read |preload| before resuming. |
| int64_t preload_high = preload + kPreloadHighExtra; |
| |
| // We pin a few seconds of data behind the current reading position. |
| int64_t pin_backward = clamp(kTargetSecondsBufferedBehind * bytes_per_second, |
| kMinBufferPreload, kMaxBufferPreload); |
| |
| // We always pin at least kDefaultPinSize ahead of the read position. |
| // Normally, the extra space between preload_high and kDefaultPinSize will |
| // not actually have any data in it, but if it does, we don't want to throw it |
| // away right before we need it. |
| int64_t pin_forward = std::max(preload_high, kDefaultPinSize); |
| |
| // Note that the buffer size is advisory as only non-pinned data is allowed |
| // to be thrown away. Most of the time we pin a region that is larger than |
| // |buffer_size|, which only makes sense because most of the time, some of |
| // the data in pinned region is not present in the cache. |
| int64_t buffer_size = |
| std::min((kTargetSecondsBufferedAhead + kTargetSecondsBufferedBehind) * |
| bytes_per_second, |
| preload_high + pin_backward); |
| reader_->SetMaxBuffer(buffer_size); |
| reader_->SetPinRange(pin_backward, pin_forward); |
| |
| if (preload_ == METADATA) { |
| reader_->SetPreload(0, 0); |
| } else { |
| reader_->SetPreload(preload_high, preload); |
| } |
| } |
| |
| } // namespace media |