blob: ab830e1773c93b00ab2f44e1090d4dcdbba777b2 [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 <initializer_list>
#include <limits>
#include <string>
#include <utility>
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_piece.h"
#include "media/formats/hls/media_playlist_test_builder.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/tags.h"
#include "media/formats/hls/test_util.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
namespace media::hls {
namespace {
scoped_refptr<MultivariantPlaylist> CreateMultivariantPlaylist(
std::initializer_list<base::StringPiece> lines,
GURL uri = GURL("http://localhost/multi_playlist.m3u8"),
types::DecimalInteger version = Playlist::kDefaultVersion) {
std::string source;
for (auto line : lines) {
source.append(line.data(), line.size());
source.append("\n");
}
// Parse the given source. Failure here isn't supposed to be part of the test,
// so use a CHECK.
auto result = MultivariantPlaylist::Parse(source, std::move(uri), version);
CHECK(result.has_value());
return std::move(result).value();
}
} // namespace
TEST(HlsMediaPlaylistTest, Segments) {
MediaPlaylistTestBuilder builder;
builder.AppendLine("#EXTM3U");
builder.AppendLine("#EXT-X-TARGETDURATION:10");
builder.AppendLine("#EXT-X-VERSION:5");
builder.SetVersion(5);
builder.ExpectPlaylist(HasTargetDuration, base::Seconds(10));
builder.AppendLine("#EXTINF:9.2,\t");
builder.AppendLine("video.ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(HasDiscontinuity, false);
builder.ExpectSegment(HasDuration, base::Seconds(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
{
auto fork = builder;
fork.AppendLine("foobar.ts");
fork.ExpectError(ParseStatusCode::kMediaSegmentMissingInfTag);
}
builder.AppendLine("#EXTINF:9.3,foo");
builder.AppendLine("#EXT-X-DISCONTINUITY");
builder.AppendLine("foo.ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(HasDiscontinuity, true);
builder.ExpectSegment(HasDuration, base::Seconds(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");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(HasDiscontinuity, false);
builder.ExpectSegment(HasDuration, base::Seconds(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
{
auto fork = builder;
fork.AppendLine("#EXTINF:10.499,bar");
fork.AppendLine("bar.ts");
fork.ExpectAdditionalSegment();
fork.ExpectOk();
fork.AppendLine("#EXTINF:10.5,baz");
fork.AppendLine("baz.ts");
fork.ExpectError(ParseStatusCode::kMediaSegmentExceedsTargetDuration);
}
builder.ExpectOk();
}
TEST(HlsMediaPlaylistTest, TotalDuration) {
constexpr types::DecimalInteger kSegmentDuration =
MediaPlaylist::kMaxTargetDuration.InSeconds();
constexpr size_t kMaxSegments =
base::TimeDelta::FiniteMax().InSeconds() / kSegmentDuration;
// Make sure this test won't take an unreasonable amount of time to run
static_assert(kMaxSegments < 1000);
// Ensure that if we have a playlist large enough where the total duration
// overflows `base::TimeDelta`, this is caught.
MediaPlaylistTestBuilder builder;
builder.AppendLine("#EXTM3U");
builder.AppendLine("#EXT-X-TARGETDURATION:" +
base::NumberToString(kSegmentDuration));
builder.ExpectPlaylist(HasTargetDuration, base::Seconds(kSegmentDuration));
for (size_t i = 0; i < kMaxSegments; ++i) {
builder.AppendLine("#EXTINF:" + base::NumberToString(kSegmentDuration) +
",\t");
builder.AppendLine("segment" + base::NumberToString(i) + ".ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(HasDuration, base::Seconds(kSegmentDuration));
builder.ExpectSegment(HasUri, GURL("http://localhost/segment" +
base::NumberToString(i) + ".ts"));
}
// The segments above should not overflow the playlist duration
builder.ExpectPlaylist(HasComputedDuration,
base::Seconds(kSegmentDuration * kMaxSegments));
builder.ExpectOk();
// But an additional segment would
builder.AppendLine("#EXTINF:" + base::NumberToString(kSegmentDuration) +
",\t");
builder.AppendLine("segmentX.ts");
builder.ExpectError(ParseStatusCode::kPlaylistOverflowsTimeDelta);
}
// This test is similar to the `HlsMultivariantPlaylistTest` test of the same
// name, but due to subtle differences between media playlists and multivariant
// playlists its difficult to combine them. If new cases are added here that are
// also relevant to multivariant playlists, they should be added to that test as
// well.
TEST(HlsMediaPlaylistTest, VariableSubstitution) {
MediaPlaylistTestBuilder builder;
builder.AppendLine("#EXTM3U");
builder.AppendLine("#EXT-X-TARGETDURATION:10");
builder.AppendLine("#EXT-X-VERSION:8");
builder.SetVersion(8);
builder.ExpectPlaylist(HasTargetDuration, base::Seconds(10));
builder.AppendLine(R"(#EXT-X-DEFINE:NAME="ROOT",VALUE="http://video.com")");
builder.AppendLine(R"(#EXT-X-DEFINE:NAME="MOVIE",VALUE="some_video/low")");
// Valid variable references within URI items should be substituted
builder.AppendLine("#EXTINF:9.9,\t");
builder.AppendLine("{$ROOT}/{$MOVIE}/fileSegment0.ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(
HasUri, GURL("http://video.com/some_video/low/fileSegment0.ts"));
{
// Invalid variable references within URI lines should result in an error
auto fork = builder;
fork.AppendLine("#EXTINF:9.9,\t");
fork.AppendLine("{$root}/{$movie}/fileSegment0.ts");
fork.ExpectError(ParseStatusCode::kVariableUndefined);
}
// Variable references outside of valid substitution points should not be
// substituted
{
auto fork = builder;
fork.AppendLine(R"(#EXT-X-DEFINE:NAME="LENGTH",VALUE="9.9")");
fork.AppendLine("#EXTINF:{$LENGTH},\t");
fork.AppendLine("http://foo/bar");
fork.ExpectError(ParseStatusCode::kMalformedTag);
}
// Redefinition is not allowed
{
auto fork = builder;
fork.AppendLine(
R"(#EXT-X-DEFINE:NAME="ROOT",VALUE="https://www.google.com")");
fork.ExpectError(ParseStatusCode::kVariableDefinedMultipleTimes);
}
// Importing in a parentless playlist is not allowed
{
auto fork = builder;
fork.AppendLine(R"(#EXT-X-DEFINE:IMPORT="IMPORTED")");
fork.ExpectError(ParseStatusCode::kImportedVariableInParentlessPlaylist);
}
// Test importing variables in a playlist with a parent
auto parent = CreateMultivariantPlaylist(
{"#EXTM3U", "#EXT-X-VERSION:8",
R"(#EXT-X-DEFINE:NAME="IMPORTED",VALUE="HELLO")"},
GURL("http://localhost/multi_playlist.m3u8"), 8);
{
// Referring to a parent playlist variable without importing it is an error
auto fork = builder;
fork.SetParent(parent.get());
fork.AppendLine("#EXTINF:9.9,\t");
fork.AppendLine("segments/{$IMPORTED}.ts");
fork.ExpectError(ParseStatusCode::kVariableUndefined);
}
{
// Locally overwriting an unimported variable from a parent playlist is NOT
// an error
auto fork = builder;
fork.SetParent(parent.get());
fork.AppendLine(R"(#EXT-X-DEFINE:NAME="IMPORTED",VALUE="WORLD")");
fork.AppendLine("#EXTINF:9.9,\t");
fork.AppendLine("segments/{$IMPORTED}.ts");
fork.ExpectAdditionalSegment();
fork.ExpectSegment(HasUri, GURL("http://localhost/segments/WORLD.ts"));
fork.ExpectOk();
// Importing a variable once it's been defined is an error
fork.AppendLine(R"(#EXT-X-DEFINE:IMPORT="IMPORTED")");
fork.ExpectError(ParseStatusCode::kVariableDefinedMultipleTimes);
}
{
// Defining a variable once it's been imported is an error
auto fork = builder;
fork.SetParent(parent.get());
fork.AppendLine(R"(#EXT-X-DEFINE:IMPORT="IMPORTED")");
fork.AppendLine(R"(#EXT-X-DEFINE:NAME="IMPORTED",VALUE="WORLD")");
fork.ExpectError(ParseStatusCode::kVariableDefinedMultipleTimes);
}
{
// Importing the same variable twice is an error
auto fork = builder;
fork.SetParent(parent.get());
fork.AppendLine(R"(#EXT-X-DEFINE:IMPORT="IMPORTED")");
fork.AppendLine(R"(#EXT-X-DEFINE:IMPORT="IMPORTED")");
fork.ExpectError(ParseStatusCode::kVariableDefinedMultipleTimes);
}
{
// Importing a variable that hasn't been defined in the parent playlist is
// an error
auto fork = builder;
fork.SetParent(parent.get());
fork.AppendLine(R"(#EXT-X-DEFINE:IMPORT="FOO")");
fork.ExpectError(ParseStatusCode::kImportedVariableUndefined);
}
{
// Test actually using an imported variable
auto fork = builder;
fork.SetParent(parent.get());
fork.AppendLine(R"(#EXT-X-DEFINE:IMPORT="IMPORTED")");
fork.AppendLine("#EXTINF:9.9,\t");
fork.AppendLine("segments/{$IMPORTED}.ts");
fork.ExpectAdditionalSegment();
fork.ExpectSegment(HasUri, GURL("http://localhost/segments/HELLO.ts"));
fork.ExpectOk();
}
// Variables are not resolved recursively
builder.AppendLine(R"(#EXT-X-DEFINE:NAME="BAR",VALUE="BAZ")");
builder.AppendLine(R"(#EXT-X-DEFINE:NAME="FOO",VALUE="{$BAR}")");
builder.AppendLine("#EXTINF:9.9,\t");
builder.AppendLine("http://{$FOO}.com/video");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(HasUri, GURL("http://{$BAR}.com/video"));
builder.ExpectOk();
}
TEST(HlsMediaPlaylistTest, MultivariantPlaylistTag) {
MediaPlaylistTestBuilder builder;
builder.AppendLine("#EXTM3U");
builder.AppendLine("#EXT-X-TARGETDURATION:10");
// Media playlists may not contain tags exclusive to multivariant playlists
for (TagName name = ToTagName(MultivariantPlaylistTagName::kMinValue);
name <= ToTagName(MultivariantPlaylistTagName::kMaxValue); ++name) {
auto tag_line = "#" + std::string{TagNameToString(name)};
auto fork = builder;
fork.AppendLine(tag_line);
fork.ExpectError(ParseStatusCode::kMediaPlaylistHasMultivariantPlaylistTag);
}
}
TEST(HlsMediaPlaylistTest, XIndependentSegmentsTagInParent) {
auto parent1 = CreateMultivariantPlaylist({
"#EXTM3U",
"#EXT-X-INDEPENDENT-SEGMENTS",
});
// Parent value should carryover to media playlist
MediaPlaylistTestBuilder builder;
builder.SetParent(parent1.get());
builder.AppendLine("#EXTM3U");
builder.AppendLine("#EXT-X-TARGETDURATION:10");
builder.ExpectPlaylist(HasIndependentSegments, true);
builder.ExpectOk();
// It's OK for this tag to reappear in the media playlist
builder.AppendLine("#EXT-X-INDEPENDENT-SEGMENTS");
builder.ExpectOk();
// Without that tag in the parent, the value depends entirely on its presence
// in the child
auto parent2 = CreateMultivariantPlaylist({"#EXTM3U"});
builder = MediaPlaylistTestBuilder();
builder.SetParent(parent2.get());
builder.AppendLine("#EXTM3U");
builder.AppendLine("#EXT-X-TARGETDURATION:10");
{
auto fork = builder;
fork.ExpectPlaylist(HasIndependentSegments, false);
fork.ExpectOk();
}
builder.AppendLine("#EXT-X-INDEPENDENT-SEGMENTS");
builder.ExpectPlaylist(HasIndependentSegments, true);
builder.ExpectOk();
EXPECT_FALSE(parent2->AreSegmentsIndependent());
}
TEST(HlsMediaPlaylistTest, XBitrateTag) {
MediaPlaylistTestBuilder builder;
builder.AppendLine("#EXTM3U");
builder.AppendLine("#EXT-X-TARGETDURATION:10");
// The EXT-X-BITRATE tag must be a valid DecimalInteger
{
for (base::StringPiece x : {"", ":", ": 1", ":1 ", ":-1", ":{$bitrate}"}) {
auto fork = builder;
fork.AppendLine("#EXT-X-BITRATE", x);
fork.ExpectError(ParseStatusCode::kMalformedTag);
}
}
// The EXT-X-BITRATE tag applies only to the segments that it appears after
builder.AppendLine("#EXTINF:9.2,");
builder.AppendLine("segment0.ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(HasMediaSequenceNumber, 0);
builder.ExpectSegment(HasUri, GURL("http://localhost/segment0.ts"));
builder.ExpectSegment(HasBitRate, absl::nullopt);
builder.AppendLine("#EXT-X-BITRATE:15");
builder.AppendLine("#EXTINF:9.2,");
builder.AppendLine("segment1.ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(HasMediaSequenceNumber, 1);
builder.ExpectSegment(HasUri, GURL("http://localhost/segment1.ts"));
builder.ExpectSegment(HasBitRate, 15000);
builder.AppendLine("#EXTINF:9.2,");
builder.AppendLine("segment2.ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(HasMediaSequenceNumber, 2);
builder.ExpectSegment(HasUri, GURL("http://localhost/segment2.ts"));
builder.ExpectSegment(HasBitRate, 15000);
// The EXT-X-BITRATE tag does not apply to segments that are byteranges
builder.AppendLine("#EXT-X-BYTERANGE:1024@0");
builder.AppendLine("#EXTINF:9.2,");
builder.AppendLine("segment3.ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(HasMediaSequenceNumber, 3);
builder.ExpectSegment(HasUri, GURL("http://localhost/segment3.ts"));
builder.ExpectSegment(HasByteRange, CreateByteRange(1024, 0));
builder.ExpectSegment(HasBitRate, absl::nullopt);
builder.AppendLine("#EXTINF:9.2,");
builder.AppendLine("segment4.ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(HasMediaSequenceNumber, 4);
builder.ExpectSegment(HasUri, GURL("http://localhost/segment4.ts"));
builder.ExpectSegment(HasByteRange, absl::nullopt);
builder.ExpectSegment(HasBitRate, 15000);
// The EXT-X-BITRATE tag is allowed to appear twice
builder.AppendLine("#EXT-X-BITRATE:20");
builder.AppendLine("#EXT-X-BITRATE:21");
builder.AppendLine("#EXTINF:9.2,");
builder.AppendLine("segment5.ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(HasMediaSequenceNumber, 5);
builder.ExpectSegment(HasUri, GURL("http://localhost/segment5.ts"));
builder.ExpectSegment(HasBitRate, 21000);
// A value of 0 is tolerated
builder.AppendLine("#EXT-X-BITRATE:0");
builder.AppendLine("#EXTINF:9.2,");
builder.AppendLine("segment6.ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(HasMediaSequenceNumber, 6);
builder.ExpectSegment(HasUri, GURL("http://localhost/segment6.ts"));
builder.ExpectSegment(HasBitRate, 0);
// Large values should saturate to `DecimalInteger::max`
builder.AppendLine("#EXT-X-BITRATE:18446744073709551");
builder.AppendLine("#EXTINF:9.2,");
builder.AppendLine("segment7.ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(HasMediaSequenceNumber, 7);
builder.ExpectSegment(HasUri, GURL("http://localhost/segment7.ts"));
builder.ExpectSegment(HasBitRate, 18446744073709551000u);
builder.AppendLine("#EXT-X-BITRATE:18446744073709552");
builder.AppendLine("#EXTINF:9.2,");
builder.AppendLine("segment8.ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(HasMediaSequenceNumber, 8);
builder.ExpectSegment(HasUri, GURL("http://localhost/segment8.ts"));
builder.ExpectSegment(HasBitRate,
std::numeric_limits<types::DecimalInteger>::max());
builder.AppendLine("#EXT-X-BITRATE:18446744073709551615");
builder.AppendLine("#EXTINF:9.2,");
builder.AppendLine("segment9.ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(HasMediaSequenceNumber, 9);
builder.ExpectSegment(HasUri, GURL("http://localhost/segment9.ts"));
builder.ExpectSegment(HasBitRate,
std::numeric_limits<types::DecimalInteger>::max());
builder.ExpectOk();
}
TEST(HlsMediaPlaylistTest, XByteRangeTag) {
MediaPlaylistTestBuilder builder;
builder.AppendLine("#EXTM3U");
builder.AppendLine("#EXT-X-TARGETDURATION:10");
// EXT-X-BYTERANGE content must be a valid ByteRange
{
for (base::StringPiece x :
{"", ":", ": 12@34", ":12@34 ", ":12@", ":12@{$offset}"}) {
auto fork = builder;
fork.AppendLine("#EXT-X-BYTERANGE", x);
fork.AppendLine("#EXTINF:9.2,\t");
fork.AppendLine("segment.ts");
fork.ExpectError(ParseStatusCode::kMalformedTag);
}
}
// EXT-X-BYTERANGE may not appear twice per-segment.
// TODO(https://crbug.com/1328528): Some players support this, using only the
// final occurrence.
{
auto fork = builder;
fork.AppendLine("#EXT-X-BYTERANGE:12@34");
fork.AppendLine("#EXT-X-BYTERANGE:34@56");
fork.AppendLine("#EXTINF:9.2,\t");
fork.AppendLine("segment.ts");
fork.ExpectError(ParseStatusCode::kPlaylistHasDuplicateTags);
}
// Offset is required if this is the first media segment.
// TODO(https://crbug.com/1328528): Some players support this, default offset
// to 0.
{
auto fork = builder;
fork.AppendLine("#EXT-X-BYTERANGE:12");
fork.AppendLine("#EXTINF:9.2,\t");
fork.AppendLine("segment.ts");
fork.ExpectError(ParseStatusCode::kByteRangeRequiresOffset);
fork = builder;
fork.AppendLine("#EXT-X-BYTERANGE:12@34");
fork.AppendLine("#EXTINF:9.2,\t");
fork.AppendLine("segment.ts");
fork.ExpectAdditionalSegment();
fork.ExpectSegment(HasByteRange, CreateByteRange(12, 34));
fork.ExpectOk();
}
// Offset is required if the previous media segment is not a byterange.
{
auto fork = builder;
fork.AppendLine("#EXTINF:9.2,\t");
fork.AppendLine("segment.ts");
fork.AppendLine("#EXT-X-BYTERANGE:12");
fork.AppendLine("#EXTINF:9.2,\t");
fork.AppendLine("segment.ts");
fork.ExpectError(ParseStatusCode::kByteRangeRequiresOffset);
fork = builder;
fork.AppendLine("#EXTINF:9.2,\t");
fork.AppendLine("segment.ts");
fork.ExpectAdditionalSegment();
fork.ExpectSegment(HasUri, GURL("http://localhost/segment.ts"));
fork.ExpectSegment(HasByteRange, absl::nullopt);
fork.AppendLine("#EXT-X-BYTERANGE:12@34");
fork.AppendLine("#EXTINF:9.2,\t");
fork.AppendLine("segment.ts");
fork.ExpectAdditionalSegment();
fork.ExpectSegment(HasUri, GURL("http://localhost/segment.ts"));
fork.ExpectSegment(HasByteRange, CreateByteRange(12, 34));
fork.ExpectOk();
}
// Offset is required if the previous media segment is a byterange of a
// different resource.
// TODO(https://crbug.com/1328528): Some players support this.
{
auto fork = builder;
fork.AppendLine("#EXT-X-BYTERANGE:12@34");
fork.AppendLine("#EXTINF:9.2,\t");
fork.AppendLine("segment1.ts");
fork.AppendLine("#EXT-X-BYTERANGE:56");
fork.AppendLine("#EXTINF:9.2,\t");
fork.AppendLine("segment2.ts");
fork.ExpectError(ParseStatusCode::kByteRangeRequiresOffset);
fork = builder;
fork.AppendLine("#EXT-X-BYTERANGE:12@34");
fork.AppendLine("#EXTINF:9.2,\t");
fork.AppendLine("segment1.ts");
fork.ExpectAdditionalSegment();
fork.ExpectSegment(HasUri, GURL("http://localhost/segment1.ts"));
fork.ExpectSegment(HasByteRange, CreateByteRange(12, 34));
fork.AppendLine("#EXT-X-BYTERANGE:56@78");
fork.AppendLine("#EXTINF:9.2,\t");
fork.AppendLine("segment2.ts");
fork.ExpectAdditionalSegment();
fork.ExpectSegment(HasUri, GURL("http://localhost/segment2.ts"));
fork.ExpectSegment(HasByteRange, CreateByteRange(56, 78));
fork.ExpectOk();
}
// Offset is required even if a prior segment is a byterange of the same
// resource, but not the immediately previous segment.
{
auto fork = builder;
fork.AppendLine("#EXT-X-BYTERANGE:12@34");
fork.AppendLine("#EXTINF:9.2,\t");
fork.AppendLine("segment1.ts");
fork.AppendLine("#EXTINF:9.2,\t");
fork.AppendLine("segment2.ts");
fork.AppendLine("#EXT-X-BYTERANGE:45");
fork.AppendLine("#EXTINF:9.2,\t");
fork.AppendLine("segment1.ts");
fork.ExpectError(ParseStatusCode::kByteRangeRequiresOffset);
fork = builder;
fork.AppendLine("#EXT-X-BYTERANGE:12@34");
fork.AppendLine("#EXTINF:9.2,\t");
fork.AppendLine("segment1.ts");
fork.ExpectAdditionalSegment();
fork.ExpectSegment(HasUri, GURL("http://localhost/segment1.ts"));
fork.ExpectSegment(HasByteRange, CreateByteRange(12, 34));
fork.AppendLine("#EXTINF:9.2,\t");
fork.AppendLine("segment2.ts");
fork.ExpectAdditionalSegment();
fork.ExpectSegment(HasUri, GURL("http://localhost/segment2.ts"));
fork.ExpectSegment(HasByteRange, absl::nullopt);
fork.AppendLine("#EXT-X-BYTERANGE:56@78");
fork.AppendLine("#EXTINF:9.2,\t");
fork.AppendLine("segment1.ts");
fork.ExpectAdditionalSegment();
fork.ExpectSegment(HasUri, GURL("http://localhost/segment1.ts"));
fork.ExpectSegment(HasByteRange, CreateByteRange(56, 78));
fork.ExpectOk();
}
// Offset can be elided if the previous segment is a byterange of the same
// resource.
{
auto fork = builder;
fork.AppendLine("#EXT-X-BYTERANGE:12@34");
fork.AppendLine("#EXTINF:9.2,\t");
fork.AppendLine("segment1.ts");
fork.ExpectAdditionalSegment();
fork.ExpectSegment(HasUri, GURL("http://localhost/segment1.ts"));
fork.ExpectSegment(HasByteRange, CreateByteRange(12, 34));
fork.AppendLine("#EXT-X-BYTERANGE:56");
fork.AppendLine("#EXTINF:9.2,\t");
fork.AppendLine("segment1.ts");
fork.ExpectAdditionalSegment();
fork.ExpectSegment(HasUri, GURL("http://localhost/segment1.ts"));
fork.ExpectSegment(HasByteRange, CreateByteRange(56, 46));
// If an explicit offset is given (even it it's eligible to be elided), it
// must be used.
fork.AppendLine("#EXT-X-BYTERANGE:78@99999");
fork.AppendLine("#EXTINF:9.2,\t");
fork.AppendLine("segment1.ts");
fork.ExpectAdditionalSegment();
fork.ExpectSegment(HasUri, GURL("http://localhost/segment1.ts"));
fork.ExpectSegment(HasByteRange, CreateByteRange(78, 99999));
fork.ExpectOk();
}
// Range given by tag may not be empty or overflow a uint64, even across
// segments.
{
auto fork = builder;
fork.AppendLine("#EXT-X-BYTERANGE:0@0");
fork.AppendLine("#EXTINF:9.2,\t");
fork.AppendLine("segment1.ts");
fork.ExpectError(ParseStatusCode::kByteRangeInvalid);
fork = builder;
fork.AppendLine("#EXT-X-BYTERANGE:18446744073709551615@1");
fork.AppendLine("#EXTINF:9.2,\t");
fork.AppendLine("segment1.ts");
fork.ExpectError(ParseStatusCode::kByteRangeInvalid);
fork = builder;
fork.AppendLine("#EXT-X-BYTERANGE:1@18446744073709551615");
fork.AppendLine("#EXTINF:9.2,\t");
fork.AppendLine("segment1.ts");
fork.ExpectError(ParseStatusCode::kByteRangeInvalid);
fork = builder;
fork.AppendLine(
"#EXT-X-BYTERANGE:18446744073709551615@18446744073709551615");
fork.AppendLine("#EXTINF:9.2,\t");
fork.AppendLine("segment1.ts");
fork.ExpectError(ParseStatusCode::kByteRangeInvalid);
fork = builder;
fork.AppendLine("#EXT-X-BYTERANGE:1@18446744073709551614");
fork.AppendLine("#EXTINF:9.2,\t");
fork.AppendLine("segment1.ts");
fork.ExpectAdditionalSegment();
fork.ExpectSegment(HasByteRange, CreateByteRange(1, 18446744073709551614u));
fork.ExpectOk();
// Since the previous segment ends at uint64_t::max, an additional
// contiguous byterange would overflow.
fork.AppendLine("#EXT-X-BYTERANGE:1");
fork.AppendLine("#EXTINF:9.2,\t");
fork.AppendLine("segment1.ts");
fork.ExpectError(ParseStatusCode::kByteRangeInvalid);
}
}
TEST(HlsMediaPlaylistTest, XDiscontinuityTag) {
MediaPlaylistTestBuilder builder;
builder.AppendLine("#EXTM3U");
builder.AppendLine("#EXT-X-TARGETDURATION:10");
builder.ExpectPlaylist(HasTargetDuration, base::Seconds(10));
// Default discontinuity state is false
builder.AppendLine("#EXTINF:9.9,\t");
builder.AppendLine("video.ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(HasDiscontinuity, false);
builder.ExpectSegment(HasDiscontinuitySequenceNumber, 0);
builder.AppendLine("#EXT-X-DISCONTINUITY");
builder.AppendLine("#EXTINF:9.9,\t");
builder.AppendLine("video.ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(HasDiscontinuity, true);
builder.ExpectSegment(HasDiscontinuitySequenceNumber, 1);
// The discontinuity tag does not apply to subsequent segments
builder.AppendLine("#EXTINF:9.9,\t");
builder.AppendLine("video.ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(HasDiscontinuity, false);
builder.ExpectSegment(HasDiscontinuitySequenceNumber, 1);
// The discontinuity tag may appear multiple times per segment
builder.AppendLine("#EXT-X-DISCONTINUITY");
builder.AppendLine("#EXT-X-DISCONTINUITY");
builder.AppendLine("#EXTINF:9.9,\t");
builder.AppendLine("video.ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(HasDiscontinuity, true);
builder.ExpectSegment(HasDiscontinuitySequenceNumber, 3);
builder.ExpectOk();
}
TEST(HlsMediaPlaylistTest, XDiscontinuitySequenceTag) {
MediaPlaylistTestBuilder builder;
builder.AppendLine("#EXTM3U");
builder.AppendLine("#EXT-X-TARGETDURATION:10");
// The EXT-X-DISCONTINUITY-SEQUENCE tag must be a valid DecimalInteger
{
for (const base::StringPiece x : {"", ":-1", ":{$foo}", ":1.5", ":one"}) {
auto fork = builder;
fork.AppendLine("#EXT-X-DISCONTINUITY-SEQUENCE", x);
fork.ExpectError(ParseStatusCode::kMalformedTag);
}
}
// The EXT-X-DISCONTINUITY-SEQUENCE tag may not appear twice
{
auto fork = builder;
fork.AppendLine("#EXT-X-DISCONTINUITY-SEQUENCE:1");
fork.AppendLine("#EXT-X-DISCONTINUITY-SEQUENCE:1");
fork.ExpectError(ParseStatusCode::kPlaylistHasDuplicateTags);
}
{
auto fork = builder;
fork.AppendLine("#EXT-X-DISCONTINUITY-SEQUENCE:0");
fork.AppendLine("#EXT-X-DISCONTINUITY");
fork.AppendLine("#EXT-X-DISCONTINUITY-SEQUENCE:1");
fork.ExpectError(ParseStatusCode::kPlaylistHasDuplicateTags);
}
// The EXT-X-DISCONTINUITY-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-DISCONTINUITY-SEQUENCE:0");
fork.ExpectError(
ParseStatusCode::kMediaSegmentBeforeDiscontinuitySequenceTag);
}
// The EXT-X-DISCONTINUITY-SEQUENCE tag must appear before any
// EXT-X-DISCONTINUITY tag
{
auto fork = builder;
fork.AppendLine("#EXT-X-DISCONTINUITY");
fork.AppendLine("#EXT-X-DISCONTINUITY-SEQUENCE:0");
fork.AppendLine("#EXTINF:9.8,\t");
fork.AppendLine("segment0.ts");
fork.ExpectError(
ParseStatusCode::kDiscontinuityTagBeforeDiscontinuitySequenceTag);
}
const auto fill_playlist = [](auto& builder, auto first_media_sequence_number,
auto first_discontinuity_sequence_number) {
builder.AppendLine("#EXTINF:9.8,\t");
builder.AppendLine("segment0.ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(HasUri, GURL("http://localhost/segment0.ts"));
builder.ExpectSegment(HasDiscontinuity, false);
builder.ExpectSegment(HasMediaSequenceNumber, first_media_sequence_number);
builder.ExpectSegment(HasDiscontinuitySequenceNumber,
first_discontinuity_sequence_number);
builder.AppendLine("#EXT-X-DISCONTINUITY");
builder.AppendLine("#EXTINF:9.8,\t");
builder.AppendLine("segment1.ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(HasDiscontinuity, true);
builder.ExpectSegment(HasMediaSequenceNumber,
first_media_sequence_number + 1);
builder.ExpectSegment(HasDiscontinuitySequenceNumber,
first_discontinuity_sequence_number + 1);
builder.AppendLine("#EXTINF:9.8,\t");
builder.AppendLine("segment2.ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(HasDiscontinuity, false);
builder.ExpectSegment(HasMediaSequenceNumber,
first_media_sequence_number + 2);
builder.ExpectSegment(HasDiscontinuitySequenceNumber,
first_discontinuity_sequence_number + 1);
};
// If the playlist does not contain the EXT-X-DISCONTINUITY-SEQUENCE tag, the
// default starting value is 0.
auto fork = builder;
fill_playlist(fork, 0, 0);
fork.ExpectOk();
fork = builder;
fork.AppendLine("#EXT-X-MEDIA-SEQUENCE:10");
fill_playlist(fork, 10, 0);
fork.ExpectOk();
// If the playlist has the EXT-X-DISCONTINUITY-SEQUENCE tag, it specifies the
// starting value.
fork = builder;
fork.AppendLine("#EXT-X-DISCONTINUITY-SEQUENCE:5");
fill_playlist(fork, 0, 5);
fork.ExpectOk();
fork = builder;
fork.AppendLine("#EXT-X-MEDIA-SEQUENCE:10");
fork.AppendLine("#EXT-X-DISCONTINUITY-SEQUENCE:5");
fill_playlist(fork, 10, 5);
fork.ExpectOk();
// If the very first segment is a discontinuity, it should still have a
// subsequent discontinuity sequence number.
fork = builder;
fork.AppendLine("#EXT-X-MEDIA-SEQUENCE:10");
fork.AppendLine("#EXT-X-DISCONTINUITY");
fork.AppendLine("#EXTINF:9.2,\t");
fork.AppendLine("segment.ts");
fork.ExpectAdditionalSegment();
fork.ExpectSegment(HasDiscontinuity, true);
fork.ExpectSegment(HasMediaSequenceNumber, 10);
fork.ExpectSegment(HasDiscontinuitySequenceNumber, 1);
fill_playlist(fork, 11, 1);
fork.ExpectOk();
fork = builder;
fork.AppendLine("#EXT-X-MEDIA-SEQUENCE:10");
fork.AppendLine("#EXT-X-DISCONTINUITY-SEQUENCE:5");
fork.AppendLine("#EXT-X-DISCONTINUITY");
fork.AppendLine("#EXTINF:9.2,\t");
fork.AppendLine("segment.ts");
fork.ExpectAdditionalSegment();
fork.ExpectSegment(HasDiscontinuity, true);
fork.ExpectSegment(HasMediaSequenceNumber, 10);
fork.ExpectSegment(HasDiscontinuitySequenceNumber, 6);
fill_playlist(fork, 11, 6);
fork.ExpectOk();
}
TEST(HlsMediaPlaylistTest, XEndListTag) {
MediaPlaylistTestBuilder builder;
builder.AppendLine("#EXTM3U");
builder.AppendLine("#EXT-X-TARGETDURATION:10");
// Without the 'EXT-X-ENDLIST' tag, the default value is false, regardless of
// the playlist type.
{
for (const base::StringPiece type : {"", "EVENT", "VOD"}) {
auto fork = builder;
if (!type.empty()) {
fork.AppendLine("#EXT-X-PLAYLIST-TYPE:", type);
}
fork.ExpectPlaylist(IsEndList, false);
fork.ExpectOk();
}
}
// The 'EXT-X-ENDLIST' tag may not have any content
{
for (const base::StringPiece x : {"", "FOO=BAR", "1"}) {
auto fork = builder;
fork.AppendLine("#EXT-X-ENDLIST:", x);
fork.ExpectError(ParseStatusCode::kMalformedTag);
}
}
// The EXT-X-ENDLIST tag can appear anywhere in the playlist
builder.AppendLine("#EXTINF:9.2,\t");
builder.AppendLine("segment0.ts");
builder.ExpectAdditionalSegment();
builder.AppendLine("#EXT-X-ENDLIST");
builder.ExpectPlaylist(IsEndList, true);
builder.AppendLine("#EXTINF:9.2,\n");
builder.AppendLine("segment1.ts");
builder.ExpectAdditionalSegment();
builder.ExpectOk();
// The EXT-X-ENDLIST tag may not appear twice
builder.AppendLine("#EXT-X-ENDLIST");
builder.ExpectError(ParseStatusCode::kPlaylistHasDuplicateTags);
}
TEST(HlsMediaPlaylistTest, XGapTag) {
MediaPlaylistTestBuilder builder;
builder.AppendLine("#EXTM3U");
builder.AppendLine("#EXT-X-TARGETDURATION:10");
builder.ExpectPlaylist(HasTargetDuration, base::Seconds(10));
// Default gap state is false
builder.AppendLine("#EXTINF:9.9,\t");
builder.AppendLine("video.ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(IsGap, false);
builder.AppendLine("#EXT-X-GAP");
builder.AppendLine("#EXTINF:9.9,\t");
builder.AppendLine("video.ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(IsGap, true);
// The gap tag does not apply to subsequent segments
builder.AppendLine("#EXTINF:9.9,\t");
builder.AppendLine("video.ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(IsGap, false);
// The gap tag may only appear once per segment
{
auto fork = builder;
fork.AppendLine("#EXT-X-GAP");
fork.AppendLine("#EXT-X-GAP");
fork.AppendLine("#EXTINF:9.9,\t");
fork.AppendLine("video.ts");
fork.ExpectAdditionalSegment();
fork.ExpectSegment(IsGap, true);
fork.ExpectError(ParseStatusCode::kPlaylistHasDuplicateTags);
}
builder.ExpectOk();
}
TEST(HlsMediaPlaylistTest, XIFramesOnlyTag) {
MediaPlaylistTestBuilder builder;
builder.AppendLine("#EXTM3U");
builder.AppendLine("#EXT-X-TARGETDURATION:10");
// Without the 'EXT-X-I-FRAMES-ONLY' tag, the default value is false.
{
auto fork = builder;
fork.ExpectPlaylist(IsIFramesOnly, false);
fork.ExpectOk();
}
// The 'EXT-X-I-FRAMES-ONLY' tag may not have any content
{
for (const base::StringPiece x : {"", "FOO=BAR", "1"}) {
auto fork = builder;
fork.AppendLine("#EXT-X-I-FRAMES-ONLY:", x);
fork.ExpectError(ParseStatusCode::kMalformedTag);
}
}
builder.AppendLine("#EXT-X-I-FRAMES-ONLY");
builder.ExpectPlaylist(IsIFramesOnly, true);
// This should not affect the calculation of the playlist's duration
builder.AppendLine("#EXTINF:10,\t");
builder.AppendLine("segment0.ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(HasDuration, base::Seconds(10));
builder.AppendLine("#EXTINF:10,\t");
builder.AppendLine("segment1.ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(HasDuration, base::Seconds(10));
builder.ExpectPlaylist(HasComputedDuration, base::Seconds(20));
builder.ExpectOk();
// The 'EXT-X-I-FRAMES-ONLY' tag should not appear twice
builder.AppendLine("#EXT-X-I-FRAMES-ONLY");
builder.ExpectError(ParseStatusCode::kPlaylistHasDuplicateTags);
}
TEST(HlsMediaPlaylistTest, XMapTag) {
MediaPlaylistTestBuilder builder;
builder.AppendLine("#EXTM3U");
builder.AppendLine("#EXT-X-TARGETDURATION:10");
// The EXT-X-MAP tag must be valid
for (base::StringPiece x : {"", "BYTERANGE=\"10\"", "URI=foo.ts"}) {
auto fork = builder;
fork.AppendLine("#EXT-X-MAP:", x);
fork.ExpectError(ParseStatusCode::kMalformedTag);
}
// The EXT-X-MAP tag only applies to subsequent elements
builder.AppendLine("#EXTINF:9.2,\t");
builder.AppendLine("foo1.ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(HasDuration, base::Seconds(9.2));
builder.ExpectSegment(HasUri, GURL("http://localhost/foo1.ts"));
builder.ExpectSegment(HasInitializationSegment, nullptr);
builder.AppendLine("#EXT-X-MAP:URI=\"init1.ts\"");
auto init1 = base::MakeRefCounted<MediaSegment::InitializationSegment>(
GURL("http://localhost/init1.ts"), absl::nullopt);
builder.AppendLine("#EXTINF:9.2,\t");
builder.AppendLine("foo2.ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(HasDuration, base::Seconds(9.2));
builder.ExpectSegment(HasUri, GURL("http://localhost/foo2.ts"));
builder.ExpectSegment(HasInitializationSegment, init1);
builder.AppendLine("#EXTINF:9.2,\t");
builder.AppendLine("foo3.ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(HasDuration, base::Seconds(9.2));
builder.ExpectSegment(HasUri, GURL("http://localhost/foo3.ts"));
builder.ExpectSegment(HasInitializationSegment, init1);
// Consecutive EXT-X-MAP tags are tolerated
builder.AppendLine("#EXT-X-MAP:URI=\"init2.ts\"");
builder.AppendLine("#EXT-X-MAP:URI=\"init3.ts\",BYTERANGE=\"10@0\"");
auto init3 = base::MakeRefCounted<MediaSegment::InitializationSegment>(
GURL("http://localhost/init3.ts"),
types::ByteRange::Validate(10, 0).value());
builder.AppendLine("#EXTINF:9.2,\t");
builder.AppendLine("foo4.ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(HasDuration, base::Seconds(9.2));
builder.ExpectSegment(HasUri, GURL("http://localhost/foo4.ts"));
builder.ExpectSegment(HasInitializationSegment, init3);
// If the BYTERANGE offset is not specified, it defaults to 0 (even if the
// previous, initialization segment is a byterange of the same resource)
builder.AppendLine("#EXT-X-MAP:URI=\"init3.ts\",BYTERANGE=\"10\"");
builder.AppendLine("#EXTINF:9.2,\t");
builder.AppendLine("foo5.ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(HasDuration, base::Seconds(9.2));
builder.ExpectSegment(HasUri, GURL("http://localhost/foo5.ts"));
builder.ExpectSegment(HasInitializationSegment, init3);
builder.ExpectOk();
}
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.ExpectSegment(HasDiscontinuitySequenceNumber, 0);
builder.AppendLine("#EXTINF:9.8,\t");
builder.AppendLine("segment1.ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(HasMediaSequenceNumber, first_sequence_number + 1);
builder.ExpectSegment(HasDiscontinuitySequenceNumber, 0);
builder.AppendLine("#EXTINF:9.8,\t");
builder.AppendLine("segment2.ts");
builder.ExpectAdditionalSegment();
builder.ExpectSegment(HasMediaSequenceNumber, first_sequence_number + 2);
builder.ExpectSegment(HasDiscontinuitySequenceNumber, 0);
};
// 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();
}
TEST(HlsMediaPlaylistTest, XPartInfTag) {
MediaPlaylistTestBuilder builder;
builder.AppendLine("#EXTM3U");
builder.AppendLine("#EXT-X-TARGETDURATION:100");
builder.AppendLine("#EXT-X-SERVER-CONTROL:PART-HOLD-BACK=500");
// EXT-X-PART-INF tag must be well-formed
for (base::StringPiece x : {"", ":", ":TARGET=1", ":PART-TARGET=two"}) {
auto fork = builder;
fork.AppendLine("#EXT-X-PART-INF", x);
fork.ExpectError(ParseStatusCode::kMalformedTag);
}
auto fork = builder;
fork.AppendLine("#EXT-X-PART-INF:PART-TARGET=0");
fork.ExpectPlaylist(
HasPartialSegmentInfo,
MediaPlaylist::PartialSegmentInfo{.target_duration = base::Seconds(0)});
fork.ExpectOk();
fork = builder;
fork.AppendLine("#EXT-X-PART-INF:PART-TARGET=1");
fork.ExpectPlaylist(
HasPartialSegmentInfo,
MediaPlaylist::PartialSegmentInfo{.target_duration = base::Seconds(1)});
fork.ExpectOk();
fork = builder;
fork.AppendLine("#EXT-X-PART-INF:PART-TARGET=1.2");
fork.ExpectPlaylist(
HasPartialSegmentInfo,
MediaPlaylist::PartialSegmentInfo{.target_duration = base::Seconds(1.2)});
fork.ExpectOk();
fork = builder;
fork.AppendLine("#EXT-X-PART-INF:PART-TARGET=99.99");
fork.ExpectPlaylist(HasPartialSegmentInfo,
MediaPlaylist::PartialSegmentInfo{
.target_duration = base::Seconds(99.99)});
fork.ExpectOk();
// PART-TARGET may not exceed the playlist's target duration
fork = builder;
fork.AppendLine("#EXT-X-PART-INF:PART-TARGET=100");
fork.ExpectPlaylist(HasTargetDuration, base::Seconds(100));
fork.ExpectPlaylist(
HasPartialSegmentInfo,
MediaPlaylist::PartialSegmentInfo{.target_duration = base::Seconds(100)});
fork.ExpectOk();
fork = builder;
fork.AppendLine("#EXT-X-PART-INF:PART-TARGET=101");
fork.ExpectError(ParseStatusCode::kPartTargetDurationExceedsTargetDuration);
// The EXT-X-PART-INF tag may not appear twice
fork.AppendLine("#EXT-X-PART-INF:PART-TARGET=10");
fork.ExpectError(ParseStatusCode::kPlaylistHasDuplicateTags);
}
TEST(HlsMediaPlaylistTest, XPlaylistTypeTag) {
MediaPlaylistTestBuilder builder;
builder.AppendLine("#EXTM3U");
builder.AppendLine("#EXT-X-TARGETDURATION:10");
// 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);
}
{
auto fork = builder;
fork.AppendLine("#EXT-X-PLAYLIST-TYPE");
fork.ExpectError(ParseStatusCode::kMalformedTag);
}
}
TEST(HlsMediaPlaylistTest, XServerControlTag) {
MediaPlaylistTestBuilder builder;
builder.AppendLine("#EXTM3U");
builder.AppendLine("#EXT-X-TARGETDURATION:6");
builder.ExpectPlaylist(HasTargetDuration, base::Seconds(6));
// Without the EXT-X-SERVER-CONTROL tag, certain properties have default
// values
auto fork = builder;
fork.ExpectPlaylist(HasSkipBoundary, absl::nullopt);
fork.ExpectPlaylist(CanSkipDateRanges, false);
fork.ExpectPlaylist(HasHoldBackDistance, base::Seconds(6) * 3);
fork.ExpectPlaylist(HasPartHoldBackDistance, absl::nullopt);
fork.ExpectPlaylist(CanBlockReload, false);
fork.ExpectOk();
// An empty EXT-X-SERVER-CONTROL tag shouldn't change these defaults
fork.AppendLine("#EXT-X-SERVER-CONTROL:");
fork.ExpectOk();
// This tag may not appear twice
fork.AppendLine("#EXT-X-SERVER-CONTROL:");
fork.ExpectError(ParseStatusCode::kPlaylistHasDuplicateTags);
// If attributes are malformed, playlist should be rejected
fork = builder;
fork.AppendLine("#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL={$foo}");
fork.ExpectError(ParseStatusCode::kMalformedTag);
// The CAN-SKIP-UNTIL attribute must be at least six times the target duration
fork = builder;
fork.AppendLine("#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=35");
fork.ExpectError(ParseStatusCode::kSkipBoundaryTooLow);
fork = builder;
fork.AppendLine("#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=36");
fork.ExpectPlaylist(HasSkipBoundary, base::Seconds(36));
fork.ExpectPlaylist(CanSkipDateRanges, false);
fork.ExpectPlaylist(HasHoldBackDistance, base::Seconds(6) * 3);
fork.ExpectPlaylist(HasPartHoldBackDistance, absl::nullopt);
fork.ExpectPlaylist(CanBlockReload, false);
fork.ExpectOk();
// The CAN-SKIP-DATERANGES tag may not appear without CAN-SKIP-UNTIL
fork = builder;
fork.AppendLine("#EXT-X-SERVER-CONTROL:CAN-SKIP-DATERANGES=YES");
fork.ExpectError(ParseStatusCode::kMalformedTag);
fork = builder;
fork.AppendLine(
"#EXT-X-SERVER-CONTROL:CAN-SKIP-DATERANGES=YES,CAN-SKIP-UNTIL=40");
fork.ExpectPlaylist(CanSkipDateRanges, true);
fork.ExpectPlaylist(HasSkipBoundary, base::Seconds(40));
fork.ExpectPlaylist(HasHoldBackDistance, base::Seconds(6) * 3);
fork.ExpectPlaylist(HasPartHoldBackDistance, absl::nullopt);
fork.ExpectPlaylist(CanBlockReload, false);
fork.ExpectOk();
fork = builder;
fork.AppendLine(
"#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=40,CAN-SKIP-DATERANGES=YES");
fork.ExpectPlaylist(CanSkipDateRanges, true);
fork.ExpectPlaylist(HasSkipBoundary, base::Seconds(40));
fork.ExpectPlaylist(HasHoldBackDistance, base::Seconds(6) * 3);
fork.ExpectPlaylist(HasPartHoldBackDistance, absl::nullopt);
fork.ExpectPlaylist(CanBlockReload, false);
fork.ExpectOk();
// The 'HOLD-BACK' attribute must be at least three times the playlist's
// target duration
fork = builder;
fork.AppendLine("#EXT-X-SERVER-CONTROL:HOLD-BACK=18");
fork.ExpectPlaylist(HasHoldBackDistance, base::Seconds(18));
fork.ExpectOk();
fork = builder;
fork.AppendLine("#EXT-X-SERVER-CONTROL:HOLD-BACK=17");
fork.ExpectError(ParseStatusCode::kHoldBackDistanceTooLow);
fork = builder;
fork.AppendLine("#EXT-X-SERVER-CONTROL:HOLD-BACK=17.999");
fork.ExpectError(ParseStatusCode::kHoldBackDistanceTooLow);
// The 'EXT-X-PART-INF' tag requires the 'PART-HOLD-BACK' field
fork = builder;
fork.AppendLine("#EXT-X-PART-INF:PART-TARGET=0.2");
fork.ExpectError(ParseStatusCode::kPartInfTagWithoutPartHoldBack);
fork = builder;
fork.AppendLine("#EXT-X-PART-INF:PART-TARGET=0.2");
fork.AppendLine("#EXT-X-SERVER-CONTROL:");
fork.ExpectError(ParseStatusCode::kPartInfTagWithoutPartHoldBack);
fork = builder;
fork.AppendLine("#EXT-X-PART-INF:PART-TARGET=0.2");
fork.AppendLine("#EXT-X-SERVER-CONTROL:PART-HOLD-BACK=0.5");
fork.ExpectPlaylist(
HasPartialSegmentInfo,
MediaPlaylist::PartialSegmentInfo{.target_duration = base::Seconds(0.2)});
fork.ExpectPlaylist(HasPartHoldBackDistance, base::Seconds(0.5));
fork.ExpectPlaylist(HasSkipBoundary, absl::nullopt);
fork.ExpectPlaylist(CanSkipDateRanges, false);
fork.ExpectPlaylist(HasHoldBackDistance, base::Seconds(6) * 3);
fork.ExpectPlaylist(CanBlockReload, false);
fork.ExpectOk();
// PART-HOLD-BACK must not be less than PART-TARGET * 2 (unless that tag
// doesn't exist)
fork = builder;
fork.AppendLine("#EXT-X-PART-INF:PART-TARGET=0.2");
fork.AppendLine("#EXT-X-SERVER-CONTROL:PART-HOLD-BACK=0.4");
fork.ExpectPlaylist(
HasPartialSegmentInfo,
MediaPlaylist::PartialSegmentInfo{.target_duration = base::Seconds(0.2)});
fork.ExpectPlaylist(HasPartHoldBackDistance, base::Seconds(0.4));
fork.ExpectPlaylist(HasSkipBoundary, absl::nullopt);
fork.ExpectPlaylist(CanSkipDateRanges, false);
fork.ExpectPlaylist(HasHoldBackDistance, base::Seconds(6) * 3);
fork.ExpectPlaylist(CanBlockReload, false);
fork.ExpectOk();
fork = builder;
fork.AppendLine("#EXT-X-PART-INF:PART-TARGET=0.2");
fork.AppendLine("#EXT-X-SERVER-CONTROL:PART-HOLD-BACK=0.3");
fork.ExpectError(ParseStatusCode::kPartHoldBackDistanceTooLow);
fork = builder;
fork.AppendLine("#EXT-X-SERVER-CONTROL:PART-HOLD-BACK=0.3");
fork.ExpectPlaylist(HasPartialSegmentInfo, absl::nullopt);
fork.ExpectPlaylist(HasPartHoldBackDistance, base::Seconds(0.3));
fork.ExpectPlaylist(HasSkipBoundary, absl::nullopt);
fork.ExpectPlaylist(CanSkipDateRanges, false);
fork.ExpectPlaylist(HasHoldBackDistance, base::Seconds(6) * 3);
fork.ExpectPlaylist(CanBlockReload, false);
fork.ExpectOk();
// Test the effect of the 'CAN-BLOCK-RELOAD' attribute
fork = builder;
fork.AppendLine("#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES");
fork.ExpectPlaylist(CanBlockReload, true);
fork.ExpectPlaylist(HasPartialSegmentInfo, absl::nullopt);
fork.ExpectPlaylist(HasPartHoldBackDistance, absl::nullopt);
fork.ExpectPlaylist(HasSkipBoundary, absl::nullopt);
fork.ExpectPlaylist(CanSkipDateRanges, false);
fork.ExpectPlaylist(HasHoldBackDistance, base::Seconds(6) * 3);
fork.ExpectOk();
}
TEST(HlsMediaPlaylistTest, XSkipTag) {
MediaPlaylistTestBuilder builder;
builder.AppendLine("#EXTM3U");
builder.AppendLine("#EXT-X-TARGETDURATION:10");
// The XSkip tag may not appear unless a playlist delta update was requested.
builder.AppendLine("#EXT-X-SKIP:SKIPPED-SEGMENTS=10");
builder.ExpectError(ParseStatusCode::kPlaylistHasUnexpectedDeltaUpdate);
}
TEST(HlsMediaPlaylistTest, XTargetDurationTag) {
MediaPlaylistTestBuilder builder;
builder.AppendLine("#EXTM3U");
// The XTargetDurationTag tag is required
builder.ExpectError(ParseStatusCode::kMediaPlaylistMissingTargetDuration);
// The XTargetDurationTag must appear exactly once
builder.AppendLine("#EXT-X-TARGETDURATION:10");
builder.ExpectPlaylist(HasTargetDuration, base::Seconds(10));
builder.ExpectOk();
{
auto fork = builder;
fork.AppendLine("#EXT-X-TARGETDURATION:10");
fork.ExpectError(ParseStatusCode::kPlaylistHasDuplicateTags);
}
{
auto fork = builder;
fork.AppendLine("#EXT-X-TARGETDURATION:11");
fork.ExpectError(ParseStatusCode::kPlaylistHasDuplicateTags);
}
// The XTargetDurationTag must be a valid DecimalInteger (unsigned)
for (base::StringPiece x : {"-1", "0.5", "-1.5", "999999999999999999999"}) {
MediaPlaylistTestBuilder builder2;
builder2.AppendLine("#EXTM3U");
builder2.AppendLine("#EXT-X-TARGETDURATION:", x);
builder2.ExpectError(ParseStatusCode::kMalformedTag);
}
// The target duration value may not exceed this implementation's max
builder = MediaPlaylistTestBuilder();
builder.AppendLine("#EXTM3U");
builder.AppendLine(
"#EXT-X-TARGETDURATION:",
base::NumberToString(MediaPlaylist::kMaxTargetDuration.InSeconds()));
builder.ExpectPlaylist(
HasTargetDuration,
base::Seconds(MediaPlaylist::kMaxTargetDuration.InSeconds()));
builder.ExpectOk();
builder = MediaPlaylistTestBuilder();
builder.AppendLine("#EXTM3U");
builder.AppendLine(
"#EXT-X-TARGETDURATION:",
base::NumberToString(MediaPlaylist::kMaxTargetDuration.InSeconds() + 1));
builder.ExpectError(ParseStatusCode::kTargetDurationExceedsMax);
}
} // namespace media::hls