blob: 8687dc8c128708bd3313dcc80a399f638b31d052 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "media/filters/hls_vod_rendition.h"
#include "base/task/bind_post_task.h"
#include "media/filters/hls_manifest_demuxer_engine.h"
namespace media {
// Chosen mostly arbitrarily.
constexpr size_t kChunkSize = 1024 * 32;
// A nice round number, chosen to make sure that we get a good average network
// speed calculation.
constexpr size_t kMovingAverageSampleSize = 128;
HlsVodRendition::SegmentInfo::SegmentInfo() {}
HlsVodRendition::SegmentInfo::SegmentInfo(const HlsVodRendition::SegmentInfo&) =
default;
HlsVodRendition::SegmentInfo::~SegmentInfo() {}
HlsVodRendition::PendingSegment::PendingSegment(HlsDataSourceStream&& stream,
size_t index)
: stream(std::move(stream)), index(index) {}
HlsVodRendition::~HlsVodRendition() {
engine_host_->RemoveRole(role_);
}
HlsVodRendition::HlsVodRendition(ManifestDemuxerEngineHost* engine_host,
HlsRenditionHost* rendition_host,
std::string role,
scoped_refptr<hls::MediaPlaylist> playlist,
base::TimeDelta duration)
: engine_host_(engine_host),
rendition_host_(rendition_host),
role_(role),
segment_duration_upper_limit_(playlist->GetTargetDuration()),
duration_(duration),
fetch_time_(MovingAverage(kMovingAverageSampleSize)) {
base::TimeDelta time;
for (const auto& segment : playlist->GetSegments()) {
SegmentInfo info;
info.index = segments_.size();
info.segment = segment;
info.absolute_start = time;
time += segment->GetDuration();
info.absolute_end = time;
segments_.push_back(info);
}
fetch_queue_ = segments_.begin();
}
absl::optional<base::TimeDelta> HlsVodRendition::GetDuration() {
return duration_;
}
void HlsVodRendition::CheckState(
base::TimeDelta media_time,
double playback_rate,
ManifestDemuxer::DelayCallback time_remaining_cb) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (segments_.empty()) {
std::move(time_remaining_cb).Run(kNoTimestamp);
return;
}
auto ranges = engine_host_->GetBufferedRanges(role_);
if (ranges.empty()) {
// Ensure that we don't have a half-fetched stream when there's nothing
// loaded, since this case implies playback has just started or just
// finished a seek.
CHECK(!pending_stream_fetch_.has_value());
if (fetch_queue_ == segments_.end()) {
std::move(time_remaining_cb).Run(kNoTimestamp);
return;
}
FetchNext(base::BindOnce(std::move(time_remaining_cb), base::Seconds(0)),
media_time);
return;
}
// Not being in the last loaded range is an error.
// TODO(crbug/1266991) This can sometimes happen if the manifest lies about
// the duration and gets to the end of playback, and a user seeks there.
if (!ranges.contains(ranges.size() - 1, media_time)) {
PipelineStatus error = DEMUXER_ERROR_COULD_NOT_PARSE;
engine_host_->OnError(std::move(error)
.WithData("timestamp", media_time)
.WithData("range_start", ranges.back().first)
.WithData("range_end", ranges.back().second));
return;
}
// Determine if we should delay and clear out old buffered frames.
auto remaining = ranges.back().second - media_time;
auto remaining_rate = playback_rate ? playback_rate : 1;
remaining /= remaining_rate;
// If the remaining time is large enough, then there is time to clear old
// data and delay for a bit. "Large enough" is calculated to be at least 3
// seconds, chosen based on the "slow 3g" connection setting in devtools for
// a 1080p h264 video stream. "Large enough" must also be much more than the
// amount of time it would take to fetch the next segment, and the 6x
// multiplier was again chosen after messing about with the devtools network
// speed panel. The delay is then chosen such that there is roughly 4x as long
// to make a fetch request as it should take, based on the running average, or
// 1 second, whichever is longer.
// TODO(crbug/1266991) Update the `fetch_time_` moving average when network
// speed changes.
if (remaining > base::Seconds(3) && remaining > fetch_time_.Average() * 6) {
// We have buffered enough to have time to clear old segments and delay.
base::TimeDelta time_to_clear = ClearOldSegments(media_time);
auto delay_time = kNoTimestamp;
if (playback_rate) {
delay_time = remaining - (fetch_time_.Average() * 10) - time_to_clear -
base::Seconds(1);
if (delay_time < base::TimeDelta()) {
delay_time = base::TimeDelta();
}
}
std::move(time_remaining_cb).Run(delay_time);
return;
}
// If there is nothing more to fetch, then playback should just continue until
// the end and stop.
if (!pending_stream_fetch_.has_value() && fetch_queue_ == segments_.end()) {
std::move(time_remaining_cb).Run(kNoTimestamp);
return;
}
FetchNext(base::BindOnce(std::move(time_remaining_cb), base::Seconds(0)),
media_time);
}
bool HlsVodRendition::Seek(base::TimeDelta seek_time) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto ranges = engine_host_->GetBufferedRanges(role_);
if (!ranges.empty()) {
if (ranges.contains(ranges.size() - 1, seek_time)) {
// Only if we're in the last loaded range is it ok to keep the same
// fetch queue and ranges.
return false;
}
}
pending_stream_fetch_ = absl::nullopt;
fetch_queue_ =
std::lower_bound(segments_.begin(), segments_.end(), seek_time,
[](const SegmentInfo& segment, base::TimeDelta time) {
return segment.absolute_end < time;
});
if (fetch_queue_ != segments_.end()) {
engine_host_->EvictCodedFrames(role_, base::Seconds(0), 0);
engine_host_->RemoveAndReset(role_, base::TimeDelta(), duration_,
&parse_offset_);
engine_host_->SetGroupStartIfParsingAndSequenceMode(
role_, (*fetch_queue_).absolute_start);
return true;
}
return false;
}
void HlsVodRendition::CancelPendingNetworkRequests() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Nothing to do!
}
base::TimeDelta HlsVodRendition::ClearOldSegments(base::TimeDelta media_time) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
base::TimeTicks removal_start = base::TimeTicks::Now();
auto it =
std::lower_bound(segments_.begin(), segments_.end(), media_time,
[](const SegmentInfo& segment, base::TimeDelta time) {
return segment.absolute_end < time;
});
if (it != segments_.end()) {
if ((*it).absolute_start > base::TimeDelta()) {
engine_host_->Remove(role_, base::TimeDelta(), (*it).absolute_start);
}
}
return base::TimeTicks::Now() - removal_start;
}
void HlsVodRendition::FetchNext(base::OnceClosure cb, base::TimeDelta time) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(pending_stream_fetch_.has_value() || fetch_queue_ != segments_.end());
if (pending_stream_fetch_.has_value()) {
FetchMoreDataFromPendingStream(std::move(cb), time);
return;
}
SegmentInfo* segment = &*fetch_queue_;
std::advance(fetch_queue_, 1);
LoadSegment(segment, time, std::move(cb));
}
void HlsVodRendition::LoadSegment(SegmentInfo* segment,
base::TimeDelta fetch_required_time,
base::OnceClosure cb) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(segment);
rendition_host_->ReadFromUrl(
segment->segment->GetUri(), true, segment->segment->GetByteRange(),
base::BindPostTaskToCurrentDefault(base::BindOnce(
&HlsVodRendition::OnSegmentData, weak_factory_.GetWeakPtr(),
std::move(cb), std::move(fetch_required_time), segment->index,
base::TimeTicks::Now())));
}
void HlsVodRendition::FetchMoreDataFromPendingStream(
base::OnceClosure cb,
base::TimeDelta fetch_required_time) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(pending_stream_fetch_.has_value());
auto stream = std::move(pending_stream_fetch_->stream);
const SegmentInfo& segment = segments_[pending_stream_fetch_->index];
pending_stream_fetch_.reset();
std::move(stream).ReadChunk(
base::BindPostTaskToCurrentDefault(base::BindOnce(
&HlsVodRendition::OnSegmentData, weak_factory_.GetWeakPtr(),
std::move(cb), std::move(fetch_required_time), segment.index,
base::TimeTicks::Now())),
kChunkSize);
}
void HlsVodRendition::OnSegmentData(base::OnceClosure cb,
base::TimeDelta required_time,
size_t segment_index,
base::TimeTicks net_req_start,
HlsDataSourceStream::ReadResult result) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!result.has_value()) {
// Drop |cb| here, and let the abort handler pick up the pieces.
return engine_host_->OnError(
{DEMUXER_ERROR_COULD_NOT_PARSE, std::move(result).error()});
}
CHECK_LT(segment_index, segments_.size());
base::TimeDelta end =
segments_[segment_index].absolute_start + segment_duration_upper_limit_;
auto source = std::move(result).value();
if (!engine_host_->AppendAndParseData(
role_, base::TimeDelta(), end + base::Seconds(1), &parse_offset_,
source.AsRawData(), source.BytesInBuffer())) {
return engine_host_->OnError(DEMUXER_ERROR_COULD_NOT_PARSE);
}
auto fetch_duration = base::TimeTicks::Now() - net_req_start;
// Store the time it took to download this chunk. The time should be scaled
// for situations where we only have a few bytes left to download.
auto scaled = (fetch_duration * source.BytesInBuffer()) / kChunkSize;
fetch_time_.AddSample(scaled);
if (source.CanReadMore()) {
source.Flush();
pending_stream_fetch_.emplace(std::move(source), segment_index);
}
// After a seek especially, we will start loading content that comes
// potentially much earlier than the seek time, and it's possible that the
// loaded ranges won't yet contain the timestamp that is required to be loaded
// for the seek to complete. In this case, we just keep fetching until
// the seek time is loaded.
auto ranges = engine_host_->GetBufferedRanges(role_);
if (ranges.size() && ranges.contains(ranges.size() - 1, required_time)) {
std::move(cb).Run();
return;
}
// If the last range doesn't contain the timestamp, keep parsing until it
// does. If there is nothing left to download, then we can return.
if (!pending_stream_fetch_.has_value() && fetch_queue_ == segments_.end()) {
std::move(cb).Run();
return;
}
FetchNext(std::move(cb), required_time);
}
} // namespace media