blob: 088d991cac98b4bfb44b777271028cee039184e0 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "media/filters/pipeline_integration_test_base.h"
#include "base/bind.h"
#include "base/string_util.h"
#include "media/base/decoder_buffer.h"
#include "media/base/decryptor_client.h"
#include "media/base/test_data_util.h"
#include "media/crypto/aes_decryptor.h"
using testing::AtMost;
namespace media {
static const char kSourceId[] = "SourceId";
static const char kClearKeySystem[] = "org.w3.clearkey";
static const uint8 kInitData[] = { 0x69, 0x6e, 0x69, 0x74 };
static const char kWebM[] = "video/webm; codecs=\"vp8,vorbis\"";
static const char kAudioOnlyWebM[] = "video/webm; codecs=\"vorbis\"";
static const char kVideoOnlyWebM[] = "video/webm; codecs=\"vp8\"";
static const char kMP4[] = "video/mp4; codecs=\"avc1.4D4041,mp4a.40.2\"";
// Key used to encrypt video track in test file "bear-320x240-encrypted.webm".
static const uint8 kSecretKey[] = {
0xeb, 0xdd, 0x62, 0xf1, 0x68, 0x14, 0xd2, 0x7b,
0x68, 0xef, 0x12, 0x2a, 0xfc, 0xe4, 0xae, 0x3c
};
static const int kAppendWholeFile = -1;
// Helper class that emulates calls made on the ChunkDemuxer by the
// Media Source API.
class MockMediaSource {
public:
MockMediaSource(const std::string& filename, const std::string& mimetype,
int initial_append_size)
: file_path_(GetTestDataFilePath(filename)),
current_position_(0),
initial_append_size_(initial_append_size),
mimetype_(mimetype) {
chunk_demuxer_ = new ChunkDemuxer(
base::Bind(&MockMediaSource::DemuxerOpened, base::Unretained(this)),
base::Bind(&MockMediaSource::DemuxerNeedKey, base::Unretained(this)),
LogCB());
file_data_ = ReadTestDataFile(filename);
if (initial_append_size_ == kAppendWholeFile)
initial_append_size_ = file_data_->GetDataSize();
DCHECK_GT(initial_append_size_, 0);
DCHECK_LE(initial_append_size_, file_data_->GetDataSize());
}
virtual ~MockMediaSource() {}
const scoped_refptr<ChunkDemuxer>& demuxer() const { return chunk_demuxer_; }
void set_decryptor_client(DecryptorClient* decryptor_client) {
decryptor_client_ = decryptor_client;
}
void Seek(int new_position, int seek_append_size) {
chunk_demuxer_->StartWaitingForSeek();
chunk_demuxer_->Abort(kSourceId);
DCHECK_GE(new_position, 0);
DCHECK_LT(new_position, file_data_->GetDataSize());
current_position_ = new_position;
AppendData(seek_append_size);
}
void AppendData(int size) {
DCHECK(chunk_demuxer_.get());
DCHECK_LT(current_position_, file_data_->GetDataSize());
DCHECK_LE(current_position_ + size, file_data_->GetDataSize());
CHECK(chunk_demuxer_->AppendData(
kSourceId, file_data_->GetData() + current_position_, size));
current_position_ += size;
}
void AppendAtTime(const base::TimeDelta& timestampOffset,
const uint8* pData, int size) {
CHECK(chunk_demuxer_->SetTimestampOffset(kSourceId, timestampOffset));
CHECK(chunk_demuxer_->AppendData(kSourceId, pData, size));
CHECK(chunk_demuxer_->SetTimestampOffset(kSourceId, base::TimeDelta()));
}
void EndOfStream() {
chunk_demuxer_->EndOfStream(PIPELINE_OK);
}
void Abort() {
if (!chunk_demuxer_.get())
return;
chunk_demuxer_->Shutdown();
chunk_demuxer_ = NULL;
}
void DemuxerOpened() {
MessageLoop::current()->PostTask(
FROM_HERE, base::Bind(&MockMediaSource::DemuxerOpenedTask,
base::Unretained(this)));
}
void DemuxerOpenedTask() {
size_t semicolon = mimetype_.find(";");
std::string type = mimetype_.substr(0, semicolon);
size_t quote1 = mimetype_.find("\"");
size_t quote2 = mimetype_.find("\"", quote1 + 1);
std::string codecStr = mimetype_.substr(quote1 + 1, quote2 - quote1 - 1);
std::vector<std::string> codecs;
Tokenize(codecStr, ",", &codecs);
CHECK_EQ(chunk_demuxer_->AddId(kSourceId, type, codecs), ChunkDemuxer::kOk);
AppendData(initial_append_size_);
}
void DemuxerNeedKey(const std::string& type,
scoped_array<uint8> init_data, int init_data_size) {
DCHECK(init_data.get());
DCHECK_GT(init_data_size, 0);
DCHECK(decryptor_client_);
decryptor_client_->NeedKey("", "", type, init_data.Pass(), init_data_size);
}
private:
FilePath file_path_;
scoped_refptr<DecoderBuffer> file_data_;
int current_position_;
int initial_append_size_;
std::string mimetype_;
scoped_refptr<ChunkDemuxer> chunk_demuxer_;
DecryptorClient* decryptor_client_;
};
class FakeDecryptorClient : public DecryptorClient {
public:
FakeDecryptorClient() : decryptor_(this) {}
AesDecryptor* decryptor() {
return &decryptor_;
}
// DecryptorClient implementation.
virtual void KeyAdded(const std::string& key_system,
const std::string& session_id) {
EXPECT_EQ(kClearKeySystem, key_system);
EXPECT_FALSE(session_id.empty());
}
virtual void KeyError(const std::string& key_system,
const std::string& session_id,
AesDecryptor::KeyError error_code,
int system_code) {
NOTIMPLEMENTED();
}
virtual void KeyMessage(const std::string& key_system,
const std::string& session_id,
const std::string& message,
const std::string& default_url) {
EXPECT_EQ(kClearKeySystem, key_system);
EXPECT_FALSE(session_id.empty());
EXPECT_FALSE(message.empty());
current_key_system_ = key_system;
current_session_id_ = session_id;
}
virtual void NeedKey(const std::string& key_system,
const std::string& session_id,
const std::string& type,
scoped_array<uint8> init_data,
int init_data_length) {
current_key_system_ = key_system;
current_session_id_ = session_id;
// When NeedKey is called from the demuxer, the |key_system| will be empty.
// In this case, we need to call GenerateKeyRequest() to initialize a
// session (which will call KeyMessage).
if (current_key_system_.empty()) {
DCHECK(current_session_id_.empty());
EXPECT_TRUE(decryptor_.GenerateKeyRequest(
kClearKeySystem, type, kInitData, arraysize(kInitData)));
}
EXPECT_FALSE(current_key_system_.empty());
EXPECT_FALSE(current_session_id_.empty());
decryptor_.AddKey(current_key_system_, kSecretKey, arraysize(kSecretKey),
init_data.get(), init_data_length, current_session_id_);
}
private:
AesDecryptor decryptor_;
std::string current_key_system_;
std::string current_session_id_;
};
class PipelineIntegrationTest
: public testing::Test,
public PipelineIntegrationTestBase {
public:
void StartPipelineWithMediaSource(MockMediaSource* source) {
EXPECT_CALL(*this, OnBufferingState(Pipeline::kHaveMetadata))
.Times(AtMost(1));
EXPECT_CALL(*this, OnBufferingState(Pipeline::kPrerollCompleted))
.Times(AtMost(1));
pipeline_->Start(
CreateFilterCollection(source->demuxer(), NULL),
base::Bind(&PipelineIntegrationTest::OnEnded, base::Unretained(this)),
base::Bind(&PipelineIntegrationTest::OnError, base::Unretained(this)),
QuitOnStatusCB(PIPELINE_OK),
base::Bind(&PipelineIntegrationTest::OnBufferingState,
base::Unretained(this)));
message_loop_.Run();
}
void StartPipelineWithEncryptedMedia(
MockMediaSource* source,
FakeDecryptorClient* encrypted_media) {
EXPECT_CALL(*this, OnBufferingState(Pipeline::kHaveMetadata))
.Times(AtMost(1));
EXPECT_CALL(*this, OnBufferingState(Pipeline::kPrerollCompleted))
.Times(AtMost(1));
pipeline_->Start(
CreateFilterCollection(source->demuxer(), encrypted_media->decryptor()),
base::Bind(&PipelineIntegrationTest::OnEnded, base::Unretained(this)),
base::Bind(&PipelineIntegrationTest::OnError, base::Unretained(this)),
QuitOnStatusCB(PIPELINE_OK),
base::Bind(&PipelineIntegrationTest::OnBufferingState,
base::Unretained(this)));
source->set_decryptor_client(encrypted_media);
message_loop_.Run();
}
// Verifies that seeking works properly for ChunkDemuxer when the
// seek happens while there is a pending read on the ChunkDemuxer
// and no data is available.
bool TestSeekDuringRead(const std::string& filename,
const std::string& mimetype,
int initial_append_size,
base::TimeDelta start_seek_time,
base::TimeDelta seek_time,
int seek_file_position,
int seek_append_size) {
MockMediaSource source(filename, mimetype, initial_append_size);
StartPipelineWithMediaSource(&source);
if (pipeline_status_ != PIPELINE_OK)
return false;
Play();
if (!WaitUntilCurrentTimeIsAfter(start_seek_time))
return false;
source.Seek(seek_file_position, seek_append_size);
if (!Seek(seek_time))
return false;
source.EndOfStream();
source.Abort();
Stop();
return true;
}
};
TEST_F(PipelineIntegrationTest, BasicPlayback) {
ASSERT_TRUE(Start(GetTestDataFilePath("bear-320x240.webm"), PIPELINE_OK));
Play();
ASSERT_TRUE(WaitUntilOnEnded());
}
TEST_F(PipelineIntegrationTest, BasicPlaybackHashed) {
ASSERT_TRUE(Start(GetTestDataFilePath("bear-320x240.webm"),
PIPELINE_OK, true));
Play();
ASSERT_TRUE(WaitUntilOnEnded());
EXPECT_EQ(GetVideoHash(), "f0be120a90a811506777c99a2cdf7cc1");
EXPECT_EQ(GetAudioHash(), "5699a4415b620e45b9d0aae531c9df76");
}
TEST_F(PipelineIntegrationTest, BasicPlayback_MediaSource) {
MockMediaSource source("bear-320x240.webm", kWebM, 219229);
StartPipelineWithMediaSource(&source);
source.EndOfStream();
ASSERT_EQ(pipeline_status_, PIPELINE_OK);
EXPECT_EQ(pipeline_->GetBufferedTimeRanges().size(), 1u);
EXPECT_EQ(pipeline_->GetBufferedTimeRanges().start(0).InMilliseconds(), 0);
EXPECT_EQ(pipeline_->GetBufferedTimeRanges().end(0).InMilliseconds(), 2737);
Play();
ASSERT_TRUE(WaitUntilOnEnded());
source.Abort();
Stop();
}
TEST_F(PipelineIntegrationTest, MediaSource_ConfigChange_WebM) {
MockMediaSource source("bear-320x240-16x9-aspect.webm", kWebM,
kAppendWholeFile);
StartPipelineWithMediaSource(&source);
scoped_refptr<DecoderBuffer> second_file =
ReadTestDataFile("bear-640x360.webm");
source.AppendAtTime(base::TimeDelta::FromSeconds(2),
second_file->GetData(), second_file->GetDataSize());
source.EndOfStream();
ASSERT_EQ(pipeline_status_, PIPELINE_OK);
EXPECT_EQ(pipeline_->GetBufferedTimeRanges().size(), 1u);
EXPECT_EQ(pipeline_->GetBufferedTimeRanges().start(0).InMilliseconds(), 0);
EXPECT_EQ(pipeline_->GetBufferedTimeRanges().end(0).InMilliseconds(), 4763);
Play();
ASSERT_TRUE(WaitUntilOnEnded());
source.Abort();
Stop();
}
#if defined(GOOGLE_CHROME_BUILD) || defined(USE_PROPRIETARY_CODECS)
TEST_F(PipelineIntegrationTest, MediaSource_ConfigChange_MP4) {
MockMediaSource source("bear.640x360_dash.mp4", kMP4, kAppendWholeFile);
StartPipelineWithMediaSource(&source);
scoped_refptr<DecoderBuffer> second_file =
ReadTestDataFile("bear.1280x720_dash.mp4");
source.AppendAtTime(base::TimeDelta::FromSeconds(2),
second_file->GetData(), second_file->GetDataSize());
source.EndOfStream();
ASSERT_EQ(pipeline_status_, PIPELINE_OK);
EXPECT_EQ(pipeline_->GetBufferedTimeRanges().size(), 1u);
EXPECT_EQ(pipeline_->GetBufferedTimeRanges().start(0).InMilliseconds(), 0);
EXPECT_EQ(pipeline_->GetBufferedTimeRanges().end(0).InMilliseconds(), 4736);
Play();
ASSERT_TRUE(WaitUntilOnEnded());
source.Abort();
Stop();
}
#endif
TEST_F(PipelineIntegrationTest, BasicPlayback_16x9AspectRatio) {
ASSERT_TRUE(Start(GetTestDataFilePath("bear-320x240-16x9-aspect.webm"),
PIPELINE_OK));
Play();
ASSERT_TRUE(WaitUntilOnEnded());
}
TEST_F(PipelineIntegrationTest, EncryptedPlayback) {
MockMediaSource source("bear-320x240-encrypted.webm", kWebM, 219816);
FakeDecryptorClient encrypted_media;
StartPipelineWithEncryptedMedia(&source, &encrypted_media);
source.EndOfStream();
ASSERT_EQ(PIPELINE_OK, pipeline_status_);
Play();
ASSERT_TRUE(WaitUntilOnEnded());
source.Abort();
Stop();
}
// TODO(acolwell): Fix flakiness http://crbug.com/117921
TEST_F(PipelineIntegrationTest, DISABLED_SeekWhilePaused) {
ASSERT_TRUE(Start(GetTestDataFilePath("bear-320x240.webm"), PIPELINE_OK));
base::TimeDelta duration(pipeline_->GetMediaDuration());
base::TimeDelta start_seek_time(duration / 4);
base::TimeDelta seek_time(duration * 3 / 4);
Play();
ASSERT_TRUE(WaitUntilCurrentTimeIsAfter(start_seek_time));
Pause();
ASSERT_TRUE(Seek(seek_time));
EXPECT_EQ(pipeline_->GetMediaTime(), seek_time);
Play();
ASSERT_TRUE(WaitUntilOnEnded());
// Make sure seeking after reaching the end works as expected.
Pause();
ASSERT_TRUE(Seek(seek_time));
EXPECT_EQ(pipeline_->GetMediaTime(), seek_time);
Play();
ASSERT_TRUE(WaitUntilOnEnded());
}
// TODO(acolwell): Fix flakiness http://crbug.com/117921
TEST_F(PipelineIntegrationTest, DISABLED_SeekWhilePlaying) {
ASSERT_TRUE(Start(GetTestDataFilePath("bear-320x240.webm"), PIPELINE_OK));
base::TimeDelta duration(pipeline_->GetMediaDuration());
base::TimeDelta start_seek_time(duration / 4);
base::TimeDelta seek_time(duration * 3 / 4);
Play();
ASSERT_TRUE(WaitUntilCurrentTimeIsAfter(start_seek_time));
ASSERT_TRUE(Seek(seek_time));
EXPECT_GE(pipeline_->GetMediaTime(), seek_time);
ASSERT_TRUE(WaitUntilOnEnded());
// Make sure seeking after reaching the end works as expected.
ASSERT_TRUE(Seek(seek_time));
EXPECT_GE(pipeline_->GetMediaTime(), seek_time);
ASSERT_TRUE(WaitUntilOnEnded());
}
// Verify audio decoder & renderer can handle aborted demuxer reads.
TEST_F(PipelineIntegrationTest, ChunkDemuxerAbortRead_AudioOnly) {
ASSERT_TRUE(TestSeekDuringRead("bear-320x240-audio-only.webm", kAudioOnlyWebM,
8192,
base::TimeDelta::FromMilliseconds(464),
base::TimeDelta::FromMilliseconds(617),
0x10CA, 19730));
}
// Verify video decoder & renderer can handle aborted demuxer reads.
TEST_F(PipelineIntegrationTest, ChunkDemuxerAbortRead_VideoOnly) {
ASSERT_TRUE(TestSeekDuringRead("bear-320x240-video-only.webm", kVideoOnlyWebM,
32768,
base::TimeDelta::FromMilliseconds(200),
base::TimeDelta::FromMilliseconds(1668),
0x1C896, 65536));
}
} // namespace media