blob: 45ebb9cbfc48896876fd7f84741adfc428a9cee3 [file] [log] [blame]
// Copyright 2022 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/formats/hls/media_playlist.h"
#include <cmath>
#include <utility>
#include <vector>
#include "base/memory/scoped_refptr.h"
#include "base/numerics/clamped_math.h"
#include "base/strings/string_piece.h"
#include "base/time/time.h"
#include "media/formats/hls/media_segment.h"
#include "media/formats/hls/multivariant_playlist.h"
#include "media/formats/hls/parse_status.h"
#include "media/formats/hls/playlist.h"
#include "media/formats/hls/playlist_common.h"
#include "media/formats/hls/source_string.h"
#include "media/formats/hls/tags.h"
#include "media/formats/hls/types.h"
#include "media/formats/hls/variable_dictionary.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/abseil-cpp/absl/types/variant.h"
#include "url/gurl.h"
namespace media::hls {
struct MediaPlaylist::CtorArgs {
GURL uri;
types::DecimalInteger version;
bool independent_segments;
base::TimeDelta target_duration;
absl::optional<PartialSegmentInfo> partial_segment_info;
std::vector<scoped_refptr<MediaSegment>> segments;
base::TimeDelta total_duration;
absl::optional<PlaylistType> playlist_type;
bool end_list;
bool i_frames_only;
bool has_media_sequence_tag;
bool can_skip_dateranges;
bool can_block_reload;
absl::optional<base::TimeDelta> skip_boundary;
base::TimeDelta hold_back_distance;
absl::optional<base::TimeDelta> part_hold_back_distance;
};
MediaPlaylist::~MediaPlaylist() = default;
Playlist::Kind MediaPlaylist::GetKind() const {
return Kind::kMediaPlaylist;
}
// static
ParseStatus::Or<scoped_refptr<MediaPlaylist>> MediaPlaylist::Parse(
base::StringPiece source,
GURL uri,
types::DecimalInteger version,
const MultivariantPlaylist* parent_playlist) {
DCHECK(version != 0);
if (version < Playlist::kMinSupportedVersion ||
version > Playlist::kMaxSupportedVersion) {
return ParseStatusCode::kPlaylistHasUnsupportedVersion;
}
if (!uri.is_valid()) {
return ParseStatusCode::kInvalidUri;
}
SourceLineIterator src_iter{source};
// Parse the first line of the playlist. This must be an M3U tag.
{
auto m3u_tag_result = CheckM3uTag(&src_iter);
if (!m3u_tag_result.has_value()) {
return std::move(m3u_tag_result).error();
}
}
CommonParserState common_state;
VariableDictionary::SubstitutionBuffer sub_buffer;
absl::optional<XTargetDurationTag> target_duration_tag;
absl::optional<InfTag> inf_tag;
absl::optional<XGapTag> gap_tag;
absl::optional<XDiscontinuityTag> discontinuity_tag;
absl::optional<XByteRangeTag> byterange_tag;
absl::optional<XBitrateTag> bitrate_tag;
absl::optional<XPlaylistTypeTag> playlist_type_tag;
absl::optional<XEndListTag> end_list_tag;
absl::optional<XIFramesOnlyTag> i_frames_only_tag;
absl::optional<XPartInfTag> part_inf_tag;
absl::optional<XServerControlTag> server_control_tag;
absl::optional<XMediaSequenceTag> media_sequence_tag;
absl::optional<XDiscontinuitySequenceTag> discontinuity_sequence_tag;
std::vector<scoped_refptr<MediaSegment>> segments;
scoped_refptr<MediaSegment::InitializationSegment> initialization_segment;
types::DecimalInteger discontinuity_sequence_number = 0;
// If this media playlist was found through a multivariant playlist, it may
// import variables from that playlist.
if (parent_playlist) {
common_state.parent_variable_dict =
&parent_playlist->GetVariableDictionary();
}
// Get segments out of the playlist
while (true) {
auto item_result = GetNextLineItem(&src_iter);
if (!item_result.has_value()) {
auto error = std::move(item_result).error();
// Only tolerated error is EOF, in which case we're done.
if (error.code() == ParseStatusCode::kReachedEOF) {
break;
}
return std::move(error);
}
auto item = std::move(item_result).value();
// Handle tags
if (auto* tag = absl::get_if<TagItem>(&item)) {
if (!tag->GetName().has_value()) {
HandleUnknownTag(*tag);
continue;
}
switch (GetTagKind(*tag->GetName())) {
case TagKind::kCommonTag: {
auto error = ParseCommonTag(*tag, &common_state);
if (error.has_value()) {
return std::move(error).value();
}
continue;
}
case TagKind::kMultivariantPlaylistTag:
return ParseStatusCode::kMediaPlaylistHasMultivariantPlaylistTag;
case TagKind::kMediaPlaylistTag:
// Handled below
break;
}
switch (static_cast<MediaPlaylistTagName>(*tag->GetName())) {
case MediaPlaylistTagName::kInf: {
auto error = ParseUniqueTag(*tag, inf_tag);
if (error.has_value()) {
return std::move(error).value();
}
break;
}
case MediaPlaylistTagName::kXBitrate: {
auto result = XBitrateTag::Parse(*tag);
if (!result.has_value()) {
return std::move(result).error();
}
bitrate_tag = std::move(result).value();
break;
}
case MediaPlaylistTagName::kXByteRange: {
// TODO(https://crbug.com/1328528): Investigate supporting aspects of
// this tag not described by the spec
auto error = ParseUniqueTag(*tag, byterange_tag);
if (error.has_value()) {
return std::move(error).value();
}
break;
}
case MediaPlaylistTagName::kXDateRange: {
// TODO(crbug.com/1266991): Implement the EXT-X-DATERANGE tag.
break;
}
case MediaPlaylistTagName::kXDiscontinuity: {
// Multiple occurrences of `EXT-X-DISCONTINUITY` per media segment are
// allowed, and each increments the segment's discontinuity sequence
// number by 1. The spec doesn't explicitly forbid this, and this
// seems to be how other HLS clients handle this scenario.
auto result = XDiscontinuityTag::Parse(*tag);
if (!result.has_value()) {
return std::move(result).error();
}
// Even if there was a previous discontinuity tag, overwrite the value
// and increment the discontinuity sequence number by 1.
discontinuity_tag = std::move(result).value();
discontinuity_sequence_number += 1;
break;
}
case MediaPlaylistTagName::kXDiscontinuitySequence: {
auto error = ParseUniqueTag(*tag, discontinuity_sequence_tag);
if (error.has_value()) {
return std::move(error).value();
}
// This tag must appear before any media segment or
// EXT-X-DISCONTINUITY tag.
if (!segments.empty()) {
return ParseStatusCode::kMediaSegmentBeforeDiscontinuitySequenceTag;
}
if (discontinuity_sequence_number != 0) {
return ParseStatusCode::
kDiscontinuityTagBeforeDiscontinuitySequenceTag;
}
discontinuity_sequence_number = discontinuity_sequence_tag->number;
break;
}
case MediaPlaylistTagName::kXEndList: {
auto error = ParseUniqueTag(*tag, end_list_tag);
if (error.has_value()) {
return std::move(error).value();
}
break;
}
case MediaPlaylistTagName::kXGap: {
auto error = ParseUniqueTag(*tag, gap_tag);
if (error.has_value()) {
return std::move(error).value();
}
break;
}
case MediaPlaylistTagName::kXIFramesOnly: {
auto error = ParseUniqueTag(*tag, i_frames_only_tag);
if (error.has_value()) {
return std::move(error).value();
}
break;
}
case MediaPlaylistTagName::kXKey: {
// TODO(crbug.com/1266991): Implement the EXT-X-KEY tag.
break;
}
case MediaPlaylistTagName::kXMap: {
auto result =
XMapTag::Parse(*tag, common_state.variable_dict, sub_buffer);
if (!result.has_value()) {
return std::move(result).error();
}
auto value = std::move(result).value();
// Resolve the URI against the playlist URI
auto resource_uri = uri.Resolve(value.uri.Str());
if (!resource_uri.is_valid()) {
return ParseStatusCode::kInvalidUri;
}
// Extract the byte range
absl::optional<types::ByteRange> byte_range;
if (value.byte_range.has_value()) {
// Safari defaults byte range offset to 0, do that here as well.
byte_range = types::ByteRange::Validate(
value.byte_range->length, value.byte_range->offset.value_or(0));
if (!byte_range.has_value()) {
return ParseStatusCode::kByteRangeInvalid;
}
}
initialization_segment =
base::MakeRefCounted<MediaSegment::InitializationSegment>(
std::move(resource_uri), byte_range);
break;
}
case MediaPlaylistTagName::kXMediaSequence: {
// This tag must appear before any media segment
if (!segments.empty()) {
return ParseStatusCode::kMediaSegmentBeforeMediaSequenceTag;
}
auto error = ParseUniqueTag(*tag, media_sequence_tag);
if (error.has_value()) {
return std::move(error).value();
}
break;
}
case MediaPlaylistTagName::kXPart: {
// TODO(crbug.com/1266991): Integrate the EXT-X-PART tag.
break;
}
case MediaPlaylistTagName::kXPartInf: {
auto error = ParseUniqueTag(*tag, part_inf_tag);
if (error.has_value()) {
return std::move(error).value();
}
break;
}
case MediaPlaylistTagName::kXPlaylistType: {
auto error = ParseUniqueTag(*tag, playlist_type_tag);
if (error.has_value()) {
return std::move(error).value();
}
break;
}
case MediaPlaylistTagName::kXPreloadHint: {
// TODO(crbug.com/1266991): Implement the EXT-X-PRELOAD-HINT tag.
break;
}
case MediaPlaylistTagName::kXProgramDateTime: {
// TODO(crbug.com/1266991): Implement the EXT-X-PROGRAM-DATE-TIME tag.
break;
}
case MediaPlaylistTagName::kXRenditionReport: {
// TODO(crbug.com/1266991): Implement the EXT-X-RENDITION-REPORT tag.
break;
}
case MediaPlaylistTagName::kXServerControl: {
auto error = ParseUniqueTag(*tag, server_control_tag);
if (error.has_value()) {
return std::move(error).value();
}
break;
}
case MediaPlaylistTagName::kXSkip: {
// TODO(crbug.com/1266991): Implement the EXT-X-SKIP tag.
// Since the appearance of the EXT-X-SKIP tag implies that this is a
// playlist delta update, we cannot parse this playlist.
return ParseStatusCode::kPlaylistHasUnexpectedDeltaUpdate;
}
case MediaPlaylistTagName::kXTargetDuration: {
auto error = ParseUniqueTag(*tag, target_duration_tag);
if (error.has_value()) {
return std::move(error).value();
}
break;
}
}
continue;
}
// Handle URIs
// `GetNextLineItem` should return either a TagItem (handled above) or a
// UriItem.
static_assert(absl::variant_size<GetNextLineItemResult>() == 2);
auto segment_uri_result = ParseUri(absl::get<UriItem>(std::move(item)), uri,
common_state, sub_buffer);
if (!segment_uri_result.has_value()) {
return std::move(segment_uri_result).error();
}
auto segment_uri = std::move(segment_uri_result).value();
// For this to be a valid media segment, we must have parsed an Inf tag
// since the last segment.
if (!inf_tag.has_value()) {
return ParseStatusCode::kMediaSegmentMissingInfTag;
}
// The media sequence number of this segment can be calculated by the value
// given by `EXT-X-MEDIA-SEQUENCE:n` (or 0), plus the number of prior
// segments in this playlist. It's an error for the EXT-X-MEDIA-SEQUENCE
// tag to appear after the first media segment (handled above).
const types::DecimalInteger media_sequence_number =
(media_sequence_tag ? media_sequence_tag->number : 0) + segments.size();
absl::optional<types::ByteRange> byterange;
if (byterange_tag.has_value()) {
auto range = byterange_tag->range;
// If this media segment had an EXT-X-BYTERANGE tag without an offset, the
// previous media segment must have been a byterange of the same resource.
// In that case, the offset is that of the byte following the previous
// media segment.
types::DecimalInteger offset;
if (range.offset.has_value()) {
offset = range.offset.value();
} else if (segments.empty()) {
return ParseStatusCode::kByteRangeRequiresOffset;
} else if (!segments.back()->GetByteRange().has_value()) {
return ParseStatusCode::kByteRangeRequiresOffset;
} else if (segments.back()->GetUri() != segment_uri) {
return ParseStatusCode::kByteRangeRequiresOffset;
} else {
offset = segments.back()->GetByteRange()->GetEnd();
}
byterange = types::ByteRange::Validate(range.length, offset);
if (!byterange) {
return ParseStatusCode::kByteRangeInvalid;
}
}
// The previous occurrence of the EXT-X-BITRATE tag applies to this segment
// only if this segment is not a byterange of its resource.
absl::optional<types::DecimalInteger> bitrate;
if (bitrate_tag.has_value() && !byterange.has_value()) {
// The value in the tag is expressed in kilobits per-second, but we wish
// to normalize all bitrates to bits-per-second. The spec specifically
// uses 'kilobit' as opposed to 'kibibit', so we multiply by 1000 instead
// of 1024.
// Ensure we don't overflow `DecimalInteger` when doing this
// multiplication.
bitrate = base::ClampMul(bitrate_tag->bitrate, 1000u);
}
segments.push_back(base::MakeRefCounted<MediaSegment>(
inf_tag->duration, media_sequence_number, discontinuity_sequence_number,
std::move(segment_uri), initialization_segment, byterange, bitrate,
discontinuity_tag.has_value(), gap_tag.has_value()));
// Reset per-segment tags
inf_tag.reset();
gap_tag.reset();
discontinuity_tag.reset();
byterange_tag.reset();
}
// Version must match what was expected.
if (!common_state.CheckVersion(version)) {
return ParseStatusCode::kPlaylistHasVersionMismatch;
}
if (!target_duration_tag.has_value()) {
return ParseStatusCode::kMediaPlaylistMissingTargetDuration;
}
const auto target_duration = target_duration_tag->duration;
if (target_duration > kMaxTargetDuration) {
return ParseStatusCode::kTargetDurationExceedsMax;
}
absl::optional<PartialSegmentInfo> partial_segment_info;
if (part_inf_tag.has_value()) {
partial_segment_info = MediaPlaylist::PartialSegmentInfo{
.target_duration = part_inf_tag->target_duration};
// Since the combination of partial segments should be equivalent to their
// parent segment, the partial segment target duration should not exceed the
// parent segment target duration.
if (partial_segment_info->target_duration > target_duration) {
return ParseStatusCode::kPartTargetDurationExceedsTargetDuration;
}
}
bool can_skip_dateranges = false;
bool can_block_reload = false;
absl::optional<base::TimeDelta> skip_boundary;
base::TimeDelta hold_back_distance = target_duration * 3;
absl::optional<base::TimeDelta> part_hold_back_distance;
if (server_control_tag.has_value()) {
can_skip_dateranges = server_control_tag->can_skip_dateranges;
can_block_reload = server_control_tag->can_block_reload;
if (server_control_tag->skip_boundary.has_value()) {
skip_boundary = server_control_tag->skip_boundary.value();
// The skip boundary MUST be at least six times the target
// duration.
if (skip_boundary.value() < target_duration * 6) {
return ParseStatusCode::kSkipBoundaryTooLow;
}
}
if (server_control_tag->hold_back.has_value()) {
hold_back_distance = server_control_tag->hold_back.value();
// The hold back distance MUST be at least three times the target
// duration.
if (hold_back_distance < target_duration * 3) {
return ParseStatusCode::kHoldBackDistanceTooLow;
}
}
if (server_control_tag->part_hold_back.has_value()) {
part_hold_back_distance = server_control_tag->part_hold_back.value();
// The part hold back distance MUST be at least twice the part target
// duration.
if (partial_segment_info.has_value() &&
part_hold_back_distance < partial_segment_info->target_duration * 2) {
return ParseStatusCode::kPartHoldBackDistanceTooLow;
}
}
}
// PART-HOLD-BACK is required if the PART-INF tag appeared
if (part_inf_tag.has_value() && !part_hold_back_distance.has_value()) {
return ParseStatusCode::kPartInfTagWithoutPartHoldBack;
}
// Ensure that no segment exceeds the target duration
base::TimeDelta total_duration;
for (const auto& segment : segments) {
// The spec says that the segment duration should not exceed the target
// duration after rounding to the nearest integer.
// https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#section-4.4.3.1
const auto rounded_duration =
std::round(segment->GetDuration().InSecondsF());
// Compare the rounded segment duration to the target duration (as an
// integer). Target duration should always be an integer of seconds, so to
// avoid floating-point precision issues we use `InSeconds()` rather than
// `InSecondsF()`.
if (rounded_duration > target_duration.InSeconds()) {
return ParseStatusCode::kMediaSegmentExceedsTargetDuration;
}
total_duration += segment->GetDuration();
}
if (total_duration.is_max()) {
return ParseStatusCode::kPlaylistOverflowsTimeDelta;
}
// Multivariant playlists may use the `EXT-X-INDEPENDENT-SEGMENTS` tag to
// indicate that every media playlist has independent segments. If that was
// the case, apply that to this playlist (this does not go in reverse).
// Otherwise, that property depends on whether that tag occurred in this
// playlist.
const bool independent_segments =
common_state.independent_segments_tag.has_value() ||
(parent_playlist && parent_playlist->AreSegmentsIndependent());
absl::optional<PlaylistType> playlist_type;
if (playlist_type_tag) {
playlist_type = playlist_type_tag->type;
}
return base::MakeRefCounted<MediaPlaylist>(
base::PassKey<MediaPlaylist>(),
CtorArgs{.uri = std::move(uri),
.version = version,
.independent_segments = independent_segments,
.target_duration = target_duration,
.partial_segment_info = std::move(partial_segment_info),
.segments = std::move(segments),
.total_duration = total_duration,
.playlist_type = playlist_type,
.end_list = end_list_tag.has_value(),
.i_frames_only = i_frames_only_tag.has_value(),
.has_media_sequence_tag = media_sequence_tag.has_value(),
.can_skip_dateranges = can_skip_dateranges,
.can_block_reload = can_block_reload,
.skip_boundary = skip_boundary,
.hold_back_distance = hold_back_distance,
.part_hold_back_distance = part_hold_back_distance});
}
MediaPlaylist::MediaPlaylist(base::PassKey<MediaPlaylist>, CtorArgs args)
: Playlist(std::move(args.uri), args.version, args.independent_segments),
target_duration_(args.target_duration),
partial_segment_info_(std::move(args.partial_segment_info)),
segments_(std::move(args.segments)),
computed_duration_(args.total_duration),
playlist_type_(args.playlist_type),
end_list_(args.end_list),
i_frames_only_(args.i_frames_only),
has_media_sequence_tag_(args.has_media_sequence_tag),
can_skip_dateranges_(args.can_skip_dateranges),
can_block_reload_(args.can_block_reload),
skip_boundary_(args.skip_boundary),
hold_back_distance_(args.hold_back_distance),
part_hold_back_distance_(args.part_hold_back_distance) {}
} // namespace media::hls