blob: feef587851f4f73dacbef10610a64ae0490126bd [file] [log] [blame]
// 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 <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 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 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 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;
class MockManifestDemuxerEngineHost : public ManifestDemuxerEngineHost {
public:
MOCK_METHOD(bool,
AddRole,
(base::StringPiece, std::string, std::string),
(override));
MOCK_METHOD(void, RemoveRole, (base::StringPiece), (override));
MOCK_METHOD(void, SetSequenceMode, (base::StringPiece, bool), (override));
MOCK_METHOD(void, SetDuration, (double), (override));
MOCK_METHOD(Ranges<base::TimeDelta>,
GetBufferedRanges,
(base::StringPiece),
(override));
MOCK_METHOD(void,
Remove,
(base::StringPiece, base::TimeDelta, base::TimeDelta),
(override));
MOCK_METHOD(
void,
RemoveAndReset,
(base::StringPiece, base::TimeDelta, base::TimeDelta, base::TimeDelta*),
(override));
MOCK_METHOD(void,
SetGroupStartIfParsingAndSequenceMode,
(base::StringPiece, base::TimeDelta),
(override));
MOCK_METHOD(void,
EvictCodedFrames,
(base::StringPiece, base::TimeDelta, size_t),
(override));
MOCK_METHOD(bool,
AppendAndParseData,
(base::StringPiece,
base::TimeDelta,
base::TimeDelta,
base::TimeDelta*,
const uint8_t*,
size_t),
(override));
MOCK_METHOD(void, OnError, (PipelineStatus), (override));
};
class MockHlsDataSourceProvider : public HlsDataSourceProvider {
public:
MOCK_METHOD(std::unique_ptr<HlsDataSource>, GetDataSource, (std::string));
void RequestDataSource(GURL url,
absl::optional<hls::types::ByteRange> br,
RequestCb cb) override {
std::move(cb).Run(GetDataSource(url.spec()));
}
};
class FakeHlsDataSourceProvider : public HlsDataSourceProvider {
private:
base::raw_ptr<HlsDataSourceProvider> mock_;
public:
FakeHlsDataSourceProvider(HlsDataSourceProvider* mock) : mock_(mock) {}
void RequestDataSource(GURL url,
absl::optional<hls::types::ByteRange> range,
RequestCb request) override {
mock_->RequestDataSource(url, range, std::move(request));
}
};
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_;
std::unique_ptr<HlsManifestDemuxerEngine> engine_;
MOCK_METHOD(void, MockInitComplete, (PipelineStatus status), ());
template <typename T>
void BindUrlToDataSource(std::string url, std::string value) {
EXPECT_CALL(*mock_dsp_, GetDataSource(url))
.Times(1)
.WillOnce(Return(ByMove(std::make_unique<T>(value))));
}
public:
HlsManifestDemuxerEngineTest()
: media_log_(std::make_unique<NiceMock<media::MockMediaLog>>()),
mock_mdeh_(std::make_unique<MockManifestDemuxerEngineHost>()),
mock_dsp_(std::make_unique<MockHlsDataSourceProvider>()) {
ON_CALL(*mock_mdeh_, AddRole(_, _, _)).WillByDefault(Return(true));
ON_CALL(*mock_mdeh_, GetBufferedRanges(_))
.WillByDefault(Return(Ranges<base::TimeDelta>()));
base::SequenceBound<FakeHlsDataSourceProvider> dsp(
task_environment_.GetMainThreadTaskRunner(), mock_dsp_.get());
engine_ = std::make_unique<HlsManifestDemuxerEngine>(
std::move(dsp), base::SingleThreadTaskRunner::GetCurrentDefault(),
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 { base::RunLoop().RunUntilIdle(); }
};
TEST_F(HlsManifestDemuxerEngineTest, TestSimpleConfigAddsOnePrimaryRole) {
EXPECT_CALL(*mock_mdeh_, SetSequenceMode(base::StringPiece("primary"), true));
EXPECT_CALL(*mock_mdeh_, SetDuration(21.021));
EXPECT_CALL(*mock_mdeh_, AddRole(base::StringPiece("primary"), "video/mp2t",
"avc1.420000, mp4a.40.05"));
BindUrlToDataSource<StringHlsDataSource>(
"http://media.example.com/manifest.m3u8", kSimpleMediaPlaylist);
BindUrlToDataSource<FileHlsDataSource>("http://media.example.com/first.ts",
"bear-1280x720-hls.ts");
EXPECT_CALL(*this, MockInitComplete(HasStatusCode(PIPELINE_OK)));
InitializeEngine();
task_environment_.RunUntilIdle();
}
TEST_F(HlsManifestDemuxerEngineTest, TestMultivariantPlaylistNoAlternates) {
EXPECT_CALL(*mock_mdeh_, SetSequenceMode(base::StringPiece("primary"), true));
EXPECT_CALL(*mock_mdeh_, SetDuration(21.021));
EXPECT_CALL(*mock_mdeh_, AddRole(base::StringPiece("primary"), "video/mp2t",
"avc1.420000, mp4a.40.05"));
BindUrlToDataSource<StringHlsDataSource>(
"http://media.example.com/manifest.m3u8", kSimpleMultivariantPlaylist);
BindUrlToDataSource<StringHlsDataSource>("http://example.com/hi.m3u8",
kSimpleMediaPlaylist);
BindUrlToDataSource<FileHlsDataSource>("http://media.example.com/first.ts",
"bear-1280x720-hls.ts");
EXPECT_CALL(*this, MockInitComplete(HasStatusCode(PIPELINE_OK)));
InitializeEngine();
task_environment_.RunUntilIdle();
}
TEST_F(HlsManifestDemuxerEngineTest, TestMultivariantPlaylistWithAlternates) {
EXPECT_CALL(*mock_mdeh_,
SetSequenceMode(base::StringPiece("audio-override"), true));
EXPECT_CALL(*mock_mdeh_, SetSequenceMode(base::StringPiece("primary"), true));
EXPECT_CALL(*mock_mdeh_, SetDuration(21.021));
EXPECT_CALL(*mock_mdeh_, AddRole(base::StringPiece("audio-override"),
"video/mp2t", "avc1.420000"));
EXPECT_CALL(*mock_mdeh_, AddRole(base::StringPiece("primary"), "video/mp2t",
"avc1.420000"));
// 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<StringHlsDataSource>(
"http://media.example.com/manifest.m3u8", kMultivariantPlaylistWithAlts);
BindUrlToDataSource<StringHlsDataSource>(
"http://media.example.com/eng-audio.m3u8", kSingleInfoMediaPlaylist);
BindUrlToDataSource<FileHlsDataSource>("http://media.example.com/only.ts",
"bear-1280x720-aac_he.ts");
BindUrlToDataSource<StringHlsDataSource>(
"http://media.example.com/hi/video-only.m3u8", kSimpleMediaPlaylist);
BindUrlToDataSource<FileHlsDataSource>("http://media.example.com/first.ts",
"bear-1280x720-hls.ts");
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<StringHlsDataSource>(
"http://media.example.com/manifest.m3u8", kUnsupportedCodecs);
EXPECT_CALL(*mock_mdeh_,
OnError(HasStatusCode(DEMUXER_ERROR_COULD_NOT_PARSE)));
InitializeEngine();
task_environment_.RunUntilIdle();
}
} // namespace media