Skip to content

Commit

Permalink
HLS: Implement the EXT-X-MEDIA-SEQUENCE tag
Browse files Browse the repository at this point in the history
The EXT-X-MEDIA-SEQUENCE tag represents the starting value to use for
each media segment's media sequence number. If the tag is absent, it's
implied that the first media sequence number is 0. If the tag is
present, it implies (for live playlists) that old media segments may
become unavailable at a future point in time. In either case, the media
sequence number is the primary mechanism for aligning segment sequences
in the same playlist across reloads.

Bug: 1266991
Change-Id: I3f466ae6aab4e5ae9b548843c666dee9b31e354e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3635988
Reviewed-by: Matthew Wolenetz <wolenetz@chromium.org>
Commit-Queue: Will Cassella <cassew@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1001831}
  • Loading branch information
willcassella authored and Chromium LUCI CQ committed May 11, 2022
1 parent ef9d60d commit d7bc2c0
Show file tree
Hide file tree
Showing 13 changed files with 228 additions and 59 deletions.
34 changes: 29 additions & 5 deletions media/formats/hls/media_playlist.cc
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ ParseStatus::Or<MediaPlaylist> MediaPlaylist::Parse(
absl::optional<XPlaylistTypeTag> playlist_type_tag;
absl::optional<XEndListTag> end_list_tag;
absl::optional<XIFramesOnlyTag> i_frames_only_tag;
absl::optional<XMediaSequenceTag> media_sequence_tag;
std::vector<MediaSegment> segments;

// If this media playlist was found through a multivariant playlist, it may
Expand Down Expand Up @@ -155,6 +156,18 @@ ParseStatus::Or<MediaPlaylist> MediaPlaylist::Parse(
}
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;
}
}

continue;
Expand All @@ -177,8 +190,16 @@ ParseStatus::Or<MediaPlaylist> MediaPlaylist::Parse(
return ParseStatusCode::kMediaSegmentMissingInfTag;
}

segments.emplace_back(inf_tag->duration, std::move(segment_uri),
discontinuity_tag.has_value(), gap_tag.has_value());
// 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();

segments.emplace_back(inf_tag->duration, media_sequence_number,
std::move(segment_uri), discontinuity_tag.has_value(),
gap_tag.has_value());

// Reset per-segment tags
inf_tag.reset();
Expand Down Expand Up @@ -216,7 +237,8 @@ ParseStatus::Or<MediaPlaylist> MediaPlaylist::Parse(
return MediaPlaylist(
std::move(uri), common_state.GetVersion(), independent_segments,
base::Seconds(target_duration_tag->duration), std::move(segments),
playlist_type, end_list_tag.has_value(), i_frames_only_tag.has_value());
playlist_type, end_list_tag.has_value(), i_frames_only_tag.has_value(),
media_sequence_tag.has_value());
}

MediaPlaylist::MediaPlaylist(GURL uri,
Expand All @@ -226,13 +248,15 @@ MediaPlaylist::MediaPlaylist(GURL uri,
std::vector<MediaSegment> segments,
absl::optional<PlaylistType> playlist_type,
bool end_list,
bool i_frames_only)
bool i_frames_only,
bool has_media_sequence_tag)
: Playlist(std::move(uri), version, independent_segments),
target_duration_(target_duration),
segments_(std::move(segments)),
playlist_type_(playlist_type),
end_list_(end_list),
i_frames_only_(i_frames_only) {
i_frames_only_(i_frames_only),
has_media_sequence_tag_(has_media_sequence_tag) {
base::TimeDelta duration;
for (const auto& segment : segments_) {
duration += base::Seconds(segment.GetDuration());
Expand Down
10 changes: 9 additions & 1 deletion media/formats/hls/media_playlist.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ class MEDIA_EXPORT MediaPlaylist final : public Playlist {
// https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#section-4.4.3.6
bool IsIFramesOnly() const { return i_frames_only_; }

// The presence of the EXT-X-MEDIA-SEQUENCE tag is a hint that, in the case of
// live playlists, media segments may become unavailable after the time this
// playlist was loaded + the duration of this playlist.
// https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#:~:text=nominal%20playback%20rate).-,If,-the%20Media%20Playlist
bool HasMediaSequenceTag() const { return has_media_sequence_tag_; }

// Attempts to parse the media playlist represented by `source`. `uri` must be
// a valid, non-empty GURL referring to the URI of this playlist. If this
// playlist was found through a multivariant playlist, `parent_playlist` must
Expand All @@ -84,14 +90,16 @@ class MEDIA_EXPORT MediaPlaylist final : public Playlist {
std::vector<MediaSegment> segments,
absl::optional<PlaylistType> playlist_type,
bool end_list,
bool i_frames_only);
bool i_frames_only,
bool has_media_sequence_tag_);

base::TimeDelta target_duration_;
std::vector<MediaSegment> segments_;
base::TimeDelta computed_duration_;
absl::optional<PlaylistType> playlist_type_;
bool end_list_;
bool i_frames_only_;
bool has_media_sequence_tag_;
};

} // namespace media::hls
Expand Down
15 changes: 15 additions & 0 deletions media/formats/hls/media_playlist_test_builder.h
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,28 @@ inline void HasComputedDuration(base::TimeDelta value,
EXPECT_EQ(playlist.GetComputedDuration(), value) << from.ToString();
}

// Checks the media playlist's `HasMediaSequenceTag` property against
// the given value.
inline void HasMediaSequenceTag(bool value,
const base::Location& from,
const MediaPlaylist& playlist) {
EXPECT_EQ(playlist.HasMediaSequenceTag(), value) << from.ToString();
}

// Checks that the latest media segment has the given duration.
inline void HasDuration(types::DecimalFloatingPoint duration,
const base::Location& from,
const MediaSegment& segment) {
EXPECT_DOUBLE_EQ(segment.GetDuration(), duration) << from.ToString();
}

// Checks that the latest media segment has the given media sequence number.
inline void HasMediaSequenceNumber(types::DecimalInteger number,
const base::Location& from,
const MediaSegment& segment) {
EXPECT_EQ(segment.GetMediaSequenceNumber(), number) << from.ToString();
}

// Checks that the latest media segment has the given URI.
inline void HasUri(GURL uri,
const base::Location& from,
Expand Down
78 changes: 78 additions & 0 deletions media/formats/hls/media_playlist_unittest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ TEST(HlsMediaPlaylistTest, Segments) {
builder.ExpectSegment(HasDuration, 9.2);
builder.ExpectSegment(HasUri, GURL("http://localhost/video.ts"));
builder.ExpectSegment(IsGap, false);
builder.ExpectSegment(HasMediaSequenceNumber, 0);

// Segments without #EXTINF tags are not allowed
{
Expand All @@ -149,6 +150,7 @@ TEST(HlsMediaPlaylistTest, Segments) {
builder.ExpectSegment(HasDuration, 9.3);
builder.ExpectSegment(IsGap, false);
builder.ExpectSegment(HasUri, GURL("http://localhost/foo.ts"));
builder.ExpectSegment(HasMediaSequenceNumber, 1);

builder.AppendLine("#EXTINF:9.2,bar");
builder.AppendLine("http://foo/bar.ts");
Expand All @@ -157,6 +159,7 @@ TEST(HlsMediaPlaylistTest, Segments) {
builder.ExpectSegment(HasDuration, 9.2);
builder.ExpectSegment(IsGap, false);
builder.ExpectSegment(HasUri, GURL("http://foo/bar.ts"));
builder.ExpectSegment(HasMediaSequenceNumber, 2);

// Segments must not exceed the playlist's target duration when rounded to the
// nearest integer
Expand Down Expand Up @@ -535,4 +538,79 @@ TEST(HlsMediaPlaylistTest, XIFramesOnlyTag) {
builder.ExpectError(ParseStatusCode::kPlaylistHasDuplicateTags);
}

TEST(HlsMediaPlaylistTest, XMediaSequenceTag) {
MediaPlaylistTestBuilder builder;
builder.AppendLine("#EXTM3U");
builder.AppendLine("#EXT-X-TARGETDURATION:10");

// The EXT-X-MEDIA-SEQUENCE tag's content must be a valid DecimalInteger
{
for (const base::StringPiece x : {"", ":-1", ":{$foo}", ":1.5", ":one"}) {
auto fork = builder;
fork.AppendLine("#EXT-X-MEDIA-SEQUENCE", x);
fork.ExpectError(ParseStatusCode::kMalformedTag);
}
}
// The EXT-X-MEDIA-SEQUENCE tag may not appear twice
{
auto fork = builder;
fork.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
fork.AppendLine("#EXT-X-MEDIA-SEQUENCE:1");
fork.ExpectError(ParseStatusCode::kPlaylistHasDuplicateTags);
}
// The EXT-X-MEDIA-SEQUENCE tag must appear before any media segment
{
auto fork = builder;
fork.AppendLine("#EXTINF:9.8,\t");
fork.AppendLine("segment0.ts");
fork.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
fork.ExpectError(ParseStatusCode::kMediaSegmentBeforeMediaSequenceTag);
}

const auto fill_playlist = [](auto& builder, auto first_sequence_number) {
builder.AppendLine("#EXTINF:9.8,\t");
builder.AppendLine("segment0.ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(HasUri, GURL("http://localhost/segment0.ts"));
builder.ExpectSegment(HasMediaSequenceNumber, first_sequence_number);

builder.AppendLine("#EXTINF:9.8,\t");
builder.AppendLine("segment1.ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(HasMediaSequenceNumber, first_sequence_number + 1);

builder.AppendLine("#EXTINF:9.8,\t");
builder.AppendLine("segment2.ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(HasMediaSequenceNumber, first_sequence_number + 2);
};

// If the playlist does not contain the EXT-X-MEDIA-SEQUENCE tag, the default
// starting segment number is 0.
auto fork = builder;
fill_playlist(fork, 0);
fork.ExpectPlaylist(HasMediaSequenceTag, false);
fork.ExpectOk();

// If the playlist has the EXT-X-MEDIA-SEQUENCE tag, it specifies the starting
// segment number.
fork = builder;
fork.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
fill_playlist(fork, 0);
fork.ExpectPlaylist(HasMediaSequenceTag, true);
fork.ExpectOk();

fork = builder;
fork.AppendLine("#EXT-X-MEDIA-SEQUENCE:15");
fill_playlist(fork, 15);
fork.ExpectPlaylist(HasMediaSequenceTag, true);
fork.ExpectOk();

fork = builder;
fork.AppendLine("#EXT-X-MEDIA-SEQUENCE:9999");
fill_playlist(fork, 9999);
fork.ExpectPlaylist(HasMediaSequenceTag, true);
fork.ExpectOk();
}

} // namespace media::hls
2 changes: 2 additions & 0 deletions media/formats/hls/media_segment.cc
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
namespace media::hls {

MediaSegment::MediaSegment(types::DecimalFloatingPoint duration,
types::DecimalInteger media_sequence_number,
GURL uri,
bool has_discontinuity,
bool is_gap)
: duration_(duration),
media_sequence_number_(media_sequence_number),
uri_(std::move(uri)),
has_discontinuity_(has_discontinuity),
is_gap_(is_gap) {}
Expand Down
7 changes: 7 additions & 0 deletions media/formats/hls/media_segment.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ namespace media::hls {
class MEDIA_EXPORT MediaSegment {
public:
MediaSegment(types::DecimalFloatingPoint duration,
types::DecimalInteger media_sequence_number,
GURL uri,
bool has_discontinuity,
bool is_gap);
Expand All @@ -26,6 +27,11 @@ class MEDIA_EXPORT MediaSegment {
// The approximate duration of this media segment in seconds.
types::DecimalFloatingPoint GetDuration() const { return duration_; }

// Returns the media sequence number of this media segment.
types::DecimalInteger GetMediaSequenceNumber() const {
return media_sequence_number_;
}

// The URI of the media resource. This will have already been resolved against
// the playlist URI. This is guaranteed to be valid and non-empty, unless
// `gap` is true, in which case this URI should not be used.
Expand All @@ -41,6 +47,7 @@ class MEDIA_EXPORT MediaSegment {

private:
types::DecimalFloatingPoint duration_;
types::DecimalInteger media_sequence_number_;
GURL uri_;
bool has_discontinuity_;
bool is_gap_;
Expand Down
1 change: 1 addition & 0 deletions media/formats/hls/parse_status.cc
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ base::StringPiece ParseStatusCodeToString(ParseStatusCode code) {
PARSE_STATUS_CODE_CASE(kImportedVariableUndefined);
PARSE_STATUS_CODE_CASE(kXStreamInfTagNotFollowedByUri);
PARSE_STATUS_CODE_CASE(kVariantMissingStreamInfTag);
PARSE_STATUS_CODE_CASE(kMediaSegmentBeforeMediaSequenceTag);
}

NOTREACHED();
Expand Down
1 change: 1 addition & 0 deletions media/formats/hls/parse_status.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ enum class ParseStatusCode : StatusCodeType {
kImportedVariableUndefined,
kXStreamInfTagNotFollowedByUri,
kVariantMissingStreamInfTag,
kMediaSegmentBeforeMediaSequenceTag,
};

struct ParseStatusTraits {
Expand Down
1 change: 1 addition & 0 deletions media/formats/hls/tag_name.cc
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ constexpr auto kTagNames = base::MakeFixedFlatMap({
TagNameEntry("EXT-X-INDEPENDENT-SEGMENTS",
CommonTagName::kXIndependentSegments),
TagNameEntry("EXT-X-MEDIA", MultivariantPlaylistTagName::kXMedia),
TagNameEntry("EXT-X-MEDIA-SEQUENCE", MediaPlaylistTagName::kXMediaSequence),
TagNameEntry("EXT-X-PLAYLIST-TYPE", MediaPlaylistTagName::kXPlaylistType),
TagNameEntry("EXT-X-SESSION-DATA",
MultivariantPlaylistTagName::kXSessionData),
Expand Down
3 changes: 2 additions & 1 deletion media/formats/hls/tag_name.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ enum class MediaPlaylistTagName : TagName {
kXDiscontinuity,
kXGap,
kXPlaylistType,
kMaxValue = kXPlaylistType,
kXMediaSequence,
kMaxValue = kXMediaSequence,
};

constexpr TagKind GetTagKind(CommonTagName) {
Expand Down
53 changes: 29 additions & 24 deletions media/formats/hls/tags.cc
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,25 @@ ParseStatus::Or<T> ParseEmptyTag(TagItem tag) {
return T{};
}

template <typename T>
ParseStatus::Or<T> ParseDecimalIntegerTag(TagItem tag,
types::DecimalInteger T::*field) {
DCHECK(tag.GetName() == ToTagName(T::kName));
if (!tag.GetContent().has_value()) {
return ParseStatusCode::kMalformedTag;
}

auto value = types::ParseDecimalInteger(*tag.GetContent());
if (value.has_error()) {
return ParseStatus(ParseStatusCode::kMalformedTag)
.AddCause(std::move(value).error());
}

T out;
out.*field = std::move(value).value();
return out;
}

// Attributes expected in `EXT-X-DEFINE` tag contents.
// These must remain sorted alphabetically.
enum class XDefineTagAttribute {
Expand Down Expand Up @@ -152,26 +171,19 @@ ParseStatus::Or<M3uTag> M3uTag::Parse(TagItem tag) {
}

ParseStatus::Or<XVersionTag> XVersionTag::Parse(TagItem tag) {
DCHECK(tag.GetName() == ToTagName(XVersionTag::kName));

if (!tag.GetContent().has_value()) {
return ParseStatusCode::kMalformedTag;
}

auto value_result = types::ParseDecimalInteger(*tag.GetContent());
if (value_result.has_error()) {
return ParseStatus(ParseStatusCode::kMalformedTag)
.AddCause(std::move(value_result).error());
auto result = ParseDecimalIntegerTag(tag, &XVersionTag::version);
if (result.has_error()) {
return std::move(result).error();
}

// Reject invalid version numbers.
// For valid version numbers, caller will decide if the version is supported.
auto value = std::move(value_result).value();
if (value == 0) {
auto out = std::move(result).value();
if (out.version == 0) {
return ParseStatusCode::kInvalidPlaylistVersion;
}

return XVersionTag{.version = value};
return out;
}

ParseStatus::Or<InfTag> InfTag::Parse(TagItem tag) {
Expand Down Expand Up @@ -430,18 +442,11 @@ ParseStatus::Or<XStreamInfTag> XStreamInfTag::Parse(
}

ParseStatus::Or<XTargetDurationTag> XTargetDurationTag::Parse(TagItem tag) {
DCHECK(tag.GetName() == ToTagName(XTargetDurationTag::kName));
if (!tag.GetContent().has_value()) {
return ParseStatusCode::kMalformedTag;
}

auto duration = types::ParseDecimalInteger(*tag.GetContent());
if (duration.has_error()) {
return ParseStatus(ParseStatusCode::kMalformedTag)
.AddCause(std::move(duration).error());
}
return ParseDecimalIntegerTag(tag, &XTargetDurationTag::duration);
}

return XTargetDurationTag{.duration = std::move(duration).value()};
ParseStatus::Or<XMediaSequenceTag> XMediaSequenceTag::Parse(TagItem tag) {
return ParseDecimalIntegerTag(tag, &XMediaSequenceTag::number);
}

} // namespace media::hls

0 comments on commit d7bc2c0

Please sign in to comment.