// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "media/filters/hls_manifest_demuxer_engine.h"
#include "media/filters/manifest_demuxer.h"
#include <memory>
#include <string>
#include <string_view>
#include <vector>
#include "base/memory/scoped_refptr.h"
#include "base/run_loop.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/task_environment.h"
#include "media/base/mock_media_log.h"
#include "media/base/pipeline_status.h"
#include "media/base/test_helpers.h"
#include "media/filters/hls_data_source_provider.h"
#include "media/filters/hls_test_helpers.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace media {
const std::string kInvalidMediaPlaylist =
"#This Wont Parse!\n"
"#EXT-X-ENDLIST\n";
const std::string kShortMediaPlaylist =
"#EXTM3U\n"
"#EXT-X-TARGETDURATION:10\n"
"#EXT-X-VERSION:3\n"
"#EXTINF:9.009,\n"
"http://media.example.com/first.ts\n"
"#EXT-X-ENDLIST\n";
const std::string kSimpleMediaPlaylist =
"#EXTM3U\n"
"#EXT-X-TARGETDURATION:10\n"
"#EXT-X-VERSION:3\n"
"#EXTINF:9.009,\n"
"http://media.example.com/first.ts\n"
"#EXTINF:9.009,\n"
"http://media.example.com/second.ts\n"
"#EXTINF:3.003,\n"
"http://media.example.com/third.ts\n"
"#EXT-X-ENDLIST\n";
const std::string kSimpleLiveMediaPlaylist =
"#EXTM3U\n"
"#EXT-X-TARGETDURATION:10\n"
"#EXT-X-VERSION:3\n"
"#EXT-X-MEDIA-SEQUENCE:18698597\n"
"#EXTINF:9.009,\n"
"http://media.example.com/first.ts\n"
"#EXTINF:9.009,\n"
"http://media.example.com/second.ts\n"
"#EXTINF:3.003,\n"
"http://media.example.com/third.ts\n";
const std::string kInitialRequestLiveMediaPlaylist =
"#EXTM3U\n"
"#EXT-X-TARGETDURATION:10\n"
"#EXT-X-VERSION:3\n"
"#EXT-X-MEDIA-SEQUENCE:1234\n"
"#EXTINF:9.009,\n"
"http://media.example.com/a.ts\n"
"#EXTINF:9.009,\n"
"http://media.example.com/b.ts\n"
"#EXTINF:9.009,\n"
"http://media.example.com/c.ts\n"
"#EXTINF:9.009,\n"
"http://media.example.com/d.ts\n";
const std::string kSecondRequestLiveMediaPlaylist =
"#EXTM3U\n"
"#EXT-X-TARGETDURATION:10\n"
"#EXT-X-VERSION:3\n"
"#EXT-X-MEDIA-SEQUENCE:1236\n"
"#EXTINF:9.009,\n"
"http://media.example.com/c.ts\n"
"#EXTINF:9.009,\n"
"http://media.example.com/d.ts\n"
"#EXTINF:9.009,\n"
"http://media.example.com/e.ts\n"
"#EXTINF:9.009,\n"
"http://media.example.com/f.ts\n"
"#EXTINF:9.009,\n"
"http://media.example.com/g.ts\n"
"#EXTINF:9.009,\n"
"http://media.example.com/h.ts\n";
const std::string kSingleInfoMediaPlaylist =
"#EXTM3U\n"
"#EXT-X-TARGETDURATION:10\n"
"#EXT-X-VERSION:3\n"
"#EXTINF:9.009,\n"
"http://media.example.com/only.ts\n";
const std::string kUnsupportedCodecs =
"#EXTM3U\n"
"#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"vvc1.00.00\"\n"
"http://example.com/audio-only.m3u8\n"
"#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"sheet.music\"\n"
"http://example.com/audio-only.m3u8\n"
"#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"av02.00.00\"\n"
"http://example.com/audio-only.m3u8\n";
const std::string kSimpleMultivariantPlaylist =
"#EXTM3U\n"
"#EXT-X-STREAM-INF:BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1000000\n"
"http://example.com/low.m3u8\n"
"#EXT-X-STREAM-INF:BANDWIDTH=2560000,AVERAGE-BANDWIDTH=2000000\n"
"http://example.com/mid.m3u8\n"
"#EXT-X-STREAM-INF:BANDWIDTH=7680000,AVERAGE-BANDWIDTH=6000000\n"
"http://example.com/hi.m3u8\n"
"#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n"
"http://example.com/audio-only.m3u8\n";
const std::string kMultivariantPlaylistWithAlts =
"#EXTM3U\n"
"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac\",NAME=\"Eng\",DEFAULT=YES,"
"AUTOSELECT=YES,LANGUAGE=\"en\",URI=\"eng-audio.m3u8\"\n"
"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac\",NAME=\"Ger\",DEFAULT=NO,"
"AUTOSELECT=YES,LANGUAGE=\"en\",URI=\"ger-audio.m3u8\"\n"
"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac\",NAME=\"Com\",DEFAULT=NO,"
"AUTOSELECT=NO,LANGUAGE=\"en\",URI=\"eng-comments.m3u8\"\n"
"#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"avc1.420000\",AUDIO=\"aac\"\n"
"low/video-only.m3u8\n"
"#EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS=\"avc1.420000\",AUDIO=\"aac\"\n"
"mid/video-only.m3u8\n"
"#EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS=\"avc1.420000\",AUDIO=\"aac\"\n"
"hi/video-only.m3u8\n"
"#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.05\",AUDIO=\"aac\"\n"
"main/english-audio.m3u8\n";
using ::base::test::RunOnceCallback;
using ::base::test::RunOnceClosure;
using testing::_;
using testing::AtLeast;
using testing::ByMove;
using testing::DoAll;
using testing::Eq;
using testing::Invoke;
using testing::NiceMock;
using testing::NotNull;
using testing::Ref;
using testing::Return;
using testing::SaveArg;
using testing::SetArgPointee;
using testing::StrictMock;
MATCHER_P2(CloseTo,
Target,
Radius,
std::string(negation ? "isn't" : "is") + " within " +
testing::PrintToString(Radius) + " of " +
testing::PrintToString(Target)) {
return (arg - Target <= Radius) || (Target - arg <= Radius);
}
MATCHER_P2(SingleSegmentQueue,
urlstr,
range,
"Segment Queue matcher for HlsDataSourceProvider") {
if (arg.size() != 1) {
return false;
}
auto first = arg.front();
return first.uri == GURL(urlstr) && first.range == range;
}
class FakeHlsDataSourceProvider : public HlsDataSourceProvider {
private:
raw_ptr<HlsDataSourceProvider> mock_;
public:
FakeHlsDataSourceProvider(HlsDataSourceProvider* mock) : mock_(mock) {}
void ReadFromCombinedUrlQueue(
SegmentQueue segments,
HlsDataSourceProvider::ReadCb callback) override {
mock_->ReadFromCombinedUrlQueue(std::move(segments), std::move(callback));
}
void ReadFromExistingStream(std::unique_ptr<HlsDataSourceStream> stream,
HlsDataSourceProvider::ReadCb cb) override {
CHECK(!stream->CanReadMore());
std::move(cb).Run(std::move(stream));
}
void AbortPendingReads(base::OnceClosure callback) override {
mock_->AbortPendingReads(std::move(callback));
}
};
template <typename T>
class CallbackEnforcer {
public:
explicit CallbackEnforcer(T expected)
: expected_(std::move(expected)), was_called_(false) {}
base::OnceCallback<void(T)> GetCallback() {
return base::BindOnce(
[](bool* writeback, T expected, T actual) {
*writeback = true;
ASSERT_EQ(actual, expected);
},
&was_called_, expected_);
}
// This method is move only, so it must be std::moved.
void AssertAndReset(base::test::TaskEnvironment& env) && {
env.RunUntilIdle();
ASSERT_TRUE(was_called_);
}
private:
T expected_;
bool was_called_ = false;
};
class HlsManifestDemuxerEngineTest : public testing::Test {
protected:
std::unique_ptr<MediaLog> media_log_;
std::unique_ptr<MockManifestDemuxerEngineHost> mock_mdeh_;
std::unique_ptr<MockHlsDataSourceProvider> mock_dsp_;
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
std::unique_ptr<HlsManifestDemuxerEngine> engine_;
base::OnceClosure pending_url_fetch_;
template <typename T>
void BindUrlToDataSource(std::string url,
std::string value,
bool taint_origin = false) {
EXPECT_CALL(*mock_dsp_, ReadFromCombinedUrlQueue(
SingleSegmentQueue(url, std::nullopt), _))
.Times(1)
.WillOnce(RunOnceCallback<1>(T::CreateStream(value, taint_origin)));
}
template <typename T>
void BindUrlAssignmentThunk(std::string url,
std::string value,
bool taint_origin = false) {
EXPECT_CALL(*mock_dsp_,
ReadFromCombinedUrlQueue(
SingleSegmentQueue(std::move(url), std::nullopt), _))
.Times(1)
.WillOnce([this, value = std::move(value), taint_origin = taint_origin](
HlsDataSourceProvider::SegmentQueue,
HlsDataSourceProvider::ReadCb cb) {
pending_url_fetch_ = base::BindOnce(
std::move(cb), T::CreateStream(std::move(value), taint_origin));
});
}
void ExpectNoNetworkRequests() {
EXPECT_CALL(*mock_dsp_, ReadFromCombinedUrlQueue(_, _)).Times(0);
}
MockHlsRendition* SetUpInterruptTest() {
EXPECT_CALL(*mock_mdeh_, SetSequenceMode("primary", true));
EXPECT_CALL(*mock_mdeh_, SetDuration(21.021));
EXPECT_CALL(*mock_mdeh_,
AddRole("primary", RelaxedParserSupportedType::kMP2T));
BindUrlToDataSource<StringHlsDataSourceStreamFactory>(
"http://media.example.com/manifest.m3u8", kSimpleMultivariantPlaylist);
BindUrlToDataSource<StringHlsDataSourceStreamFactory>(
"http://example.com/hi.m3u8", kSimpleMediaPlaylist);
EXPECT_CALL(*this, MockInitComplete(HasStatusCode(PIPELINE_OK)));
InitializeEngine();
task_environment_.RunUntilIdle();
auto rendition = std::make_unique<StrictMock<MockHlsRendition>>();
EXPECT_CALL(*rendition, GetDuration()).WillOnce(Return(base::Seconds(30)));
auto* rendition_ptr = rendition.get();
engine_->AddRenditionForTesting("primary", std::move(rendition));
EXPECT_CALL(*rendition_ptr, Stop());
task_environment_.RunUntilIdle();
return rendition_ptr;
}
base::OnceClosure StartAndCaptureSeek(MockHlsRendition* rendition_ptr) {
base::OnceClosure continue_seek;
EXPECT_CALL(*this, SeekFinished()).Times(0);
EXPECT_CALL(*rendition_ptr, Seek(_)).Times(0);
EXPECT_CALL(*mock_dsp_, AbortPendingReads(_))
.WillOnce([&continue_seek](base::OnceClosure cb) {
continue_seek = std::move(cb);
});
engine_->Seek(
base::Seconds(10),
base::BindOnce(
[](base::OnceClosure cb, ManifestDemuxer::SeekResponse resp) {
ASSERT_TRUE(resp.has_value());
std::move(cb).Run();
},
base::BindOnce(&HlsManifestDemuxerEngineTest::SeekFinished,
base::Unretained(this))));
task_environment_.RunUntilIdle();
CHECK(continue_seek);
return base::BindOnce(
[](MockHlsRendition* rendition_ptr, base::OnceClosure cb) {
EXPECT_CALL(*rendition_ptr, Seek(_))
.WillOnce(Return(ManifestDemuxer::SeekState::kIsReady));
std::move(cb).Run();
},
rendition_ptr, std::move(continue_seek));
}
base::OnceClosure StartAndCaptureTimeUpdate(MockHlsRendition* rendition_ptr,
base::TimeDelta timestamp) {
ManifestDemuxer::DelayCallback finish_time_update;
EXPECT_CALL(*rendition_ptr, CheckState(_, _, _))
.WillOnce([&finish_time_update](base::TimeDelta, double,
ManifestDemuxer::DelayCallback cb) {
finish_time_update = std::move(cb);
});
engine_->OnTimeUpdate(
base::Seconds(0), 0.0,
base::BindOnce([](base::TimeDelta timestamp,
base::TimeDelta r) { ASSERT_EQ(r, timestamp); },
timestamp));
task_environment_.RunUntilIdle();
CHECK(finish_time_update);
return base::BindOnce(std::move(finish_time_update), timestamp);
}
base::OnceClosure StartAndCaptureNetworkAdaptation(
MockHlsRendition* rendition_ptr,
std::string url,
std::string value,
size_t netspeed) {
base::OnceClosure continue_adaptation;
EXPECT_CALL(*mock_dsp_, ReadFromCombinedUrlQueue(
SingleSegmentQueue(url, std::nullopt), _))
.Times(1)
.WillOnce(
[&continue_adaptation, value](HlsDataSourceProvider::SegmentQueue,
HlsDataSourceProvider::ReadCb cb) {
continue_adaptation = base::BindOnce(
std::move(cb),
StringHlsDataSourceStreamFactory::CreateStream(value));
});
engine_->UpdateNetworkSpeed(netspeed);
task_environment_.RunUntilIdle();
CHECK(continue_adaptation);
return base::BindOnce(
[](MockHlsRendition* rendition_ptr, base::OnceClosure cb) {
EXPECT_CALL(*rendition_ptr, UpdatePlaylist(_, _));
std::move(cb).Run();
},
rendition_ptr, std::move(continue_adaptation));
}
public:
MOCK_METHOD(void, MockInitComplete, (PipelineStatus status), ());
MOCK_METHOD(void, SeekFinished, (), ());
HlsManifestDemuxerEngineTest()
: media_log_(std::make_unique<NiceMock<media::MockMediaLog>>()),
mock_mdeh_(std::make_unique<NiceMock<MockManifestDemuxerEngineHost>>()),
mock_dsp_(std::make_unique<StrictMock<MockHlsDataSourceProvider>>()) {
ON_CALL(*mock_mdeh_, AddRole(_, _)).WillByDefault(Return(true));
ON_CALL(*mock_mdeh_, GetBufferedRanges(_))
.WillByDefault(Return(Ranges<base::TimeDelta>()));
EXPECT_CALL(*mock_dsp_, ReadFromExistingStream(_, _)).Times(0);
base::SequenceBound<FakeHlsDataSourceProvider> dsp(
task_environment_.GetMainThreadTaskRunner(), mock_dsp_.get());
engine_ = std::make_unique<HlsManifestDemuxerEngine>(
std::move(dsp), base::SingleThreadTaskRunner::GetCurrentDefault(),
false, GURL("http://media.example.com/manifest.m3u8"),
media_log_.get());
}
void InitializeEngine() {
engine_->Initialize(
mock_mdeh_.get(),
base::BindOnce(&HlsManifestDemuxerEngineTest::MockInitComplete,
base::Unretained(this)));
}
~HlsManifestDemuxerEngineTest() override {
engine_->Stop();
base::RunLoop().RunUntilIdle();
}
};
TEST_F(HlsManifestDemuxerEngineTest, TestInitFailure) {
BindUrlToDataSource<StringHlsDataSourceStreamFactory>(
"http://media.example.com/manifest.m3u8", kInvalidMediaPlaylist);
EXPECT_CALL(*mock_mdeh_,
OnError(HasStatusCode(DEMUXER_ERROR_COULD_NOT_PARSE)));
EXPECT_CALL(*this, MockInitComplete(_)).Times(0);
InitializeEngine();
task_environment_.RunUntilIdle();
ASSERT_TRUE(engine_->IsSeekable());
}
TEST_F(HlsManifestDemuxerEngineTest, TestSimpleConfigAddsOnePrimaryRole) {
EXPECT_CALL(*mock_mdeh_, SetSequenceMode("primary", true));
EXPECT_CALL(*mock_mdeh_, SetDuration(21.021));
EXPECT_CALL(*mock_mdeh_,
AddRole("primary", RelaxedParserSupportedType::kMP2T));
EXPECT_CALL(*mock_mdeh_, RemoveRole("primary"));
BindUrlToDataSource<StringHlsDataSourceStreamFactory>(
"http://media.example.com/manifest.m3u8", kSimpleMediaPlaylist);
EXPECT_CALL(*this, MockInitComplete(HasStatusCode(PIPELINE_OK)));
InitializeEngine();
task_environment_.RunUntilIdle();
ASSERT_TRUE(engine_->IsSeekable());
}
TEST_F(HlsManifestDemuxerEngineTest, TestSimpleLiveConfigAddsOnePrimaryRole) {
EXPECT_CALL(*mock_mdeh_, SetSequenceMode("primary", true));
EXPECT_CALL(*mock_mdeh_,
AddRole("primary", RelaxedParserSupportedType::kMP2T));
EXPECT_CALL(*mock_mdeh_, RemoveRole("primary"));
BindUrlToDataSource<StringHlsDataSourceStreamFactory>(
"http://media.example.com/manifest.m3u8", kSimpleLiveMediaPlaylist);
EXPECT_CALL(*this, MockInitComplete(HasStatusCode(PIPELINE_OK)));
InitializeEngine();
task_environment_.RunUntilIdle();
ASSERT_FALSE(engine_->IsSeekable());
}
TEST_F(HlsManifestDemuxerEngineTest, TestLivePlaybackManifestUpdates) {
EXPECT_CALL(*mock_mdeh_, SetSequenceMode("primary", true));
EXPECT_CALL(*mock_mdeh_,
AddRole("primary", RelaxedParserSupportedType::kMP2T));
BindUrlToDataSource<StringHlsDataSourceStreamFactory>(
"http://media.example.com/manifest.m3u8",
kInitialRequestLiveMediaPlaylist);
EXPECT_CALL(*this, MockInitComplete(HasStatusCode(PIPELINE_OK)));
InitializeEngine();
task_environment_.RunUntilIdle();
// Assume that anything appended is valid, because we actually have no valid
// media for this test.
EXPECT_CALL(*mock_mdeh_, AppendAndParseData("primary", _, _, _))
.WillRepeatedly(Return(true));
BindUrlToDataSource<StringHlsDataSourceStreamFactory>(
"http://media.example.com/a.ts", "Cheese in a cstring is string cheese.");
BindUrlToDataSource<StringHlsDataSourceStreamFactory>(
"http://media.example.com/b.ts",
"Tomatoes are a fruit. Ketchup is a jam.");
BindUrlToDataSource<StringHlsDataSourceStreamFactory>(
"http://media.example.com/c.ts", "You've never been in an empty room.");
Ranges<base::TimeDelta> after_seg_a;
after_seg_a.Add(base::Seconds(0), base::Seconds(9));
Ranges<base::TimeDelta> after_seg_b;
after_seg_b.Add(base::Seconds(0), base::Seconds(18));
Ranges<base::TimeDelta> after_seg_c;
after_seg_c.Add(base::Seconds(0), base::Seconds(27));
EXPECT_CALL(*mock_mdeh_, GetBufferedRanges(_))
.WillOnce(Return(Ranges<base::TimeDelta>())) // First CheckState
.WillOnce(Return(after_seg_a)) // After appending segment A
.WillOnce(Return(after_seg_a)) // Second CheckState
.WillOnce(Return(after_seg_b)) // After appending segment B
.WillOnce(Return(after_seg_b)) // Third CheckState
.WillOnce(Return(after_seg_b)) // Fourth CheckState
.WillOnce(Return(after_seg_c)) // After appending segment C
.WillOnce(Return(after_seg_c)) // Fifth CheckState
;
// Give a playback-rate=1 time update, signaling start of play. Since a data
// append happens, we should ask for a delay of 0.
CallbackEnforcer<base::TimeDelta> first(base::Seconds(0));
engine_->OnTimeUpdate(base::Seconds(0), 1.0, first.GetCallback());
std::move(first).AssertAndReset(task_environment_);
// Make another request. This time, we have some data, but it's less than the
// ideal buffer size, so it will make another request to append data. Again
// because there was an append, we ask for a delay of 0.
CallbackEnforcer<base::TimeDelta> second(base::Seconds(0));
engine_->OnTimeUpdate(base::Seconds(0), 1.0, second.GetCallback());
std::move(second).AssertAndReset(task_environment_);
// Lets pretend time ticked forward 1 second while we were making that network
// request for segment 2, and now the range is {0-18}. we calculate that we
// can delay for (buffer - ideal/2) => (17 - 10/2) => 12.
CallbackEnforcer<base::TimeDelta> third(base::Seconds(12));
task_environment_.FastForwardBy(base::Seconds(1));
engine_->OnTimeUpdate(base::Seconds(1), 1.0, third.GetCallback());
std::move(third).AssertAndReset(task_environment_);
// Lets fast forward those 12 seconds and make another request. This will
// leave a remaining buffer of 5 seconds, which will trigger another segment
// read, which means a delay of 0.
CallbackEnforcer<base::TimeDelta> fourth(base::Seconds(0));
task_environment_.FastForwardBy(base::Seconds(12));
engine_->OnTimeUpdate(base::Seconds(13), 1.0, fourth.GetCallback());
std::move(fourth).AssertAndReset(task_environment_);
// This time, The buffer is large enough - 27 seconds is the end while the
// media time is only 13 seconds, but we've popped 3/4 segments from the
// queue, which leaves 1. Multiplying that by the 10 second max duration, we
// find that because it's been 13 seconds since the last manifest update, its
// time to make another one. We bind a new data stream to the manifest URL,
// which should populate the queue with more segments. There should then
// be a response of (27 - 13) - (10/2), or 9 seconds.
BindUrlToDataSource<StringHlsDataSourceStreamFactory>(
"http://media.example.com/manifest.m3u8",
kSecondRequestLiveMediaPlaylist);
CallbackEnforcer<base::TimeDelta> fifth(base::Seconds(9));
engine_->OnTimeUpdate(base::Seconds(13), 1.0, fifth.GetCallback());
std::move(fifth).AssertAndReset(task_environment_);
}
TEST_F(HlsManifestDemuxerEngineTest, TestMultivariantPlaylistNoAlternates) {
EXPECT_CALL(*mock_mdeh_, SetSequenceMode("primary", true));
EXPECT_CALL(*mock_mdeh_, SetDuration(21.021));
EXPECT_CALL(*mock_mdeh_,
AddRole("primary", RelaxedParserSupportedType::kMP2T));
BindUrlToDataSource<StringHlsDataSourceStreamFactory>(
"http://media.example.com/manifest.m3u8", kSimpleMultivariantPlaylist);
BindUrlToDataSource<StringHlsDataSourceStreamFactory>(
"http://example.com/hi.m3u8", kSimpleMediaPlaylist);
EXPECT_CALL(*this, MockInitComplete(HasStatusCode(PIPELINE_OK)));
InitializeEngine();
task_environment_.RunUntilIdle();
}
TEST_F(HlsManifestDemuxerEngineTest, TestMultivariantPlaylistWithAlternates) {
EXPECT_CALL(*mock_mdeh_, SetSequenceMode("audio-override", true));
EXPECT_CALL(*mock_mdeh_, SetSequenceMode("primary", true));
EXPECT_CALL(*mock_mdeh_, SetDuration(21.021));
EXPECT_CALL(*mock_mdeh_,
AddRole("audio-override", RelaxedParserSupportedType::kMP2T));
EXPECT_CALL(*mock_mdeh_,
AddRole("primary", RelaxedParserSupportedType::kMP2T));
// URL queries in order:
// - manifest.m3u8: root manifest
// - eng-audio.m3u8: audio override rendition playlist
// - only.ts: check the container/codecs for the audio override rendition
// - video-only.m3u8: primary rendition
// - first.ts: check container/codecs for the primary rendition
BindUrlToDataSource<StringHlsDataSourceStreamFactory>(
"http://media.example.com/manifest.m3u8", kMultivariantPlaylistWithAlts);
BindUrlToDataSource<StringHlsDataSourceStreamFactory>(
"http://media.example.com/eng-audio.m3u8", kSingleInfoMediaPlaylist);
BindUrlToDataSource<StringHlsDataSourceStreamFactory>(
"http://media.example.com/hi/video-only.m3u8", kSimpleMediaPlaylist);
EXPECT_CALL(*this, MockInitComplete(HasStatusCode(PIPELINE_OK)));
InitializeEngine();
task_environment_.RunUntilIdle();
}
TEST_F(HlsManifestDemuxerEngineTest, TestMultivariantWithNoSupportedCodecs) {
EXPECT_CALL(*mock_mdeh_, AddRole(_, _)).Times(0);
EXPECT_CALL(*mock_mdeh_, SetSequenceMode(_, _)).Times(0);
BindUrlToDataSource<StringHlsDataSourceStreamFactory>(
"http://media.example.com/manifest.m3u8", kUnsupportedCodecs);
EXPECT_CALL(*mock_mdeh_,
OnError(HasStatusCode(DEMUXER_ERROR_COULD_NOT_PARSE)));
InitializeEngine();
task_environment_.RunUntilIdle();
}
TEST_F(HlsManifestDemuxerEngineTest, TestAsyncSeek) {
auto rendition = std::make_unique<StrictMock<MockHlsRendition>>();
EXPECT_CALL(*rendition, GetDuration()).WillOnce(Return(base::Seconds(30)));
auto* rendition_ptr = rendition.get();
engine_->AddRenditionForTesting("primary", std::move(rendition));
// Set up rendition state and run, expecting no other callbacks.
task_environment_.RunUntilIdle();
// When seeking, indicate that we do not need to load more buffers.
EXPECT_CALL(*rendition_ptr, StartWaitingForSeek());
engine_->StartWaitingForSeek();
task_environment_.RunUntilIdle();
EXPECT_CALL(*rendition_ptr, Seek(_))
.WillOnce(Return(ManifestDemuxer::SeekState::kIsReady));
EXPECT_CALL(*mock_dsp_, AbortPendingReads(_)).WillOnce(RunOnceClosure<0>());
engine_->Seek(base::Seconds(10),
base::BindOnce([](ManifestDemuxer::SeekResponse resp) {
ASSERT_TRUE(resp.has_value());
ASSERT_EQ(std::move(resp).value(),
ManifestDemuxer::SeekState::kIsReady);
}));
task_environment_.RunUntilIdle();
// Destruction should call stop.
EXPECT_CALL(*rendition_ptr, Stop());
task_environment_.RunUntilIdle();
}
TEST_F(HlsManifestDemuxerEngineTest, TestMultiRenditionCheckState) {
auto rendition1 = std::make_unique<MockHlsRendition>();
auto rendition2 = std::make_unique<MockHlsRendition>();
EXPECT_CALL(*rendition1, GetDuration()).WillOnce(Return(std::nullopt));
EXPECT_CALL(*rendition2, GetDuration()).WillOnce(Return(std::nullopt));
auto* rend1 = rendition1.get();
auto* rend2 = rendition2.get();
engine_->AddRenditionForTesting("primary", std::move(rendition1));
// While there is only one rendition, the response from |OnTimeUpdate| is
// whatever that rendition wants.
EXPECT_CALL(*rend1, CheckState(_, _, _))
.WillOnce(RunOnceCallback<2>(base::Seconds(7)));
engine_->OnTimeUpdate(base::Seconds(0), 0.0,
base::BindOnce([](base::TimeDelta r) {
ASSERT_EQ(r, base::Seconds(7));
}));
EXPECT_CALL(*rend1, CheckState(_, _, _))
.WillOnce(RunOnceCallback<2>(kNoTimestamp));
engine_->OnTimeUpdate(
base::Seconds(0), 0.0,
base::BindOnce([](base::TimeDelta r) { ASSERT_EQ(r, kNoTimestamp); }));
// After adding the second rendition, the response from OnTimeUpdate is now
// the lesser of (rend1.response - (calc time of rend2)) and
// (rend2.response)
engine_->AddRenditionForTesting("audio-override", std::move(rendition2));
// Both renditions request time, so pick the lesser.
EXPECT_CALL(*rend1, CheckState(_, _, _))
.WillOnce(RunOnceCallback<2>(base::Seconds(7)));
EXPECT_CALL(*rend2, CheckState(_, _, _))
.WillOnce(RunOnceCallback<2>(base::Seconds(3)));
engine_->OnTimeUpdate(
base::Seconds(0), 0.0, base::BindOnce([](base::TimeDelta r) {
EXPECT_THAT(r, CloseTo(base::Seconds(3), base::Milliseconds(1)));
}));
// When one rendition provides kNoTimestamp and another does not, use the
// non-kNoTimestamp value.
EXPECT_CALL(*rend1, CheckState(_, _, _))
.WillOnce(RunOnceCallback<2>(kNoTimestamp));
EXPECT_CALL(*rend2, CheckState(_, _, _))
.WillOnce(RunOnceCallback<2>(base::Seconds(3)));
engine_->OnTimeUpdate(
base::Seconds(0), 0.0, base::BindOnce([](base::TimeDelta r) {
EXPECT_THAT(r, CloseTo(base::Seconds(3), base::Milliseconds(1)));
}));
EXPECT_CALL(*rend1, CheckState(_, _, _))
.WillOnce(RunOnceCallback<2>(base::Seconds(7)));
EXPECT_CALL(*rend2, CheckState(_, _, _))
.WillOnce(RunOnceCallback<2>(kNoTimestamp));
engine_->OnTimeUpdate(
base::Seconds(0), 0.0, base::BindOnce([](base::TimeDelta r) {
EXPECT_THAT(r, CloseTo(base::Seconds(7), base::Milliseconds(1)));
}));
}
TEST_F(HlsManifestDemuxerEngineTest, SeekAfterErrorFails) {
BindUrlToDataSource<StringHlsDataSourceStreamFactory>(
"http://media.example.com/manifest.m3u8", kInvalidMediaPlaylist);
EXPECT_CALL(*mock_mdeh_,
OnError(HasStatusCode(DEMUXER_ERROR_COULD_NOT_PARSE)));
EXPECT_CALL(*this, MockInitComplete(_)).Times(0);
InitializeEngine();
task_environment_.RunUntilIdle();
// When one of the renditions surfaces an error, ManifestDemuxer will request
// that the engine stop. Mimic that here.
engine_->Stop();
task_environment_.RunUntilIdle();
// Now if we try to seek, the response should be an instant aborted error.
engine_->Seek(base::Seconds(10),
base::BindOnce([](ManifestDemuxer::SeekResponse resp) {
ASSERT_FALSE(resp.has_value());
ASSERT_EQ(std::move(resp).error(), PIPELINE_ERROR_ABORT);
}));
task_environment_.RunUntilIdle();
}
TEST_F(HlsManifestDemuxerEngineTest, TestSeekDuringAdaptation) {
auto* rendition_ptr = SetUpInterruptTest();
// Start the adaptation and hold it from finishing.
base::OnceClosure continue_adaptation = StartAndCaptureNetworkAdaptation(
rendition_ptr, "http://example.com/low.m3u8", kSimpleMediaPlaylist,
1380000);
// Start a seek. It should wait while the adaptation is pending.
EXPECT_CALL(*this, SeekFinished()).Times(0);
EXPECT_CALL(*rendition_ptr, Seek(_)).Times(0);
EXPECT_CALL(*mock_dsp_, AbortPendingReads(_)).Times(0);
engine_->Seek(
base::Seconds(10),
base::BindOnce(
[](base::OnceClosure cb, ManifestDemuxer::SeekResponse resp) {
ASSERT_TRUE(resp.has_value());
std::move(cb).Run();
},
base::BindOnce(&HlsManifestDemuxerEngineTest::SeekFinished,
base::Unretained(this))));
task_environment_.RunUntilIdle();
// Set up final expectations for seek.
EXPECT_CALL(*this, SeekFinished());
EXPECT_CALL(*rendition_ptr, Seek(_))
.WillOnce(Return(ManifestDemuxer::SeekState::kIsReady));
EXPECT_CALL(*mock_dsp_, AbortPendingReads(_)).WillOnce(RunOnceClosure<0>());
// Finish the adaptation, seek should complete.
std::move(continue_adaptation).Run();
task_environment_.RunUntilIdle();
}
TEST_F(HlsManifestDemuxerEngineTest, TestSeekDuringTimeUpdate) {
auto* rendition_ptr = SetUpInterruptTest();
// Start the time update, and hold it from finishing.
base::OnceClosure continue_update =
StartAndCaptureTimeUpdate(rendition_ptr, base::Seconds(10));
// Start a seek. It should wait while the update is pending.
EXPECT_CALL(*this, SeekFinished()).Times(0);
EXPECT_CALL(*rendition_ptr, Seek(_)).Times(0);
EXPECT_CALL(*mock_dsp_, AbortPendingReads(_)).Times(0);
engine_->Seek(
base::Seconds(10),
base::BindOnce(
[](base::OnceClosure cb, ManifestDemuxer::SeekResponse resp) {
ASSERT_TRUE(resp.has_value());
std::move(cb).Run();
},
base::BindOnce(&HlsManifestDemuxerEngineTest::SeekFinished,
base::Unretained(this))));
task_environment_.RunUntilIdle();
// Set up final expectations for seek.
EXPECT_CALL(*this, SeekFinished());
EXPECT_CALL(*rendition_ptr, Seek(_))
.WillOnce(Return(ManifestDemuxer::SeekState::kIsReady));
EXPECT_CALL(*mock_dsp_, AbortPendingReads(_)).WillOnce(RunOnceClosure<0>());
// Finish the update, seek should complete.
std::move(continue_update).Run();
task_environment_.RunUntilIdle();
}
TEST_F(HlsManifestDemuxerEngineTest, TestAdaptDuringTimeUpdate) {
auto* rendition_ptr = SetUpInterruptTest();
// Start the time update, and hold it from finishing.
base::OnceClosure continue_update =
StartAndCaptureTimeUpdate(rendition_ptr, base::Seconds(10));
// Start an adaptation. It should wait while the update is pending.
ExpectNoNetworkRequests();
engine_->UpdateNetworkSpeed(1380000);
task_environment_.RunUntilIdle();
// When the update finishes, the adaptation requests the low quality stream.
BindUrlAssignmentThunk<StringHlsDataSourceStreamFactory>(
"http://example.com/low.m3u8", kSimpleMediaPlaylist);
std::move(continue_update).Run();
task_environment_.RunUntilIdle();
}
TEST_F(HlsManifestDemuxerEngineTest, TestAdaptDuringSeek) {
auto* rendition_ptr = SetUpInterruptTest();
// Start the seek, and hold it so it can't finish.
base::OnceClosure continue_seek = StartAndCaptureSeek(rendition_ptr);
// Start an adaptation. It should wait while the seek is pending.
ExpectNoNetworkRequests();
engine_->UpdateNetworkSpeed(1380000);
task_environment_.RunUntilIdle();
// When the seek finishes, the adaptation requests the low quality stream.
BindUrlAssignmentThunk<StringHlsDataSourceStreamFactory>(
"http://example.com/low.m3u8", kSimpleMediaPlaylist);
EXPECT_CALL(*this, SeekFinished());
std::move(continue_seek).Run();
task_environment_.RunUntilIdle();
}
TEST_F(HlsManifestDemuxerEngineTest, TestTimeUpdateDuringAdaptation) {
auto* rendition_ptr = SetUpInterruptTest();
// Start the adaptation and hold it from finishing.
base::OnceClosure continue_adaptation = StartAndCaptureNetworkAdaptation(
rendition_ptr, "http://example.com/low.m3u8", kSimpleMediaPlaylist,
1380000);
// Start the time update
EXPECT_CALL(*rendition_ptr, CheckState(_, _, _)).Times(0);
engine_->OnTimeUpdate(base::Seconds(0), 0.0,
base::BindOnce([](base::TimeDelta r) {
ASSERT_EQ(r, base::Seconds(10));
}));
task_environment_.RunUntilIdle();
// Set expectations for update finishing.
EXPECT_CALL(*rendition_ptr, CheckState(_, _, _))
.WillOnce(RunOnceCallback<2>(base::Seconds(10)));
// Finish adaptation.
std::move(continue_adaptation).Run();
task_environment_.RunUntilIdle();
}
TEST_F(HlsManifestDemuxerEngineTest, TestTimeUpdateDuringSeek) {
auto* rendition_ptr = SetUpInterruptTest();
// Start the seek and hold it from finishing.
base::OnceClosure continue_seek = StartAndCaptureSeek(rendition_ptr);
// Start the time update
EXPECT_CALL(*rendition_ptr, CheckState(_, _, _)).Times(0);
engine_->OnTimeUpdate(base::Seconds(0), 0.0,
base::BindOnce([](base::TimeDelta r) {
ASSERT_EQ(r, base::Seconds(10));
}));
task_environment_.RunUntilIdle();
// Set expectations for update finishing.
EXPECT_CALL(*rendition_ptr, CheckState(_, _, _))
.WillOnce(RunOnceCallback<2>(base::Seconds(10)));
// Finish seek.
EXPECT_CALL(*this, SeekFinished());
std::move(continue_seek).Run();
task_environment_.RunUntilIdle();
}
TEST_F(HlsManifestDemuxerEngineTest, TestEndOfStreamAfterAllFetched) {
// All the expectations set during the initialization process.
EXPECT_CALL(*mock_mdeh_, SetSequenceMode("primary", true));
EXPECT_CALL(*mock_mdeh_,
AddRole("primary", RelaxedParserSupportedType::kMP2T));
EXPECT_CALL(*mock_mdeh_, SetDuration(9.009));
EXPECT_CALL(*this, MockInitComplete(HasStatusCode(PIPELINE_OK)));
// We can't use `BindUrlToDataSource` here, since it can't re-create streams
// like we need it to. The network requests are in order:
// - manifest.m3u8 - main manifest
// - first.ts - request for the first few bytes to do codec detection
// - first.ts - request for chunks of data to add to ChunkDemuxer
std::string bitstream = "hey, this isn't a bitstream!";
EXPECT_CALL(*mock_dsp_,
ReadFromCombinedUrlQueue(
SingleSegmentQueue("http://media.example.com/manifest.m3u8",
std::nullopt),
_))
.WillOnce(RunOnceCallback<1>(
StringHlsDataSourceStreamFactory::CreateStream(kShortMediaPlaylist)));
EXPECT_CALL(
*mock_dsp_,
ReadFromCombinedUrlQueue(
SingleSegmentQueue("http://media.example.com/first.ts", std::nullopt),
_))
.WillOnce(RunOnceCallback<1>(
StringHlsDataSourceStreamFactory::CreateStream(bitstream)));
// `GetBufferedRanges` gets called many times during this process:
// - HlsVodRendition::CheckState (1) => empty ranges, nothing loaded.
// - HlsVodRendition::OnSegmentData (1) => populated by AppendAndParseData
// - HlsVodRendition::CheckState (2) => still has data
Ranges<base::TimeDelta> populated_ranges;
populated_ranges.Add(base::Seconds(0), base::Seconds(5));
EXPECT_CALL(*mock_mdeh_, GetBufferedRanges(_))
.WillOnce(Return(Ranges<base::TimeDelta>()))
.WillOnce(Return(populated_ranges))
.WillOnce(Return(populated_ranges));
// The first call to `OnTimeUpdate` should trigger the append function,
// and our data was 30 characters long.
EXPECT_CALL(*mock_mdeh_, AppendAndParseData("primary", _, _,
base::as_byte_span(bitstream)))
.WillOnce(Return(true));
// Finally, and EndOfStream call happens:
EXPECT_CALL(*mock_mdeh_, SetEndOfStream());
// And then teardown:
EXPECT_CALL(*mock_mdeh_, RemoveRole("primary"));
// Setup with a mock codec detector - this will set all the roles, duration,
// modes, and also make a request for the manifest and the first segment.
InitializeEngine();
task_environment_.RunUntilIdle();
// For the first state check, there should be empty ranges, which triggers
// `HlsVodRendition::FetchNext`, which should request the data from first.ts
// add its content, and then return.
engine_->OnTimeUpdate(base::Seconds(0), 1.0, base::DoNothing());
task_environment_.RunUntilIdle();
// For the second state check, there are no more segments, no pending segment,
// and there are loaded ranges, so HlsVodRendition will report an EndOfStream.
engine_->OnTimeUpdate(base::Seconds(6), 1.0, base::DoNothing());
task_environment_.RunUntilIdle();
// Expectations on teardown.
ASSERT_TRUE(engine_->IsSeekable());
task_environment_.RunUntilIdle();
}
TEST_F(HlsManifestDemuxerEngineTest, TestEndOfStreamPropagatesOnce) {
auto rendition1 = std::make_unique<MockHlsRendition>();
auto rendition2 = std::make_unique<MockHlsRendition>();
EXPECT_CALL(*rendition1, Stop());
EXPECT_CALL(*rendition2, Stop());
BindUrlToDataSource<StringHlsDataSourceStreamFactory>(
"http://media.example.com/manifest.m3u8", kInvalidMediaPlaylist);
EXPECT_CALL(*mock_mdeh_,
OnError(HasStatusCode(DEMUXER_ERROR_COULD_NOT_PARSE)));
EXPECT_CALL(*this, MockInitComplete(_)).Times(0);
InitializeEngine();
task_environment_.RunUntilIdle();
// Start with one rendition, to demonstrate that when it ends/starts, that
// event always bubbles up.
EXPECT_CALL(*rendition1, GetDuration()).WillOnce(Return(std::nullopt));
engine_->AddRenditionForTesting("primary", std::move(rendition1));
EXPECT_CALL(*mock_mdeh_, SetEndOfStream());
engine_->SetEndOfStream(true);
EXPECT_CALL(*mock_mdeh_, UnsetEndOfStream());
engine_->SetEndOfStream(false);
// Add a second rendition, to demonstrate seeks and reaching end state. Both
// are currently in the "unended" state.
EXPECT_CALL(*rendition2, GetDuration()).WillOnce(Return(std::nullopt));
engine_->AddRenditionForTesting("audio", std::move(rendition2));
// One rendition reaches end, nothing happens.
EXPECT_CALL(*mock_mdeh_, SetEndOfStream()).Times(0);
engine_->SetEndOfStream(true);
// Once all are ended, host gets notified.
EXPECT_CALL(*mock_mdeh_, SetEndOfStream());
engine_->SetEndOfStream(true);
// during seek, the first rendition goes unended - this notifies the host.
EXPECT_CALL(*mock_mdeh_, UnsetEndOfStream());
engine_->SetEndOfStream(false);
// during seek, the second rendition goes unended - this does nothing, as
// the host already knows the stream is unended.
EXPECT_CALL(*mock_mdeh_, UnsetEndOfStream()).Times(0);
engine_->SetEndOfStream(false);
task_environment_.RunUntilIdle();
}
TEST_F(HlsManifestDemuxerEngineTest, TestOriginTainting) {
EXPECT_CALL(*mock_mdeh_, SetSequenceMode("primary", true));
EXPECT_CALL(*mock_mdeh_, SetDuration(21.021));
EXPECT_CALL(*mock_mdeh_,
AddRole("primary", RelaxedParserSupportedType::kMP2T));
BindUrlToDataSource<StringHlsDataSourceStreamFactory>(
"http://media.example.com/manifest.m3u8", kSimpleMultivariantPlaylist,
/*taint_origin=*/true);
BindUrlToDataSource<StringHlsDataSourceStreamFactory>(
"http://example.com/hi.m3u8", kSimpleMediaPlaylist);
EXPECT_CALL(*this, MockInitComplete(HasStatusCode(PIPELINE_OK)));
InitializeEngine();
task_environment_.RunUntilIdle();
ASSERT_TRUE(engine_->WouldTaintOrigin());
}
} // namespace media