Skip to content

Commit

Permalink
HLS: Implement the EXT-X-PLAYLIST-TYPE tag
Browse files Browse the repository at this point in the history
This CL implements the EXT-X-PLAYLIST-TYPE tag, which allows the server
to indicate constraints about how the playlist may change across
reloads. There are only two valid types `EVENT` and `VOD`. The absence
of this tag indicates that the playlist is considered "live", and may
change in ways that are invalid for either `EVENT` or `VOD`.

This CL adds unit tests for parsing the tag, as well as media playlists
containing it.

Bug: 1266991
Change-Id: Ib47a197cfffad63cf224e9d9285f532b98fa5e37
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3569572
Reviewed-by: Matthew Wolenetz <wolenetz@chromium.org>
Commit-Queue: Will Cassella <cassew@chromium.org>
Cr-Commit-Position: refs/heads/main@{#989232}
  • Loading branch information
willcassella authored and Chromium LUCI CQ committed Apr 6, 2022
1 parent 4c0ea5f commit 0429788
Show file tree
Hide file tree
Showing 10 changed files with 149 additions and 5 deletions.
1 change: 1 addition & 0 deletions media/formats/hls/items.cc
Expand Up @@ -42,6 +42,7 @@ absl::optional<TagItem> GetTagItem(SourceString line) {
TagNameEntry("-X-I-FRAMES-ONLY", MediaPlaylistTagName::kXIFramesOnly),
TagNameEntry("-X-INDEPENDENT-SEGMENTS",
CommonTagName::kXIndependentSegments),
TagNameEntry("-X-PLAYLIST-TYPE:", MediaPlaylistTagName::kXPlaylistType),
TagNameEntry("-X-VERSION:", CommonTagName::kXVersion),
TagNameEntry("INF:", MediaPlaylistTagName::kInf),
TagNameEntry("M3U", CommonTagName::kM3u),
Expand Down
21 changes: 18 additions & 3 deletions media/formats/hls/media_playlist.cc
Expand Up @@ -39,6 +39,7 @@ ParseStatus::Or<MediaPlaylist> MediaPlaylist::Parse(base::StringPiece source,
absl::optional<InfTag> inf_tag;
absl::optional<XGapTag> gap_tag;
absl::optional<XDiscontinuityTag> discontinuity_tag;
absl::optional<XPlaylistTypeTag> playlist_type_tag;
std::vector<MediaSegment> segments;

// Get segments out of the playlist
Expand Down Expand Up @@ -105,6 +106,13 @@ ParseStatus::Or<MediaPlaylist> MediaPlaylist::Parse(base::StringPiece source,
case MediaPlaylistTagName::kXIFramesOnly:
// TODO(crbug.com/1266991): Implement the #EXT-X-I-FRAMES-ONLY tag
break;
case MediaPlaylistTagName::kXPlaylistType: {
auto error = ParseUniqueTag(*tag, playlist_type_tag);
if (error.has_value()) {
return std::move(error).value();
}
break;
}
}

continue;
Expand Down Expand Up @@ -136,17 +144,24 @@ ParseStatus::Or<MediaPlaylist> MediaPlaylist::Parse(base::StringPiece source,
discontinuity_tag.reset();
}

absl::optional<PlaylistType> playlist_type;
if (playlist_type_tag) {
playlist_type = playlist_type_tag->type;
}

return MediaPlaylist(std::move(uri), common_state.GetVersion(),
common_state.independent_segments_tag.has_value(),
std::move(segments));
std::move(segments), playlist_type);
}

MediaPlaylist::MediaPlaylist(GURL uri,
types::DecimalInteger version,
bool independent_segments,
std::vector<MediaSegment> segments)
std::vector<MediaSegment> segments,
absl::optional<PlaylistType> playlist_type)
: Playlist(std::move(uri), version, independent_segments),
segments_(std::move(segments)) {
segments_(std::move(segments)),
playlist_type_(playlist_type) {
base::TimeDelta duration;
for (const auto& segment : segments_) {
duration += base::Seconds(segment.GetDuration());
Expand Down
16 changes: 15 additions & 1 deletion media/formats/hls/media_playlist.h
Expand Up @@ -9,6 +9,8 @@
#include "base/time/time.h"
#include "media/base/media_export.h"
#include "media/formats/hls/playlist.h"
#include "media/formats/hls/tags.h"
#include "media/formats/hls/types.h"

namespace media::hls {

Expand All @@ -31,6 +33,16 @@ class MEDIA_EXPORT MediaPlaylist final : public Playlist {
// actual duration.
base::TimeDelta GetComputedDuration() const { return computed_duration_; }

// Returns the type of this playlist (as specified by the
// 'EXT-X-PLAYLIST-TYPE' tag). If this is present, then the server must follow
// the constraints detailed on `PlaylistType` when the playlist is reloaded.
// If this property is absent, that implies that the server may append new
// segments to the end or remove old segments from the beginning of this
// playlist upon reloading.
absl::optional<PlaylistType> GetPlaylistType() const {
return playlist_type_;
}

// Attempts to parse the playlist represented by `source`. `uri` must be a
// valid, non-empty GURL referring to the URI of this playlist. If the
// playlist is invalid, returns an error. Otherwise, returns the parsed
Expand All @@ -42,10 +54,12 @@ class MEDIA_EXPORT MediaPlaylist final : public Playlist {
MediaPlaylist(GURL uri,
types::DecimalInteger version,
bool independent_segments,
std::vector<MediaSegment> segments);
std::vector<MediaSegment> segments,
absl::optional<PlaylistType> playlist_type);

std::vector<MediaSegment> segments_;
base::TimeDelta computed_duration_;
absl::optional<PlaylistType> playlist_type_;
};

} // namespace media::hls
Expand Down
58 changes: 58 additions & 0 deletions media/formats/hls/media_playlist_unittest.cc
Expand Up @@ -112,6 +112,12 @@ void HasVersion(types::DecimalInteger version,
EXPECT_EQ(playlist.GetVersion(), version) << from.ToString();
}

void HasType(absl::optional<PlaylistType> type,
const base::Location& from,
const MediaPlaylist& playlist) {
EXPECT_EQ(playlist.GetPlaylistType(), type) << from.ToString();
}

void HasDuration(types::DecimalFloatingPoint duration,
const base::Location& from,
const MediaSegment& segment) {
Expand Down Expand Up @@ -388,4 +394,56 @@ TEST(HlsFormatParserTest, ParseMediaPlaylist_Define) {
builder.ExpectOk();
}

TEST(HlsFormatParserTest, ParseMediaPlaylist_PlaylistType) {
TestBuilder builder;
builder.AppendLine("#EXTM3U");

// Without the EXT-X-PLAYLIST-TYPE tag, the playlist has no type.
{
auto fork = builder;
fork.ExpectPlaylist(HasType, absl::nullopt);
fork.ExpectOk();
}

{
auto fork = builder;
fork.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
fork.ExpectPlaylist(HasType, PlaylistType::kVOD);
fork.ExpectOk();
}

{
auto fork = builder;
fork.AppendLine("#EXT-X-PLAYLIST-TYPE:EVENT");
fork.ExpectPlaylist(HasType, PlaylistType::kEvent);
fork.ExpectOk();
}

// This tag may not be specified twice
{
auto fork = builder;
fork.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
fork.AppendLine("#EXT-X-PLAYLIST-TYPE:EVENT");
fork.ExpectError(ParseStatusCode::kPlaylistHasDuplicateTags);
}
{
auto fork = builder;
fork.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
fork.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
fork.ExpectError(ParseStatusCode::kPlaylistHasDuplicateTags);
}

// Unknown or invalid playlist types should trigger an error
{
auto fork = builder;
fork.AppendLine("#EXT-X-PLAYLIST-TYPE:FOOBAR");
fork.ExpectError(ParseStatusCode::kUnknownPlaylistType);
}
{
auto fork = builder;
fork.AppendLine("#EXT-X-PLAYLIST-TYPE:");
fork.ExpectError(ParseStatusCode::kMalformedTag);
}
}

} // namespace media::hls
1 change: 1 addition & 0 deletions media/formats/hls/parse_status.cc
Expand Up @@ -23,6 +23,7 @@ base::StringPiece ParseStatusCodeToString(ParseStatusCode code) {
PARSE_STATUS_CODE_CASE(kFailedToParseSignedDecimalFloatingPoint);
PARSE_STATUS_CODE_CASE(kFailedToParseQuotedString);
PARSE_STATUS_CODE_CASE(kInvalidPlaylistVersion);
PARSE_STATUS_CODE_CASE(kUnknownPlaylistType);
PARSE_STATUS_CODE_CASE(kMalformedAttributeList);
PARSE_STATUS_CODE_CASE(kAttributeListHasDuplicateNames);
PARSE_STATUS_CODE_CASE(kMalformedVariableName);
Expand Down
1 change: 1 addition & 0 deletions media/formats/hls/parse_status.h
Expand Up @@ -20,6 +20,7 @@ enum class ParseStatusCode : StatusCodeType {
kFailedToParseSignedDecimalFloatingPoint,
kFailedToParseQuotedString,
kInvalidPlaylistVersion,
kUnknownPlaylistType,
kMalformedAttributeList,
kAttributeListHasDuplicateNames,
kMalformedVariableName,
Expand Down
3 changes: 2 additions & 1 deletion media/formats/hls/tag_name.h
Expand Up @@ -54,7 +54,8 @@ enum class MediaPlaylistTagName : TagName {
kXIFramesOnly,
kXDiscontinuity,
kXGap,
kMaxValue = kXGap,
kXPlaylistType,
kMaxValue = kXPlaylistType,
};

constexpr TagKind GetTagKind(CommonTagName) {
Expand Down
18 changes: 18 additions & 0 deletions media/formats/hls/tags.cc
Expand Up @@ -254,4 +254,22 @@ ParseStatus::Or<XDefineTag> XDefineTag::Parse(TagItem tag) {
return ParseStatusCode::kMalformedTag;
}

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

// This tag requires content
if (tag.content.Empty()) {
return ParseStatusCode::kMalformedTag;
}

if (tag.content.Str() == "EVENT") {
return XPlaylistTypeTag{.type = PlaylistType::kEvent};
}
if (tag.content.Str() == "VOD") {
return XPlaylistTypeTag{.type = PlaylistType::kVOD};
}

return ParseStatusCode::kUnknownPlaylistType;
}

} // namespace media::hls
18 changes: 18 additions & 0 deletions media/formats/hls/tags.h
Expand Up @@ -91,6 +91,24 @@ struct XGapTag {
static MEDIA_EXPORT ParseStatus::Or<XGapTag> Parse(TagItem);
};

enum class PlaylistType {
// Indicates that this playlist may have segments appended upon reloading
// (until the #EXT-X-ENDLIST tag appears), but segments will not be removed.
kEvent,

// Indicates that this playlist is static, and will not have segments appended
// or removed.
kVOD,
};

// Represents the contents of the #EXT-X-PLAYLIST-TYPE tag
struct XPlaylistTypeTag {
static constexpr auto kName = MediaPlaylistTagName::kXPlaylistType;
static MEDIA_EXPORT ParseStatus::Or<XPlaylistTypeTag> Parse(TagItem);

PlaylistType type;
};

} // namespace media::hls

#endif // MEDIA_FORMATS_HLS_TAGS_H_
17 changes: 17 additions & 0 deletions media/formats/hls/tags_unittest.cc
Expand Up @@ -226,4 +226,21 @@ TEST(HlsFormatParserTest, ParseXDefineTagTest) {
ParseStatusCode::kMalformedTag);
}

TEST(HlsFormatParserTest, ParseXPlaylistTypeTagTest) {
RunTagIdenficationTest<XPlaylistTypeTag>("#EXT-X-PLAYLIST-TYPE:VOD\n", "VOD");
RunTagIdenficationTest<XPlaylistTypeTag>("#EXT-X-PLAYLIST-TYPE:EVENT\n",
"EVENT");

auto tag = OkTest<XPlaylistTypeTag>("EVENT");
EXPECT_EQ(tag.type, PlaylistType::kEvent);
tag = OkTest<XPlaylistTypeTag>("VOD");
EXPECT_EQ(tag.type, PlaylistType::kVOD);

ErrorTest<XPlaylistTypeTag>("FOOBAR", ParseStatusCode::kUnknownPlaylistType);
ErrorTest<XPlaylistTypeTag>("EEVENT", ParseStatusCode::kUnknownPlaylistType);
ErrorTest<XPlaylistTypeTag>(" EVENT", ParseStatusCode::kUnknownPlaylistType);
ErrorTest<XPlaylistTypeTag>("EVENT ", ParseStatusCode::kUnknownPlaylistType);
ErrorTest<XPlaylistTypeTag>("", ParseStatusCode::kMalformedTag);
}

} // namespace media::hls

0 comments on commit 0429788

Please sign in to comment.