Fix flaky packager_test due to timestamp diff

Change muxer interface to allow injection of testing clock. Also added
more packager_tests.

Change-Id: Ie580cbd7e79607a2c2b9df5d5d52ee4be108ff8f
This commit is contained in:
Kongqun Yang 2014-02-27 18:39:52 -08:00 committed by KongQun Yang
parent cca3767c25
commit e2b29552e9
5 changed files with 260 additions and 127 deletions

View File

@ -16,7 +16,8 @@ Muxer::Muxer(const MuxerOptions& options)
: options_(options), : options_(options),
encryptor_source_(NULL), encryptor_source_(NULL),
clear_lead_in_seconds_(0), clear_lead_in_seconds_(0),
muxer_listener_(NULL) {} muxer_listener_(NULL),
clock_(NULL) {}
Muxer::~Muxer() {} Muxer::~Muxer() {}

View File

@ -16,6 +16,10 @@
#include "media/base/muxer_options.h" #include "media/base/muxer_options.h"
#include "media/base/status.h" #include "media/base/status.h"
namespace base {
class Clock;
}
namespace media { namespace media {
class EncryptorSource; class EncryptorSource;
@ -58,11 +62,21 @@ class Muxer {
const std::vector<MediaStream*>& streams() const { return streams_; } const std::vector<MediaStream*>& streams() const { return streams_; }
// Inject clock, mainly used for testing.
// The injected clock will be used to generate the creation time-stamp and
// modification time-stamp of the muxer output.
// If no clock is injected, the code uses base::Time::Now() to generate the
// time-stamps.
void set_clock(base::Clock* clock) {
clock_ = clock;
}
protected: protected:
const MuxerOptions& options() const { return options_; } const MuxerOptions& options() const { return options_; }
EncryptorSource* encryptor_source() { return encryptor_source_; } EncryptorSource* encryptor_source() { return encryptor_source_; }
double clear_lead_in_seconds() const { return clear_lead_in_seconds_; } double clear_lead_in_seconds() const { return clear_lead_in_seconds_; }
event::MuxerListener* muxer_listener() { return muxer_listener_; } event::MuxerListener* muxer_listener() { return muxer_listener_; }
base::Clock* clock() { return clock_; }
private: private:
MuxerOptions options_; MuxerOptions options_;
@ -71,6 +85,8 @@ class Muxer {
double clear_lead_in_seconds_; double clear_lead_in_seconds_;
event::MuxerListener* muxer_listener_; event::MuxerListener* muxer_listener_;
// An external injected clock, can be NULL.
base::Clock* clock_;
DISALLOW_COPY_AND_ASSIGN(Muxer); DISALLOW_COPY_AND_ASSIGN(Muxer);
}; };

View File

@ -6,6 +6,7 @@
#include "media/mp4/mp4_muxer.h" #include "media/mp4/mp4_muxer.h"
#include "base/time/clock.h"
#include "base/time/time.h" #include "base/time/time.h"
#include "media/base/aes_encryptor.h" #include "media/base/aes_encryptor.h"
#include "media/base/audio_stream_info.h" #include "media/base/audio_stream_info.h"
@ -25,17 +26,12 @@ namespace {
// The version of cenc implemented here. CENC 4. // The version of cenc implemented here. CENC 4.
const int kCencSchemeVersion = 0x00010000; const int kCencSchemeVersion = 0x00010000;
// Get time in seconds since midnight, Jan. 1, 1904, in UTC Time.
uint64 IsoTimeNow() {
// Time in seconds from Jan. 1, 1904 to epoch time, i.e. Jan. 1, 1970.
const uint64 kIsomTimeOffset = 2082844800l;
return kIsomTimeOffset + base::Time::Now().ToDoubleT();
}
// Sets the range start and end value from offset and size. // Sets the range start and end value from offset and size.
// |start| and |end| are for byte-range-spec specified in RFC2616. // |start| and |end| are for byte-range-spec specified in RFC2616.
void SetStartAndEndFromOffsetAndSize(size_t offset, size_t size, void SetStartAndEndFromOffsetAndSize(size_t offset,
uint32* start, uint32* end) { size_t size,
uint32* start,
uint32* end) {
DCHECK(start && end); DCHECK(start && end);
*start = static_cast<uint32>(offset); *start = static_cast<uint32>(offset);
// Note that ranges are inclusive. So we need - 1. // Note that ranges are inclusive. So we need - 1.
@ -103,8 +99,7 @@ Status MP4Muxer::Initialize() {
} }
if (options().single_segment) { if (options().single_segment) {
segmenter_.reset( segmenter_.reset(new MP4VODSegmenter(options(), ftyp.Pass(), moov.Pass()));
new MP4VODSegmenter(options(), ftyp.Pass(), moov.Pass()));
} else { } else {
segmenter_.reset( segmenter_.reset(
new MP4GeneralSegmenter(options(), ftyp.Pass(), moov.Pass())); new MP4GeneralSegmenter(options(), ftyp.Pass(), moov.Pass()));
@ -255,8 +250,7 @@ bool MP4Muxer::GetInitRangeStartAndEnd(uint32* start, uint32* end) {
DCHECK(start && end); DCHECK(start && end);
size_t range_offset = 0; size_t range_offset = 0;
size_t range_size = 0; size_t range_size = 0;
const bool has_range = const bool has_range = segmenter_->GetInitRange(&range_offset, &range_size);
segmenter_->GetInitRange(&range_offset, &range_size);
if (!has_range) if (!has_range)
return false; return false;
@ -269,8 +263,7 @@ bool MP4Muxer::GetIndexRangeStartAndEnd(uint32* start, uint32* end) {
DCHECK(start && end); DCHECK(start && end);
size_t range_offset = 0; size_t range_offset = 0;
size_t range_size = 0; size_t range_size = 0;
const bool has_range = const bool has_range = segmenter_->GetIndexRange(&range_offset, &range_size);
segmenter_->GetIndexRange(&range_offset, &range_size);
if (!has_range) if (!has_range)
return false; return false;
@ -329,5 +322,12 @@ void MP4Muxer::FireOnMediaEndEvent() {
IsEncryptionRequired()); IsEncryptionRequired());
} }
uint64 MP4Muxer::IsoTimeNow() {
// Time in seconds from Jan. 1, 1904 to epoch time, i.e. Jan. 1, 1970.
const uint64 kIsomTimeOffset = 2082844800l;
return kIsomTimeOffset +
(clock() ? clock()->Now() : base::Time::Now()).ToDoubleT();
}
} // namespace mp4 } // namespace mp4
} // namespace media } // namespace media

View File

@ -76,6 +76,9 @@ class MP4Muxer : public Muxer {
void FireOnMediaStartEvent(); void FireOnMediaStartEvent();
void FireOnMediaEndEvent(); void FireOnMediaEndEvent();
// Get time in seconds since midnight, Jan. 1, 1904, in UTC Time.
uint64 IsoTimeNow();
scoped_ptr<MP4Segmenter> segmenter_; scoped_ptr<MP4Segmenter> segmenter_;
DISALLOW_COPY_AND_ASSIGN(MP4Muxer); DISALLOW_COPY_AND_ASSIGN(MP4Muxer);

View File

@ -6,6 +6,8 @@
#include "base/file_util.h" #include "base/file_util.h"
#include "base/strings/string_number_conversions.h" #include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/time/clock.h"
#include "media/base/demuxer.h" #include "media/base/demuxer.h"
#include "media/base/fixed_encryptor_source.h" #include "media/base/fixed_encryptor_source.h"
#include "media/base/media_stream.h" #include "media/base/media_stream.h"
@ -18,7 +20,9 @@
using ::testing::ValuesIn; using ::testing::ValuesIn;
namespace media {
namespace { namespace {
const char* kMediaFiles[] = {"bear-1280x720.mp4", "bear-1280x720-av_frag.mp4"}; const char* kMediaFiles[] = {"bear-1280x720.mp4", "bear-1280x720-av_frag.mp4"};
// Muxer options. // Muxer options.
@ -27,12 +31,20 @@ const double kFragmentDurationInSecodns = 0.1;
const bool kSegmentSapAligned = true; const bool kSegmentSapAligned = true;
const bool kFragmentSapAligned = true; const bool kFragmentSapAligned = true;
const int kNumSubsegmentsPerSidx = 2; const int kNumSubsegmentsPerSidx = 2;
const char kOutputFileName[] = "output_file";
const char kOutputFileName2[] = "output_file2"; const char kOutputVideo[] = "output_video";
const char kOutputVideo2[] = "output_video_2";
const char kOutputAudio[] = "output_audio";
const char kOutputAudio2[] = "output_audio_2";
const char kOutputNone[] = "";
const char kSegmentTemplate[] = "template$Number$.m4s"; const char kSegmentTemplate[] = "template$Number$.m4s";
const char kSegmentTemplateOutputFile[] = "template1.m4s"; const char kSegmentTemplateOutputPattern[] = "template%d.m4s";
const char kTempFileName[] = "temp_file";
const char kTempFileName2[] = "temp_file2"; const bool kSingleSegment = true;
const bool kMultipleSegments = false;
const bool kEnableEncryption = true;
const bool kDisableEncryption = false;
// Encryption constants. // Encryption constants.
const char kKeyIdHex[] = "e5007e6e9dcd5ac095202ed3758382cd"; const char kKeyIdHex[] = "e5007e6e9dcd5ac095202ed3758382cd";
@ -43,149 +55,250 @@ const char kPsshHex[] =
"434f4e54454e545f49445f312a025344"; "434f4e54454e545f49445f312a025344";
const double kClearLeadInSeconds = 1.5; const double kClearLeadInSeconds = 1.5;
MediaStream* FindFirstStreamOfType(const std::vector<MediaStream*>& streams,
StreamType stream_type) {
typedef std::vector<MediaStream*>::const_iterator StreamIterator;
for (StreamIterator it = streams.begin(); it != streams.end(); ++it) {
if ((*it)->info()->stream_type() == stream_type)
return *it;
}
return NULL;
}
MediaStream* FindFirstVideoStream(const std::vector<MediaStream*>& streams) {
return FindFirstStreamOfType(streams, kStreamVideo);
}
MediaStream* FindFirstAudioStream(const std::vector<MediaStream*>& streams) {
return FindFirstStreamOfType(streams, kStreamAudio);
}
} // namespace } // namespace
namespace media { class FakeClock : public base::Clock {
class PackagerTest : public ::testing::TestWithParam<const char*> {
public: public:
// Fake the clock to return NULL time.
virtual base::Time Now() OVERRIDE { return base::Time(); }
};
class PackagerTestBasic : public ::testing::TestWithParam<const char*> {
public:
PackagerTestBasic() : decryptor_source_(NULL) {}
virtual void SetUp() OVERRIDE { virtual void SetUp() OVERRIDE {
// Create a test directory for testing, will be deleted after test. // Create a test directory for testing, will be deleted after test.
ASSERT_TRUE(base::CreateNewTempDirectory("packager_", &test_directory_)); ASSERT_TRUE(base::CreateNewTempDirectory("packager_", &test_directory_));
options_.segment_duration = kSegmentDurationInSeconds; // Copy the input to test directory for easy reference.
options_.fragment_duration = kFragmentDurationInSecodns; ASSERT_TRUE(base::CopyFile(GetTestDataFilePath(GetParam()),
options_.segment_sap_aligned = kSegmentSapAligned; test_directory_.AppendASCII(GetParam())));
options_.fragment_sap_aligned = kFragmentSapAligned;
options_.num_subsegments_per_sidx = kNumSubsegmentsPerSidx;
options_.output_file_name =
test_directory_.AppendASCII(kOutputFileName).value();
options_.segment_template =
test_directory_.AppendASCII(kSegmentTemplate).value();
options_.temp_file_name =
test_directory_.AppendASCII(kTempFileName).value();
} }
virtual void TearDown() OVERRIDE { base::DeleteFile(test_directory_, true); } virtual void TearDown() OVERRIDE { base::DeleteFile(test_directory_, true); }
void Remux(const std::string& input_file, Muxer* muxer) { std::string GetFullPath(const std::string& file_name);
DCHECK(muxer); // Check if |file1| and |file2| are the same.
bool ContentsEqual(const std::string& file1, const std::string file2);
Demuxer demuxer(input_file, NULL); MuxerOptions SetupOptions(const std::string& output, bool single_segment);
ASSERT_OK(demuxer.Initialize()); void Remux(const std::string& input,
ASSERT_LE(1, demuxer.streams().size()); const std::string& video_output,
const std::string& audio_output,
VLOG(1) << "Num Streams: " << demuxer.streams().size(); bool single_segment,
for (size_t i = 0; i < demuxer.streams().size(); ++i) { bool enable_encryption);
VLOG(1) << "Streams " << i << ": " << demuxer.streams()[i]->ToString();
}
ASSERT_OK(muxer->AddStream(demuxer.streams()[0]));
ASSERT_OK(muxer->Initialize());
// Start remuxing process.
ASSERT_OK(demuxer.Run());
ASSERT_OK(muxer->Finalize());
}
// Check |input_file| is a valid media file and can be initialized by Demuxer.
void CheckMediaFile(const std::string input_file) {
Demuxer demuxer(input_file, NULL);
ASSERT_OK(demuxer.Initialize());
}
protected: protected:
base::FilePath test_directory_; base::FilePath test_directory_;
MuxerOptions options_; DecryptorSource* decryptor_source_;
FakeClock fake_clock_;
}; };
TEST_P(PackagerTest, MP4MuxerSingleSegmentUnencrypted) { std::string PackagerTestBasic::GetFullPath(const std::string& file_name) {
options_.single_segment = true; return test_directory_.AppendASCII(file_name).value();
const std::string input_media_file = GetTestDataFilePath(GetParam()).value();
scoped_ptr<Muxer> muxer(new mp4::MP4Muxer(options_));
ASSERT_NO_FATAL_FAILURE(Remux(input_media_file, muxer.get()));
// Take the muxer output and feed into muxer again. The new muxer output
// should contain the same contents as the previous muxer output.
const std::string new_input_media_file = options_.output_file_name;
options_.output_file_name =
test_directory_.AppendASCII(kOutputFileName2).value();
muxer.reset(new mp4::MP4Muxer(options_));
ASSERT_NO_FATAL_FAILURE(Remux(new_input_media_file, muxer.get()));
// TODO(kqyang): This comparison might be flaky due to timestamp difference.
// Compare data beyond moov box only?
EXPECT_TRUE(base::ContentsEqual(base::FilePath(new_input_media_file),
base::FilePath(options_.output_file_name)));
} }
TEST_P(PackagerTest, MP4MuxerSingleSegmentUnencryptedSeparateAudioVideo) { bool PackagerTestBasic::ContentsEqual(const std::string& file1,
options_.single_segment = true; const std::string file2) {
return base::ContentsEqual(test_directory_.AppendASCII(file1),
test_directory_.AppendASCII(file2));
}
const std::string input_media_file = GetTestDataFilePath(GetParam()).value(); MuxerOptions PackagerTestBasic::SetupOptions(const std::string& output,
bool single_segment) {
MuxerOptions options;
options.single_segment = single_segment;
Demuxer demuxer(input_media_file, NULL); options.segment_duration = kSegmentDurationInSeconds;
options.fragment_duration = kFragmentDurationInSecodns;
options.segment_sap_aligned = kSegmentSapAligned;
options.fragment_sap_aligned = kFragmentSapAligned;
options.num_subsegments_per_sidx = kNumSubsegmentsPerSidx;
options.output_file_name = GetFullPath(output);
options.segment_template = GetFullPath(kSegmentTemplate);
options.temp_file_name = GetFullPath(output + ".temp");
return options;
}
void PackagerTestBasic::Remux(const std::string& input,
const std::string& video_output,
const std::string& audio_output,
bool single_segment,
bool enable_encryption) {
CHECK(!video_output.empty() || !audio_output.empty());
Demuxer demuxer(GetFullPath(input), decryptor_source_);
ASSERT_OK(demuxer.Initialize()); ASSERT_OK(demuxer.Initialize());
ASSERT_EQ(2, demuxer.streams().size());
// Create and initialize the first muxer.
scoped_ptr<Muxer> muxer(new mp4::MP4Muxer(options_));
ASSERT_OK(muxer->AddStream(demuxer.streams()[0]));
ASSERT_OK(muxer->Initialize());
// Create and initialize the second muxer.
MuxerOptions options2 = options_;
options2.output_file_name =
test_directory_.AppendASCII(kOutputFileName2).value();
options2.temp_file_name =
test_directory_.AppendASCII(kTempFileName2).value();
scoped_ptr<Muxer> muxer2(new mp4::MP4Muxer(options2));
ASSERT_OK(muxer2->AddStream(demuxer.streams()[1]));
ASSERT_OK(muxer2->Initialize());
// Start remuxing process.
ASSERT_OK(demuxer.Run());
ASSERT_OK(muxer->Finalize());
ASSERT_OK(muxer2->Finalize());
// Check output file is valid.
// TODO(kqyang): Compare the output with a known good output.
ASSERT_NO_FATAL_FAILURE(CheckMediaFile(options_.output_file_name));
ASSERT_NO_FATAL_FAILURE(CheckMediaFile(options2.output_file_name));
}
TEST_P(PackagerTest, MP4MuxerSingleSegmentEncrypted) {
options_.single_segment = true;
FixedEncryptorSource encryptor_source(kKeyIdHex, kKeyHex, kPsshHex); FixedEncryptorSource encryptor_source(kKeyIdHex, kKeyHex, kPsshHex);
ASSERT_OK(encryptor_source.Initialize()); ASSERT_OK(encryptor_source.Initialize());
const std::string input_media_file = GetTestDataFilePath(GetParam()).value(); scoped_ptr<Muxer> muxer_video;
scoped_ptr<Muxer> muxer(new mp4::MP4Muxer(options_)); if (!video_output.empty()) {
muxer->SetEncryptorSource(&encryptor_source, kClearLeadInSeconds); muxer_video.reset(
ASSERT_NO_FATAL_FAILURE(Remux(input_media_file, muxer.get())); new mp4::MP4Muxer(SetupOptions(video_output, single_segment)));
muxer_video->set_clock(&fake_clock_);
ASSERT_OK(muxer_video->AddStream(FindFirstVideoStream(demuxer.streams())));
if (enable_encryption)
muxer_video->SetEncryptorSource(&encryptor_source, kClearLeadInSeconds);
ASSERT_OK(muxer_video->Initialize());
}
scoped_ptr<Muxer> muxer_audio;
if (!audio_output.empty()) {
muxer_audio.reset(
new mp4::MP4Muxer(SetupOptions(audio_output, single_segment)));
muxer_audio->set_clock(&fake_clock_);
ASSERT_OK(muxer_audio->AddStream(FindFirstAudioStream(demuxer.streams())));
if (enable_encryption)
muxer_video->SetEncryptorSource(&encryptor_source, kClearLeadInSeconds);
ASSERT_OK(muxer_audio->Initialize());
}
// Start remuxing process.
ASSERT_OK(demuxer.Run());
if (muxer_video)
ASSERT_OK(muxer_video->Finalize());
if (muxer_audio)
ASSERT_OK(muxer_audio->Finalize());
}
TEST_P(PackagerTestBasic, MP4MuxerSingleSegmentUnencrypted) {
ASSERT_NO_FATAL_FAILURE(Remux(GetParam(),
kOutputVideo,
kOutputNone,
kSingleSegment,
kDisableEncryption));
}
TEST_P(PackagerTestBasic, MP4MuxerSingleSegmentEncrypted) {
ASSERT_NO_FATAL_FAILURE(Remux(GetParam(),
kOutputVideo,
kOutputNone,
kSingleSegment,
kEnableEncryption));
// Expect the output to be encrypted. // Expect the output to be encrypted.
Demuxer demuxer(options_.output_file_name, NULL); Demuxer demuxer(GetFullPath(kOutputVideo), decryptor_source_);
ASSERT_OK(demuxer.Initialize()); ASSERT_OK(demuxer.Initialize());
ASSERT_EQ(1, demuxer.streams().size()); ASSERT_EQ(1, demuxer.streams().size());
EXPECT_TRUE(demuxer.streams()[0]->info()->is_encrypted()); EXPECT_TRUE(demuxer.streams()[0]->info()->is_encrypted());
} }
TEST_P(PackagerTest, MP4MuxerMultipleSegmentsUnencrypted) { class PackagerTest : public PackagerTestBasic {
options_.single_segment = false; public:
virtual void SetUp() OVERRIDE {
PackagerTestBasic::SetUp();
const std::string input_media_file = GetTestDataFilePath(GetParam()).value(); ASSERT_NO_FATAL_FAILURE(Remux(GetParam(),
scoped_ptr<Muxer> muxer(new mp4::MP4Muxer(options_)); kOutputVideo,
ASSERT_NO_FATAL_FAILURE(Remux(input_media_file, muxer.get())); kOutputNone,
kSingleSegment,
kDisableEncryption));
EXPECT_TRUE(base::PathExists( ASSERT_NO_FATAL_FAILURE(Remux(GetParam(),
test_directory_.AppendASCII(kSegmentTemplateOutputFile))); kOutputNone,
kOutputAudio,
kSingleSegment,
kDisableEncryption));
}
};
TEST_P(PackagerTest, MP4MuxerSingleSegmentUnencryptedAgain) {
// Take the muxer output and feed into muxer again. The new muxer output
// should contain the same contents as the previous muxer output.
ASSERT_NO_FATAL_FAILURE(Remux(kOutputVideo,
kOutputVideo2,
kOutputNone,
kSingleSegment,
kDisableEncryption));
EXPECT_TRUE(ContentsEqual(kOutputVideo, kOutputVideo2));
} }
TEST_P(PackagerTest, MP4MuxerSingleSegmentUnencryptedSeparateAudioVideo) {
ASSERT_NO_FATAL_FAILURE(Remux(GetParam(),
kOutputVideo2,
kOutputAudio2,
kSingleSegment,
kDisableEncryption));
// Compare the output with single muxer output. They should match.
EXPECT_TRUE(ContentsEqual(kOutputVideo, kOutputVideo2));
EXPECT_TRUE(ContentsEqual(kOutputAudio, kOutputAudio2));
}
TEST_P(PackagerTest, MP4MuxerMultipleSegmentsUnencrypted) {
ASSERT_NO_FATAL_FAILURE(Remux(GetParam(),
kOutputVideo2,
kOutputNone,
kMultipleSegments,
kDisableEncryption));
// Find and concatenates the segments.
const std::string kOutputVideoSegmentsCombined =
std::string(kOutputVideo) + "_combined";
base::FilePath output_path =
test_directory_.AppendASCII(kOutputVideoSegmentsCombined);
ASSERT_TRUE(
base::CopyFile(test_directory_.AppendASCII(kOutputVideo2), output_path));
const int kStartSegmentIndex = 1; // start from one.
int segment_index = kStartSegmentIndex;
while (true) {
base::FilePath segment_path = test_directory_.AppendASCII(
base::StringPrintf(kSegmentTemplateOutputPattern, segment_index));
if (!base::PathExists(segment_path))
break;
std::string segment_content;
ASSERT_TRUE(file_util::ReadFileToString(segment_path, &segment_content));
int bytes_written = file_util::AppendToFile(
output_path, segment_content.data(), segment_content.size());
ASSERT_EQ(segment_content.size(), bytes_written);
++segment_index;
}
// We should have at least one segment.
ASSERT_LT(kStartSegmentIndex, segment_index);
// Feed the combined file into muxer again. The new muxer output should be
// the same as by just feeding the input to muxer.
ASSERT_NO_FATAL_FAILURE(Remux(kOutputVideoSegmentsCombined,
kOutputVideo2,
kOutputNone,
kSingleSegment,
kDisableEncryption));
EXPECT_TRUE(ContentsEqual(kOutputVideo, kOutputVideo2));
}
INSTANTIATE_TEST_CASE_P(PackagerE2ETest,
PackagerTestBasic,
ValuesIn(kMediaFiles));
INSTANTIATE_TEST_CASE_P(PackagerE2ETest, PackagerTest, ValuesIn(kMediaFiles)); INSTANTIATE_TEST_CASE_P(PackagerE2ETest, PackagerTest, ValuesIn(kMediaFiles));
} // namespace media } // namespace media