Added unit tests for WebM Segmenters.

* Changed Segmenters to accept StreamInfo rather than MediaStream
  to help in testing.
* Changed MemoryFile behavior to mirror local file:
  * Read non-existent file is an error.
  * Write deletes any existing file.
* Fixed a bug in SingleSegmentSegmenter.

Change-Id: I339e35597ca4661b7a26c6fdbbfa2f9f511c7da0
This commit is contained in:
Jacob Trimble 2015-12-21 16:33:55 -08:00 committed by Gerrit Code Review
parent b3e85ff810
commit 7b52f0a3ed
17 changed files with 869 additions and 41 deletions

View File

@ -61,7 +61,7 @@ File* CreateUdpFile(const char* file_name, const char* mode) {
}
File* CreateMemoryFile(const char* file_name, const char* mode) {
return new MemoryFile(file_name);
return new MemoryFile(file_name, mode);
}
bool DeleteMemoryFile(const char* file_name) {

View File

@ -0,0 +1,29 @@
// Copyright 2015 Google Inc. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
#ifndef MEDIA_FILE_FILE_TEST_UTIL_H_
#define MEDIA_FILE_FILE_TEST_UTIL_H_
#include <string>
#include "packager/media/file/file.h"
namespace edash_packager {
namespace media {
#define ASSERT_FILE_EQ(file_name, array) \
do { \
std::string temp_data; \
ASSERT_TRUE(File::ReadFileToString((file_name), &temp_data)); \
const char* array_ptr = reinterpret_cast<const char*>(array); \
ASSERT_EQ(std::string(array_ptr, arraysize(array)), temp_data); \
} while (false)
} // namespace media
} // namespace edash_packager
#endif // MEDIA_FILE_FILE_TEST_UTIL_H_

View File

@ -29,6 +29,10 @@ class FileSystem {
return g_file_system_.get();
}
bool Exists(const std::string& file_name) const {
return files_.find(file_name) != files_.end();
}
std::vector<uint8_t>* GetFile(const std::string& file_name) {
return &files_[file_name];
}
@ -50,10 +54,8 @@ scoped_ptr<FileSystem> FileSystem::g_file_system_;
} // namespace
MemoryFile::MemoryFile(const std::string& file_name)
: File(file_name),
file_(FileSystem::Instance()->GetFile(file_name)),
position_(0) {}
MemoryFile::MemoryFile(const std::string& file_name, const std::string& mode)
: File(file_name), mode_(mode), file_(NULL), position_(0) {}
MemoryFile::~MemoryFile() {}
@ -86,6 +88,7 @@ int64_t MemoryFile::Write(const void* buffer, uint64_t length) {
}
int64_t MemoryFile::Size() {
DCHECK(file_);
return file_->size();
}
@ -107,6 +110,19 @@ bool MemoryFile::Tell(uint64_t* position) {
}
bool MemoryFile::Open() {
FileSystem* file_system = FileSystem::Instance();
if (mode_ == "r") {
if (!file_system->Exists(file_name()))
return false;
} else if (mode_ == "w") {
file_system->Delete(file_name());
} else {
NOTIMPLEMENTED() << "File mode " << mode_ << " not supported by MemoryFile";
return false;
}
file_ = file_system->GetFile(file_name());
DCHECK(file_);
position_ = 0;
return true;
}

View File

@ -21,7 +21,7 @@ namespace media {
/// testing, since this does not support larger files.
class MemoryFile : public File {
public:
MemoryFile(const std::string& file_name);
MemoryFile(const std::string& file_name, const std::string& mode);
/// @name File implementation overrides.
/// @{
@ -47,6 +47,7 @@ class MemoryFile : public File {
bool Open() override;
private:
std::string mode_;
std::vector<uint8_t>* file_;
uint64_t position_;

View File

@ -27,12 +27,14 @@ class MemoryFileTest : public testing::Test {
TEST_F(MemoryFileTest, ModifiesSameFile) {
scoped_ptr<File, FileCloser> writer(File::Open("memory://file1", "w"));
ASSERT_TRUE(writer);
ASSERT_EQ(kWriteBufferSize, writer->Write(kWriteBuffer, kWriteBufferSize));
// Since File::Open should not create a ThreadedIoFile so there should be
// no cache.
scoped_ptr<File, FileCloser> reader(File::Open("memory://file1", "r"));
ASSERT_TRUE(reader);
uint8_t read_buffer[kWriteBufferSize];
ASSERT_EQ(kWriteBufferSize, reader->Read(read_buffer, kWriteBufferSize));
@ -40,15 +42,19 @@ TEST_F(MemoryFileTest, ModifiesSameFile) {
}
TEST_F(MemoryFileTest, SupportsDifferentFiles) {
scoped_ptr<MemoryFile, FileCloser> writer(new MemoryFile("memory://file1"));
scoped_ptr<MemoryFile, FileCloser> reader(new MemoryFile("memory://file2"));
scoped_ptr<File, FileCloser> writer(File::Open("memory://file1", "w"));
scoped_ptr<File, FileCloser> reader(File::Open("memory://file2", "w"));
ASSERT_TRUE(writer);
ASSERT_TRUE(reader);
ASSERT_EQ(kWriteBufferSize, writer->Write(kWriteBuffer, kWriteBufferSize));
ASSERT_EQ(0, reader->Size());
}
TEST_F(MemoryFileTest, SeekAndTell) {
scoped_ptr<MemoryFile, FileCloser> file(new MemoryFile("memory://file1"));
scoped_ptr<File, FileCloser> file(File::Open("memory://file1", "w"));
ASSERT_TRUE(file);
ASSERT_EQ(kWriteBufferSize, file->Write(kWriteBuffer, kWriteBufferSize));
ASSERT_TRUE(file->Seek(0));
@ -61,7 +67,9 @@ TEST_F(MemoryFileTest, SeekAndTell) {
}
TEST_F(MemoryFileTest, EndOfFile) {
scoped_ptr<MemoryFile, FileCloser> file(new MemoryFile("memory://file1"));
scoped_ptr<File, FileCloser> file(File::Open("memory://file1", "w"));
ASSERT_TRUE(file);
ASSERT_EQ(kWriteBufferSize, file->Write(kWriteBuffer, kWriteBufferSize));
ASSERT_TRUE(file->Seek(0));
@ -75,7 +83,8 @@ TEST_F(MemoryFileTest, EndOfFile) {
}
TEST_F(MemoryFileTest, ExtendsSize) {
scoped_ptr<MemoryFile, FileCloser> file(new MemoryFile("memory://file1"));
scoped_ptr<File, FileCloser> file(File::Open("memory://file1", "w"));
ASSERT_TRUE(file);
ASSERT_EQ(kWriteBufferSize, file->Write(kWriteBuffer, kWriteBufferSize));
ASSERT_EQ(kWriteBufferSize, file->Size());
@ -87,5 +96,20 @@ TEST_F(MemoryFileTest, ExtendsSize) {
EXPECT_EQ(2 * kWriteBufferSize, static_cast<int64_t>(size));
}
TEST_F(MemoryFileTest, ReadMissingFileFails) {
scoped_ptr<File, FileCloser> file(File::Open("memory://file1", "r"));
EXPECT_FALSE(file);
}
TEST_F(MemoryFileTest, WriteExistingFileDeletes) {
scoped_ptr<File, FileCloser> file1(File::Open("memory://file1", "w"));
ASSERT_TRUE(file1);
ASSERT_EQ(kWriteBufferSize, file1->Write(kWriteBuffer, kWriteBufferSize));
scoped_ptr<File, FileCloser> file2(File::Open("memory://file1", "w"));
ASSERT_TRUE(file2);
EXPECT_EQ(0, file2->Size());
}
} // namespace media
} // namespace edash_packager

View File

@ -49,7 +49,7 @@ Status MultiSegmentSegmenter::FinalizeSegment() {
const uint64_t start_webm_timecode = cluster()->timecode();
const uint64_t start_timescale = FromWebMTimecode(start_webm_timecode);
const uint64_t length = static_cast<uint64_t>(
cluster_length_sec() * stream()->info()->time_scale());
cluster_length_sec() * info()->time_scale());
muxer_listener()->OnNewSegment(start_timescale, length, size);
}

View File

@ -0,0 +1,234 @@
// Copyright (c) 2015 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 "packager/media/formats/webm/multi_segment_segmenter.h"
#include <gtest/gtest.h>
#include "packager/base/memory/scoped_ptr.h"
#include "packager/media/formats/webm/segmenter_test_base.h"
namespace edash_packager {
namespace media {
namespace {
const uint64_t kDuration = 1000;
const uint8_t kBasicSupportDataInit[] = {
// ID: EBML Header, Size: 31
0x1a, 0x45, 0xdf, 0xa3, 0x9f,
// EBMLVersion: 1
0x42, 0x86, 0x81, 0x01,
// EBMLReadVersion: 1
0x42, 0xf7, 0x81, 0x01,
// EBMLMaxIDLength: 4
0x42, 0xf2, 0x81, 0x04,
// EBMLMaxSizeLength: 8
0x42, 0xf3, 0x81, 0x08,
// DocType: 'webm'
0x42, 0x82, 0x84, 0x77, 0x65, 0x62, 0x6d,
// DocTypeVersion: 2
0x42, 0x87, 0x81, 0x02,
// DocTypeReadVersion: 2
0x42, 0x85, 0x81, 0x02,
// ID: Segment, Size: -1
0x18, 0x53, 0x80, 0x67, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
// ID: Void, Size: 87
0xec, 0xd7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00,
// ID: Info, size: 50
0x15, 0x49, 0xa9, 0x66, 0xb2,
// TimecodeScale: 1000000
0x2a, 0xd7, 0xb1, 0x83, 0x0f, 0x42, 0x40,
// Duration: float(5000)
0x44, 0x89, 0x84, 0x3f, 0x80, 0x00, 0x00,
// MuxingApp: 'libwebm-0.2.1.0'
0x4d, 0x80, 0x8f, 0x6c, 0x69, 0x62, 0x77, 0x65, 0x62, 0x6d, 0x2d, 0x30,
0x2e, 0x32, 0x2e, 0x31, 0x2e, 0x30,
// WritingApp: 'libwebm-0.2.1.0'
0x57, 0x41, 0x8f, 0x6c, 0x69, 0x62, 0x77, 0x65, 0x62, 0x6d, 0x2d, 0x30,
0x2e, 0x32, 0x2e, 0x31, 0x2e, 0x30,
// ID: Tracks, size: 41
0x16, 0x54, 0xae, 0x6b, 0xa9,
// ID: Track, size: 39
0xae, 0xa7,
// TrackNumber: 1
0xd7, 0x81, 0x01,
// TrackUID: 1
0x73, 0xc5, 0x81, 0x01,
// TrackType: 1
0x83, 0x81, 0x01,
// CodecID: 'V_VP8'
0x86, 0x85, 0x56, 0x5f, 0x56, 0x50, 0x38,
// Language: 'en'
0x22, 0xb5, 0x9c, 0x82, 0x65, 0x6e,
// ID: Video, Size: 14
0xe0, 0x8e,
// PixelWidth: 100
0xb0, 0x81, 0x64,
// PixelHeight: 100
0xba, 0x81, 0x64,
// DisplayWidth: 100
0x54, 0xb0, 0x81, 0x64,
// DisplayHeight: 100
0x54, 0xba, 0x81, 0x64
};
const uint8_t kBasicSupportDataSegment[] = {
// ID: Cluster, size: 58
0x1f, 0x43, 0xb6, 0x75, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3a,
// Timecode: 0
0xe7, 0x81, 0x00,
// ID: SimpleBlock, Size: 9
0xa3, 0x89, 0x81, 0x00, 0x00, 0x80, 0xde, 0xad, 0xbe, 0xef, 0x00,
// ID: SimpleBlock, Size: 9
0xa3, 0x89, 0x81, 0x03, 0xe8, 0x80, 0xde, 0xad, 0xbe, 0xef, 0x00,
// ID: SimpleBlock, Size: 9
0xa3, 0x89, 0x81, 0x07, 0xd0, 0x80, 0xde, 0xad, 0xbe, 0xef, 0x00,
// ID: SimpleBlock, Size: 9
0xa3, 0x89, 0x81, 0x0b, 0xb8, 0x80, 0xde, 0xad, 0xbe, 0xef, 0x00,
// ID: SimpleBlock, Size: 9
0xa3, 0x89, 0x81, 0x0f, 0xa0, 0x80, 0xde, 0xad, 0xbe, 0xef, 0x00
};
} // namespace
class MultiSegmentSegmenterTest : public SegmentTestBase {
public:
MultiSegmentSegmenterTest() : info_(CreateVideoStreamInfo()) {}
protected:
void InitializeSegmenter(const MuxerOptions& options) {
ASSERT_NO_FATAL_FAILURE(
CreateAndInitializeSegmenter<webm::MultiSegmentSegmenter>(
options, info_.get(), &segmenter_));
}
scoped_refptr<StreamInfo> info_;
scoped_ptr<webm::Segmenter> segmenter_;
};
TEST_F(MultiSegmentSegmenterTest, BasicSupport) {
MuxerOptions options = CreateMuxerOptions();
ASSERT_NO_FATAL_FAILURE(InitializeSegmenter(options));
// Write the samples to the Segmenter.
for (int i = 0; i < 5; i++) {
scoped_refptr<MediaSample> sample = CreateSample(true, kDuration);
ASSERT_OK(segmenter_->AddSample(sample));
}
ASSERT_OK(segmenter_->Finalize());
// Verify the resulting data.
ASSERT_FILE_EQ(OutputFileName().c_str(), kBasicSupportDataInit);
ASSERT_FILE_EQ(TemplateFileName(0).c_str(), kBasicSupportDataSegment);
// There is no second segment.
EXPECT_FALSE(File::Open(TemplateFileName(1).c_str(), "r"));
}
TEST_F(MultiSegmentSegmenterTest, SplitsFilesOnSegmentDuration) {
MuxerOptions options = CreateMuxerOptions();
options.segment_duration = 5; // seconds
ASSERT_NO_FATAL_FAILURE(InitializeSegmenter(options));
// Write the samples to the Segmenter.
for (int i = 0; i < 8; i++) {
scoped_refptr<MediaSample> sample = CreateSample(true, kDuration);
ASSERT_OK(segmenter_->AddSample(sample));
}
ASSERT_OK(segmenter_->Finalize());
// Verify the resulting data.
ClusterParser parser;
ASSERT_NO_FATAL_FAILURE(parser.PopulateFromCluster(TemplateFileName(0)));
ASSERT_EQ(1, parser.cluster_count());
EXPECT_EQ(5, parser.GetFrameCountForCluster(0));
ASSERT_NO_FATAL_FAILURE(parser.PopulateFromCluster(TemplateFileName(1)));
ASSERT_EQ(1, parser.cluster_count());
EXPECT_EQ(3, parser.GetFrameCountForCluster(0));
EXPECT_FALSE(File::Open(TemplateFileName(2).c_str(), "r"));
}
TEST_F(MultiSegmentSegmenterTest, RespectsSegmentSAPAlign) {
MuxerOptions options = CreateMuxerOptions();
options.segment_duration = 3; // seconds
options.segment_sap_aligned = true;
ASSERT_NO_FATAL_FAILURE(InitializeSegmenter(options));
// Write the samples to the Segmenter.
for (int i = 0; i < 10; i++) {
scoped_refptr<MediaSample> sample = CreateSample(i == 6, kDuration);
ASSERT_OK(segmenter_->AddSample(sample));
}
ASSERT_OK(segmenter_->Finalize());
// Verify the resulting data.
ClusterParser parser;
ASSERT_NO_FATAL_FAILURE(parser.PopulateFromCluster(TemplateFileName(0)));
ASSERT_EQ(1, parser.cluster_count());
EXPECT_EQ(6, parser.GetFrameCountForCluster(0));
ASSERT_NO_FATAL_FAILURE(parser.PopulateFromCluster(TemplateFileName(1)));
ASSERT_EQ(1, parser.cluster_count());
EXPECT_EQ(4, parser.GetFrameCountForCluster(0));
EXPECT_FALSE(File::Open(TemplateFileName(2).c_str(), "r"));
}
TEST_F(MultiSegmentSegmenterTest, SplitsClustersOnFragmentDuration) {
MuxerOptions options = CreateMuxerOptions();
options.fragment_duration = 5; // seconds
ASSERT_NO_FATAL_FAILURE(InitializeSegmenter(options));
// Write the samples to the Segmenter.
for (int i = 0; i < 8; i++) {
scoped_refptr<MediaSample> sample = CreateSample(true, kDuration);
ASSERT_OK(segmenter_->AddSample(sample));
}
ASSERT_OK(segmenter_->Finalize());
// Verify the resulting data.
ClusterParser parser;
ASSERT_NO_FATAL_FAILURE(parser.PopulateFromCluster(TemplateFileName(0)));
ASSERT_EQ(2, parser.cluster_count());
EXPECT_EQ(5, parser.GetFrameCountForCluster(0));
EXPECT_EQ(3, parser.GetFrameCountForCluster(1));
EXPECT_FALSE(File::Open(TemplateFileName(1).c_str(), "r"));
}
TEST_F(MultiSegmentSegmenterTest, RespectsFragmentSAPAlign) {
MuxerOptions options = CreateMuxerOptions();
options.fragment_duration = 3; // seconds
options.fragment_sap_aligned = true;
ASSERT_NO_FATAL_FAILURE(InitializeSegmenter(options));
// Write the samples to the Segmenter.
for (int i = 0; i < 10; i++) {
scoped_refptr<MediaSample> sample = CreateSample(i == 6, kDuration);
ASSERT_OK(segmenter_->AddSample(sample));
}
ASSERT_OK(segmenter_->Finalize());
// Verify the resulting data.
ClusterParser parser;
ASSERT_NO_FATAL_FAILURE(parser.PopulateFromCluster(TemplateFileName(0)));
ASSERT_EQ(2, parser.cluster_count());
EXPECT_EQ(6, parser.GetFrameCountForCluster(0));
EXPECT_EQ(4, parser.GetFrameCountForCluster(1));
EXPECT_FALSE(File::Open(TemplateFileName(1).c_str(), "r"));
}
} // namespace media
} // namespace edash_packager

View File

@ -28,7 +28,7 @@ int64_t kSecondsToNs = 1000000000L;
Segmenter::Segmenter(const MuxerOptions& options)
: options_(options),
stream_(NULL),
info_(NULL),
muxer_listener_(NULL),
progress_listener_(NULL),
progress_target_(0),
@ -43,15 +43,15 @@ Segmenter::Segmenter(const MuxerOptions& options)
Segmenter::~Segmenter() {}
Status Segmenter::Initialize(scoped_ptr<MkvWriter> writer,
MediaStream* streams,
StreamInfo* info,
ProgressListener* progress_listener,
MuxerListener* muxer_listener,
KeySource* encryption_key_source) {
muxer_listener_ = muxer_listener;
stream_ = streams;
info_ = info;
// Use media duration as progress target.
progress_target_ = stream_->info()->duration();
progress_target_ = info_->duration();
progress_listener_ = progress_listener;
segment_info_.Init();
@ -64,18 +64,16 @@ Status Segmenter::Initialize(scoped_ptr<MkvWriter> writer,
// Create the track info.
Status status;
switch (stream_->info()->stream_type()) {
switch (info_->stream_type()) {
case kStreamVideo:
status = CreateVideoTrack(
static_cast<VideoStreamInfo*>(stream_->info().get()));
status = CreateVideoTrack(static_cast<VideoStreamInfo*>(info_));
break;
case kStreamAudio:
status = CreateAudioTrack(
static_cast<AudioStreamInfo*>(stream_->info().get()));
status = CreateAudioTrack(static_cast<AudioStreamInfo*>(info_));
break;
default:
NOTIMPLEMENTED() << "Not implemented for stream type: "
<< stream_->info()->stream_type();
<< info_->stream_type();
status = Status(error::UNIMPLEMENTED, "Not implemented for stream type");
}
if (!status.ok())
@ -118,14 +116,14 @@ Status Segmenter::AddSample(scoped_refptr<MediaSample> sample) {
return status;
const int64_t time_ns =
sample->pts() * kSecondsToNs / stream_->info()->time_scale();
sample->pts() * kSecondsToNs / info_->time_scale();
if (!cluster_->AddFrame(sample->data(), sample->data_size(), track_id_,
time_ns, sample->is_key_frame())) {
LOG(ERROR) << "Error adding sample to segment.";
return Status(error::FILE_FAILURE, "Error adding sample to segment.");
}
const double duration_sec =
static_cast<double>(sample->duration()) / stream_->info()->time_scale();
static_cast<double>(sample->duration()) / info_->time_scale();
cluster_length_sec_ += duration_sec;
segment_length_sec_ += duration_sec;
total_duration_ += sample->duration();
@ -141,14 +139,14 @@ float Segmenter::GetDuration() const {
uint64_t Segmenter::FromBMFFTimescale(uint64_t time_timescale) {
// Convert the time from BMFF time_code to WebM timecode scale.
const int64_t time_ns =
kSecondsToNs * time_timescale / stream_->info()->time_scale();
kSecondsToNs * time_timescale / info_->time_scale();
return time_ns / segment_info_.timecode_scale();
}
uint64_t Segmenter::FromWebMTimecode(uint64_t time_webm_timecode) {
// Convert the time to BMFF time_code from WebM timecode scale.
const int64_t time_ns = time_webm_timecode * segment_info_.timecode_scale();
return time_ns * stream_->info()->time_scale() / kSecondsToNs;
return time_ns * info_->time_scale() / kSecondsToNs;
}
Status Segmenter::WriteSegmentHeader(uint64_t file_size, MkvWriter* writer) {

View File

@ -22,7 +22,7 @@ struct MuxerOptions;
class AudioStreamInfo;
class KeySource;
class MediaSample;
class MediaStream;
class StreamInfo;
class MuxerListener;
class ProgressListener;
class StreamInfo;
@ -39,14 +39,14 @@ class Segmenter {
/// Calling other public methods of this class without this method returning
/// Status::OK results in an undefined behavior.
/// @param writer contains the output file (or init file in multi-segment).
/// @param streams contains the MediaStream to be segmented.
/// @param info The stream info for the stream being segmented.
/// @param muxer_listener receives muxer events. Can be NULL.
/// @param encryption_key_source points to the key source which contains
/// the encryption keys. It can be NULL to indicate that no encryption
/// is required.
/// @return OK on success, an error status otherwise.
Status Initialize(scoped_ptr<MkvWriter> writer,
MediaStream* streams,
StreamInfo* info,
ProgressListener* progress_listener,
MuxerListener* muxer_listener,
KeySource* encryption_key_source);
@ -94,7 +94,7 @@ class Segmenter {
mkvmuxer::Cluster* cluster() { return cluster_.get(); }
mkvmuxer::Cues* cues() { return &cues_; }
MuxerListener* muxer_listener() { return muxer_listener_; }
MediaStream* stream() { return stream_; }
StreamInfo* info() { return info_; }
SeekHead* seek_head() { return &seek_head_; }
int track_id() const { return track_id_; }
@ -128,7 +128,7 @@ class Segmenter {
mkvmuxer::SegmentInfo segment_info_;
mkvmuxer::Tracks tracks_;
MediaStream* stream_;
StreamInfo* info_;
MuxerListener* muxer_listener_;
ProgressListener* progress_listener_;
uint64_t progress_target_;

View File

@ -0,0 +1,185 @@
// Copyright 2015 Google Inc. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
#include "packager/media/formats/webm/segmenter_test_base.h"
#include "packager/media/base/muxer_util.h"
#include "packager/media/file/memory_file.h"
#include "packager/media/formats/webm/webm_constants.h"
namespace edash_packager {
namespace media {
namespace {
// The contents of a frame does not mater.
const uint8_t kTestMediaSampleData[] = {0xDE, 0xAD, 0xBE, 0xEF, 0x00};
const size_t kTestMediaSampleDataSize = sizeof(kTestMediaSampleData);
const int kTrackId = 1;
const uint32_t kTimeScale = 1000;
const uint64_t kDuration = 8000;
const VideoCodec kVideoCodec = kCodecVP8;
const std::string kCodecString = "vp8";
const std::string kLanguage = "en";
const uint16_t kWidth = 100;
const uint16_t kHeight = 100;
const uint16_t kPixelWidth = 100;
const uint16_t kPixelHeight = 100;
const int16_t kTrickPlayRate = 1;
const uint8_t kNaluLengthSize = 0;
} // namespace
SegmentTestBase::SegmentTestBase() {}
void SegmentTestBase::SetUp() {
output_file_name_ = "memory://output-file.webm";
segment_template_ = "memory://output-template-$Number$.webm";
cur_time_timescale_ = 0;
single_segment_ = true;
}
void SegmentTestBase::TearDown() {
MemoryFile::DeleteAll();
}
scoped_refptr<MediaSample> SegmentTestBase::CreateSample(bool is_key_frame,
uint64_t duration) {
scoped_refptr<MediaSample> sample = MediaSample::CopyFrom(
kTestMediaSampleData, kTestMediaSampleDataSize, is_key_frame);
sample->set_dts(cur_time_timescale_);
sample->set_pts(cur_time_timescale_);
sample->set_duration(duration);
cur_time_timescale_ += duration;
return sample.Pass();
}
MuxerOptions SegmentTestBase::CreateMuxerOptions() const {
MuxerOptions ret;
ret.single_segment = single_segment_;
ret.output_file_name = output_file_name_;
ret.segment_template = segment_template_;
ret.segment_duration = 30; // seconds
ret.fragment_duration = 30; // seconds
ret.segment_sap_aligned = false;
ret.fragment_sap_aligned = false;
// Use memory files for temp storage. Normally this would be a bad idea
// since it wouldn't support large files, but for tests the files are small.
ret.temp_dir = "memory://temp/";
return ret;
}
VideoStreamInfo* SegmentTestBase::CreateVideoStreamInfo() const {
return new VideoStreamInfo(kTrackId, kTimeScale, kDuration, kVideoCodec,
kCodecString, kLanguage, kWidth, kHeight,
kPixelWidth, kPixelHeight, kTrickPlayRate,
kNaluLengthSize, NULL, 0, false);
}
std::string SegmentTestBase::OutputFileName() const {
return output_file_name_;
}
std::string SegmentTestBase::TemplateFileName(int number) const {
return GetSegmentName(segment_template_, 0, number, 0);
}
SegmentTestBase::ClusterParser::ClusterParser() : in_cluster_(false) {}
SegmentTestBase::ClusterParser::~ClusterParser() {}
void SegmentTestBase::ClusterParser::PopulateFromCluster(
const std::string& file_name) {
cluster_sizes_.clear();
std::string file_contents;
ASSERT_TRUE(File::ReadFileToString(file_name.c_str(), &file_contents));
const uint8_t* data = reinterpret_cast<const uint8_t*>(file_contents.c_str());
const size_t size = file_contents.size();
WebMListParser cluster_parser(kWebMIdCluster, this);
size_t position = 0;
while (position < size) {
int read = cluster_parser.Parse(data + position, size - position);
ASSERT_LT(0, read);
cluster_parser.Reset();
position += read;
}
}
void SegmentTestBase::ClusterParser::PopulateFromSegment(
const std::string& file_name) {
cluster_sizes_.clear();
std::string file_contents;
ASSERT_TRUE(File::ReadFileToString(file_name.c_str(), &file_contents));
const uint8_t* data = reinterpret_cast<const uint8_t*>(file_contents.c_str());
const size_t size = file_contents.size();
WebMListParser header_parser(kWebMIdEBMLHeader, this);
int offset = header_parser.Parse(data, size);
ASSERT_LT(0, offset);
WebMListParser segment_parser(kWebMIdSegment, this);
ASSERT_LT(0, segment_parser.Parse(data + offset, size - offset));
}
int SegmentTestBase::ClusterParser::GetFrameCountForCluster(size_t i) const {
DCHECK(i < cluster_sizes_.size());
return cluster_sizes_[i];
}
int SegmentTestBase::ClusterParser::cluster_count() const {
return cluster_sizes_.size();
}
WebMParserClient* SegmentTestBase::ClusterParser::OnListStart(int id) {
if (id == kWebMIdCluster) {
if (in_cluster_)
return NULL;
cluster_sizes_.push_back(0);
in_cluster_ = true;
}
return this;
}
bool SegmentTestBase::ClusterParser::OnListEnd(int id) {
if (id == kWebMIdCluster) {
if (!in_cluster_)
return false;
in_cluster_ = false;
}
return true;
}
bool SegmentTestBase::ClusterParser::OnUInt(int id, int64_t val) {
return true;
}
bool SegmentTestBase::ClusterParser::OnFloat(int id, double val) {
return true;
}
bool SegmentTestBase::ClusterParser::OnBinary(int id,
const uint8_t* data,
int size) {
if (in_cluster_ && id == kWebMIdSimpleBlock) {
cluster_sizes_[cluster_sizes_.size() - 1]++;
}
return true;
}
bool SegmentTestBase::ClusterParser::OnString(int id, const std::string& str) {
return true;
}
} // namespace media
} // namespace edash_packager

View File

@ -0,0 +1,100 @@
// Copyright 2015 Google Inc. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
#ifndef MEDIA_FORMATS_WEBM_SEGMENTER_TEST_UTILS_H_
#define MEDIA_FORMATS_WEBM_SEGMENTER_TEST_UTILS_H_
#include <gtest/gtest.h>
#include "packager/media/base/media_sample.h"
#include "packager/media/base/muxer_options.h"
#include "packager/media/base/video_stream_info.h"
#include "packager/media/base/status.h"
#include "packager/media/base/stream_info.h"
#include "packager/media/base/test/status_test_util.h"
#include "packager/media/file/file_closer.h"
#include "packager/media/file/file_test_util.h"
#include "packager/media/file/memory_file.h"
#include "packager/media/formats/webm/mkv_writer.h"
#include "packager/media/formats/webm/segmenter.h"
#include "packager/media/formats/webm/webm_parser.h"
namespace edash_packager {
namespace media {
class SegmentTestBase : public ::testing::Test {
protected:
SegmentTestBase();
void SetUp() override;
void TearDown() override;
/// Creates a Segmenter of the given type and initializes it.
template <typename S>
void CreateAndInitializeSegmenter(const MuxerOptions& options,
StreamInfo* info,
scoped_ptr<webm::Segmenter>* result) const {
scoped_ptr<S> segmenter(new S(options));
scoped_ptr<MkvWriter> writer(new MkvWriter());
ASSERT_OK(writer->Open(this->output_file_name_));
ASSERT_OK(segmenter->Initialize(writer.Pass(), info, NULL, NULL, NULL));
*result = segmenter.Pass();
}
/// Creates a new media sample.
scoped_refptr<MediaSample> CreateSample(bool is_key_frame, uint64_t duration);
/// Creates a Muxer options object for testing.
MuxerOptions CreateMuxerOptions() const;
/// Creates a video stream info object for testing.
VideoStreamInfo* CreateVideoStreamInfo() const;
/// Gets the file name of the current output file.
std::string OutputFileName() const;
/// Gets the file name of the given template file.
std::string TemplateFileName(int number) const;
protected:
// A helper class used to determine the number of clusters and frames for a
// given WebM file.
class ClusterParser : private WebMParserClient {
public:
ClusterParser();
~ClusterParser() override;
// Make sure to use ASSERT_NO_FATAL_FAILURE.
void PopulateFromCluster(const std::string& file_name);
void PopulateFromSegment(const std::string& file_name);
int GetFrameCountForCluster(size_t i) const;
int cluster_count() const;
private:
// WebMParserClient overrides.
WebMParserClient* OnListStart(int id) override;
bool OnListEnd(int id) override;
bool OnUInt(int id, int64_t val) override;
bool OnFloat(int id, double val) override;
bool OnBinary(int id, const uint8_t* data, int size) override;
bool OnString(int id, const std::string& str) override;
private:
std::vector<int> cluster_sizes_;
bool in_cluster_;
};
protected:
std::string output_file_name_;
std::string segment_template_;
uint64_t cur_time_timescale_;
bool single_segment_;
};
} // namespace media
} // namespace edash_packager
#endif // MEDIA_FORMATS_WEBM_SEGMENTER_TEST_UTILS_H_

View File

@ -32,6 +32,7 @@ Status SingleSegmentSegmenter::DoFinalize() {
// Write the Cues to the end of the file.
index_start_ = writer_->Position();
seek_head()->set_cues_pos(index_start_);
if (!cues()->Write(writer_.get()))
return Status(error::FILE_FAILURE, "Error writing Cues data.");

View File

@ -0,0 +1,232 @@
// Copyright (c) 2015 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 "packager/media/formats/webm/single_segment_segmenter.h"
#include "packager/media/formats/webm/two_pass_single_segment_segmenter.h"
#include <gtest/gtest.h>
#include "packager/base/memory/scoped_ptr.h"
#include "packager/media/formats/webm/segmenter_test_base.h"
namespace edash_packager {
namespace media {
namespace {
const uint64_t kDuration = 1000;
const uint8_t kBasicSupportData[] = {
// ID: EBML Header, Size: 31
0x1a, 0x45, 0xdf, 0xa3, 0x9f,
// EBMLVersion: 1
0x42, 0x86, 0x81, 0x01,
// EBMLReadVersion: 1
0x42, 0xf7, 0x81, 0x01,
// EBMLMaxIDLength: 4
0x42, 0xf2, 0x81, 0x04,
// EBMLMaxSizeLength: 8
0x42, 0xf3, 0x81, 0x08,
// DocType: 'webm'
0x42, 0x82, 0x84, 0x77, 0x65, 0x62, 0x6d,
// DocTypeVersion: 2
0x42, 0x87, 0x81, 0x02,
// DocTypeReadVersion: 2
0x42, 0x85, 0x81, 0x02,
// ID: Segment, Size: 287
0x18, 0x53, 0x80, 0x67, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x16,
// ID: SeekHead, Size: 29
0x11, 0x4d, 0x9b, 0x74, 0x9d,
// ID: Seek, Size: 11
0x4d, 0xbb, 0x8b,
// SeekID: binary(4)
0x53, 0xab, 0x84, 0x1f, 0x43, 0xb6, 0x75,
// SeekPosition: 238
0x53, 0xac, 0x81, 0xee,
// ID: Seek, Size: 12
0x4d, 0xbb, 0x8c,
// SeekID: binary(4)
0x53, 0xab, 0x84, 0x1c, 0x53, 0xbb, 0x6b,
// SeekPosition: 174
0x53, 0xac, 0x82, 0x01, 0x34,
// ID: Void, Size: 53
0xec, 0xb5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// ID: Info, size: 50
0x15, 0x49, 0xa9, 0x66, 0xb2,
// TimecodeScale: 1000000
0x2a, 0xd7, 0xb1, 0x83, 0x0f, 0x42, 0x40,
// Duration: float(5000)
0x44, 0x89, 0x84, 0x45, 0x9c, 0x40, 0x00,
// MuxingApp: 'libwebm-0.2.1.0'
0x4d, 0x80, 0x8f, 0x6c, 0x69, 0x62, 0x77, 0x65, 0x62, 0x6d, 0x2d, 0x30,
0x2e, 0x32, 0x2e, 0x31, 0x2e, 0x30,
// WritingApp: 'libwebm-0.2.1.0'
0x57, 0x41, 0x8f, 0x6c, 0x69, 0x62, 0x77, 0x65, 0x62, 0x6d, 0x2d, 0x30,
0x2e, 0x32, 0x2e, 0x31, 0x2e, 0x30,
// ID: Tracks, size: 41
0x16, 0x54, 0xae, 0x6b, 0xa9,
// ID: Track, size: 39
0xae, 0xa7,
// TrackNumber: 1
0xd7, 0x81, 0x01,
// TrackUID: 1
0x73, 0xc5, 0x81, 0x01,
// TrackType: 1
0x83, 0x81, 0x01,
// CodecID: 'V_VP8'
0x86, 0x85, 0x56, 0x5f, 0x56, 0x50, 0x38,
// Language: 'en'
0x22, 0xb5, 0x9c, 0x82, 0x65, 0x6e,
// ID: Video, Size: 14
0xe0, 0x8e,
// PixelWidth: 100
0xb0, 0x81, 0x64,
// PixelHeight: 100
0xba, 0x81, 0x64,
// DisplayWidth: 100
0x54, 0xb0, 0x81, 0x64,
// DisplayHeight: 100
0x54, 0xba, 0x81, 0x64,
// ID: Cluster, size: 58
0x1f, 0x43, 0xb6, 0x75, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3a,
// Timecode: 0
0xe7, 0x81, 0x00,
// ID: SimpleBlock, Size: 9
0xa3, 0x89, 0x81, 0x00, 0x00, 0x80, 0xde, 0xad, 0xbe, 0xef, 0x00,
// ID: SimpleBlock, Size: 9
0xa3, 0x89, 0x81, 0x03, 0xe8, 0x80, 0xde, 0xad, 0xbe, 0xef, 0x00,
// ID: SimpleBlock, Size: 9
0xa3, 0x89, 0x81, 0x07, 0xd0, 0x80, 0xde, 0xad, 0xbe, 0xef, 0x00,
// ID: SimpleBlock, Size: 9
0xa3, 0x89, 0x81, 0x0b, 0xb8, 0x80, 0xde, 0xad, 0xbe, 0xef, 0x00,
// ID: SimpleBlock, Size: 9
0xa3, 0x89, 0x81, 0x0f, 0xa0, 0x80, 0xde, 0xad, 0xbe, 0xef, 0x00,
// ID: Cues, Size: 13
0x1c, 0x53, 0xbb, 0x6b, 0x8d,
// ID: CuePoint, Size: 11
0xbb, 0x8b,
// CueTime: 0
0xb3, 0x81, 0x00,
// ID: CueTrackPositions, Size: 6
0xb7, 0x86,
// CueTrack: 1
0xf7, 0x81, 0x01,
// CueClusterPosition: 190
0xf1, 0x81, 0xbe
};
} // namespace
// This is a parameterized test that tests both SingleSegmentSegmenter and
// TwoPassSingleSegmentSegmenter, since they should provide the exact same
// output.
class SingleSegmentSegmenterTest : public SegmentTestBase,
public ::testing::WithParamInterface<bool> {
public:
SingleSegmentSegmenterTest() : info_(CreateVideoStreamInfo()) {}
protected:
void InitializeSegmenter(const MuxerOptions& options) {
if (!GetParam()) {
ASSERT_NO_FATAL_FAILURE(
CreateAndInitializeSegmenter<webm::SingleSegmentSegmenter>(
options, info_.get(), &segmenter_));
} else {
ASSERT_NO_FATAL_FAILURE(
CreateAndInitializeSegmenter<webm::TwoPassSingleSegmentSegmenter>(
options, info_.get(), &segmenter_));
}
}
scoped_refptr<StreamInfo> info_;
scoped_ptr<webm::Segmenter> segmenter_;
};
TEST_P(SingleSegmentSegmenterTest, BasicSupport) {
MuxerOptions options = CreateMuxerOptions();
ASSERT_NO_FATAL_FAILURE(InitializeSegmenter(options));
// Write the samples to the Segmenter.
for (int i = 0; i < 5; i++) {
scoped_refptr<MediaSample> sample = CreateSample(true, kDuration);
ASSERT_OK(segmenter_->AddSample(sample));
}
ASSERT_OK(segmenter_->Finalize());
ASSERT_FILE_EQ(OutputFileName().c_str(), kBasicSupportData);
}
TEST_P(SingleSegmentSegmenterTest, SplitsClustersOnSegmentDuration) {
MuxerOptions options = CreateMuxerOptions();
options.segment_duration = 4.5; // seconds
ASSERT_NO_FATAL_FAILURE(InitializeSegmenter(options));
// Write the samples to the Segmenter.
for (int i = 0; i < 8; i++) {
scoped_refptr<MediaSample> sample = CreateSample(true, kDuration);
ASSERT_OK(segmenter_->AddSample(sample));
}
ASSERT_OK(segmenter_->Finalize());
// Verify the resulting data.
ClusterParser parser;
ASSERT_NO_FATAL_FAILURE(parser.PopulateFromSegment(OutputFileName()));
ASSERT_EQ(2, parser.cluster_count());
EXPECT_EQ(5, parser.GetFrameCountForCluster(0));
EXPECT_EQ(3, parser.GetFrameCountForCluster(1));
}
TEST_P(SingleSegmentSegmenterTest, IgnoresFragmentDuration) {
MuxerOptions options = CreateMuxerOptions();
options.fragment_duration = 5; // seconds
ASSERT_NO_FATAL_FAILURE(InitializeSegmenter(options));
// Write the samples to the Segmenter.
for (int i = 0; i < 8; i++) {
scoped_refptr<MediaSample> sample = CreateSample(true, kDuration);
ASSERT_OK(segmenter_->AddSample(sample));
}
ASSERT_OK(segmenter_->Finalize());
// Verify the resulting data.
ClusterParser parser;
ASSERT_NO_FATAL_FAILURE(parser.PopulateFromSegment(OutputFileName()));
ASSERT_EQ(1, parser.cluster_count());
EXPECT_EQ(8, parser.GetFrameCountForCluster(0));
}
TEST_P(SingleSegmentSegmenterTest, RespectsSAPAlign) {
MuxerOptions options = CreateMuxerOptions();
options.segment_duration = 3; // seconds
options.segment_sap_aligned = true;
ASSERT_NO_FATAL_FAILURE(InitializeSegmenter(options));
// Write the samples to the Segmenter.
for (int i = 0; i < 10; i++) {
scoped_refptr<MediaSample> sample = CreateSample(i == 6, kDuration);
ASSERT_OK(segmenter_->AddSample(sample));
}
ASSERT_OK(segmenter_->Finalize());
// Verify the resulting data.
ClusterParser parser;
ASSERT_NO_FATAL_FAILURE(parser.PopulateFromSegment(OutputFileName()));
// Segments are 1 second, so there would normally be 3 frames per cluster,
// but since it's SAP aligned and only frame 7 is a key-frame, there are
// two clusters with 6 and 4 frames respectively.
ASSERT_EQ(2, parser.cluster_count());
EXPECT_EQ(6, parser.GetFrameCountForCluster(0));
EXPECT_EQ(4, parser.GetFrameCountForCluster(1));
}
INSTANTIATE_TEST_CASE_P(TrueIsTwoPass,
SingleSegmentSegmenterTest,
::testing::Bool());
} // namespace media
} // namespace edash_packager

View File

@ -75,13 +75,13 @@ Status
TwoPassSingleSegmentSegmenter::DoInitialize(scoped_ptr<MkvWriter> writer) {
// Assume the amount of time to copy the temp file as the same amount
// of time as to make it.
set_progress_target(stream()->info()->duration() * 2);
set_progress_target(info()->duration() * 2);
real_writer_ = writer.Pass();
std::string temp_name = TempFileName(options());
temp_file_name_ = TempFileName(options());
scoped_ptr<MkvWriter> temp(new MkvWriter);
Status status = temp->Open(temp_name);
Status status = temp->Open(temp_file_name_);
if (!status.ok())
return status;
@ -105,9 +105,9 @@ Status TwoPassSingleSegmentSegmenter::DoFinalize() {
return temp;
// Close the temp file and open it for reading.
std::string temp_name = writer()->file()->file_name();
set_writer(scoped_ptr<MkvWriter>());
scoped_ptr<File, FileCloser> temp_reader(File::Open(temp_name.c_str(), "r"));
scoped_ptr<File, FileCloser> temp_reader(
File::Open(temp_file_name_.c_str(), "r"));
if (!temp_reader)
return Status(error::FILE_FAILURE, "Error opening temp file.");
@ -123,8 +123,8 @@ Status TwoPassSingleSegmentSegmenter::DoFinalize() {
// Close and delete the temp file.
temp_reader.reset();
if (!File::Delete(temp_name.c_str())) {
LOG(WARNING) << "Unable to delete temporary file " << temp_name;
if (!File::Delete(temp_file_name_.c_str())) {
LOG(WARNING) << "Unable to delete temporary file " << temp_file_name_;
}
// Set the writer back to the real file so GetIndexRangeStartAndEnd works.

View File

@ -7,6 +7,8 @@
#ifndef MEDIA_FORMATS_WEBM_TWO_PASS_SINGLE_SEGMENT_SEGMENTER_H_
#define MEDIA_FORMATS_WEBM_TWO_PASS_SINGLE_SEGMENT_SEGMENTER_H_
#include <string>
#include "packager/media/formats/webm/single_segment_segmenter.h"
#include "packager/base/memory/scoped_ptr.h"
@ -41,6 +43,7 @@ class TwoPassSingleSegmentSegmenter : public SingleSegmentSegmenter {
uint64_t last_size);
scoped_ptr<MkvWriter> real_writer_;
std::string temp_file_name_;
DISALLOW_COPY_AND_ASSIGN(TwoPassSingleSegmentSegmenter);
};

View File

@ -65,8 +65,12 @@
'sources': [
'cluster_builder.cc',
'cluster_builder.h',
'multi_segment_segmenter_unittest.cc',
'opus_packet_builder.cc',
'opus_packet_builder.h',
'segmenter_test_base.cc',
'segmenter_test_base.h',
'single_segment_segmenter_unittest.cc',
'tracks_builder.cc',
'tracks_builder.h',
'webm_cluster_parser_unittest.cc',
@ -78,6 +82,7 @@
'dependencies': [
'../../../testing/gtest.gyp:gtest',
'../../../testing/gmock.gyp:gmock',
'../../file/file.gyp:file',
'../../test/media_test.gyp:media_test_support',
'webm',
]

View File

@ -37,9 +37,9 @@ Status WebMMuxer::Initialize() {
segmenter_.reset(new TwoPassSingleSegmentSegmenter(options()));
}
Status initialized =
segmenter_->Initialize(writer.Pass(), streams()[0], progress_listener(),
muxer_listener(), encryption_key_source());
Status initialized = segmenter_->Initialize(
writer.Pass(), streams()[0]->info().get(), progress_listener(),
muxer_listener(), encryption_key_source());
if (!initialized.ok())
return initialized;