Fix WebM timestamp overflow if longer than 2.5 hours

Fixes #233

Change-Id: I3431f8e68bfb1de222b8ab967c9cac6a821b4f53
This commit is contained in:
KongQun Yang 2017-05-12 17:02:12 -07:00
parent 27abb208aa
commit 3443048e80
12 changed files with 233 additions and 98 deletions

View File

@ -12,7 +12,8 @@ namespace shaka {
namespace media { namespace media {
namespace { namespace {
const uint64_t kDuration = 1000u; const uint32_t kTimeScale = 1000000u;
const uint64_t kDuration = 1000000u;
const bool kSubsegment = true; const bool kSubsegment = true;
const uint8_t kPerSampleIvSize = 8u; const uint8_t kPerSampleIvSize = 8u;
const uint8_t kKeyId[] = { const uint8_t kKeyId[] = {
@ -196,7 +197,7 @@ const uint8_t kBasicSupportData[] = {
class EncryptedSegmenterTest : public SegmentTestBase { class EncryptedSegmenterTest : public SegmentTestBase {
public: public:
EncryptedSegmenterTest() : info_(CreateVideoStreamInfo()) { EncryptedSegmenterTest() : info_(CreateVideoStreamInfo(kTimeScale)) {
EncryptionConfig encryption_config; EncryptionConfig encryption_config;
encryption_config.per_sample_iv_size = kPerSampleIvSize; encryption_config.per_sample_iv_size = kPerSampleIvSize;
encryption_config.key_id.assign(kKeyId, kKeyId + sizeof(kKeyId)); encryption_config.key_id.assign(kKeyId, kKeyId + sizeof(kKeyId));

View File

@ -21,12 +21,12 @@ MultiSegmentSegmenter::MultiSegmentSegmenter(const MuxerOptions& options)
MultiSegmentSegmenter::~MultiSegmentSegmenter() {} MultiSegmentSegmenter::~MultiSegmentSegmenter() {}
Status MultiSegmentSegmenter::FinalizeSegment(uint64_t start_timescale, Status MultiSegmentSegmenter::FinalizeSegment(uint64_t start_timestamp,
uint64_t duration_timescale, uint64_t duration_timestamp,
bool is_subsegment) { bool is_subsegment) {
CHECK(cluster()); CHECK(cluster());
Status status = Segmenter::FinalizeSegment(start_timescale, Status status = Segmenter::FinalizeSegment(start_timestamp,
duration_timescale, is_subsegment); duration_timestamp, is_subsegment);
if (!status.ok()) if (!status.ok())
return status; return status;
if (!cluster()->Finalize()) if (!cluster()->Finalize())
@ -35,7 +35,7 @@ Status MultiSegmentSegmenter::FinalizeSegment(uint64_t start_timescale,
if (muxer_listener()) { if (muxer_listener()) {
const uint64_t size = cluster()->Size(); const uint64_t size = cluster()->Size();
muxer_listener()->OnNewSegment(writer_->file()->file_name(), muxer_listener()->OnNewSegment(writer_->file()->file_name(),
start_timescale, duration_timescale, size); start_timestamp, duration_timestamp, size);
} }
VLOG(1) << "WEBM file '" << writer_->file()->file_name() << "' finalized."; VLOG(1) << "WEBM file '" << writer_->file()->file_name() << "' finalized.";
} }
@ -65,12 +65,12 @@ Status MultiSegmentSegmenter::DoFinalize() {
return writer_->Close(); return writer_->Close();
} }
Status MultiSegmentSegmenter::NewSegment(uint64_t start_timescale, Status MultiSegmentSegmenter::NewSegment(uint64_t start_timestamp,
bool is_subsegment) { bool is_subsegment) {
if (!is_subsegment) { if (!is_subsegment) {
// Create a new file for the new segment. // Create a new file for the new segment.
std::string segment_name = std::string segment_name =
GetSegmentName(options().segment_template, start_timescale, GetSegmentName(options().segment_template, start_timestamp,
num_segment_, options().bandwidth); num_segment_, options().bandwidth);
writer_.reset(new MkvWriter); writer_.reset(new MkvWriter);
Status status = writer_->Open(segment_name); Status status = writer_->Open(segment_name);
@ -79,8 +79,8 @@ Status MultiSegmentSegmenter::NewSegment(uint64_t start_timescale,
num_segment_++; num_segment_++;
} }
uint64_t start_webm_timecode = FromBMFFTimescale(start_timescale); const uint64_t start_timecode = FromBmffTimestamp(start_timestamp);
return SetCluster(start_webm_timecode, 0, writer_.get()); return SetCluster(start_timecode, 0, writer_.get());
} }
} // namespace webm } // namespace webm

View File

@ -28,8 +28,8 @@ class MultiSegmentSegmenter : public Segmenter {
/// @name Segmenter implementation overrides. /// @name Segmenter implementation overrides.
/// @{ /// @{
Status FinalizeSegment(uint64_t start_timescale, Status FinalizeSegment(uint64_t start_timestamp,
uint64_t duration_timescale, uint64_t duration_timestamp,
bool is_subsegment) override; bool is_subsegment) override;
bool GetInitRangeStartAndEnd(uint64_t* start, uint64_t* end) override; bool GetInitRangeStartAndEnd(uint64_t* start, uint64_t* end) override;
bool GetIndexRangeStartAndEnd(uint64_t* start, uint64_t* end) override; bool GetIndexRangeStartAndEnd(uint64_t* start, uint64_t* end) override;
@ -42,7 +42,7 @@ class MultiSegmentSegmenter : public Segmenter {
private: private:
// Segmenter implementation overrides. // Segmenter implementation overrides.
Status NewSegment(uint64_t start_timescale, bool is_subsegment) override; Status NewSegment(uint64_t start_timestamp, bool is_subsegment) override;
std::unique_ptr<MkvWriter> writer_; std::unique_ptr<MkvWriter> writer_;
uint32_t num_segment_; uint32_t num_segment_;

View File

@ -13,7 +13,8 @@ namespace shaka {
namespace media { namespace media {
namespace { namespace {
const uint64_t kDuration = 1000; const uint32_t kTimeScale = 1000000u;
const uint64_t kDuration = 1000000u;
const bool kSubsegment = true; const bool kSubsegment = true;
const uint8_t kBasicSupportDataInit[] = { const uint8_t kBasicSupportDataInit[] = {
@ -94,7 +95,7 @@ const uint8_t kBasicSupportDataSegment[] = {
class MultiSegmentSegmenterTest : public SegmentTestBase { class MultiSegmentSegmenterTest : public SegmentTestBase {
public: public:
MultiSegmentSegmenterTest() MultiSegmentSegmenterTest()
: info_(CreateVideoStreamInfo()), : info_(CreateVideoStreamInfo(kTimeScale)),
segment_template_(std::string(kMemoryFilePrefix) + segment_template_(std::string(kMemoryFilePrefix) +
"output-template-$Number$.webm") {} "output-template-$Number$.webm") {}
@ -157,11 +158,11 @@ TEST_F(MultiSegmentSegmenterTest, SplitsFilesOnSegment) {
ClusterParser parser; ClusterParser parser;
ASSERT_NO_FATAL_FAILURE(parser.PopulateFromCluster(TemplateFileName(0))); ASSERT_NO_FATAL_FAILURE(parser.PopulateFromCluster(TemplateFileName(0)));
ASSERT_EQ(1u, parser.cluster_count()); ASSERT_EQ(1u, parser.cluster_count());
EXPECT_EQ(5, parser.GetFrameCountForCluster(0)); EXPECT_EQ(5u, parser.GetFrameCountForCluster(0));
ASSERT_NO_FATAL_FAILURE(parser.PopulateFromCluster(TemplateFileName(1))); ASSERT_NO_FATAL_FAILURE(parser.PopulateFromCluster(TemplateFileName(1)));
ASSERT_EQ(1u, parser.cluster_count()); ASSERT_EQ(1u, parser.cluster_count());
EXPECT_EQ(3, parser.GetFrameCountForCluster(0)); EXPECT_EQ(3u, parser.GetFrameCountForCluster(0));
EXPECT_FALSE(File::Open(TemplateFileName(2).c_str(), "r")); EXPECT_FALSE(File::Open(TemplateFileName(2).c_str(), "r"));
} }
@ -186,8 +187,8 @@ TEST_F(MultiSegmentSegmenterTest, SplitsClustersOnSubsegment) {
ClusterParser parser; ClusterParser parser;
ASSERT_NO_FATAL_FAILURE(parser.PopulateFromCluster(TemplateFileName(0))); ASSERT_NO_FATAL_FAILURE(parser.PopulateFromCluster(TemplateFileName(0)));
ASSERT_EQ(2u, parser.cluster_count()); ASSERT_EQ(2u, parser.cluster_count());
EXPECT_EQ(5, parser.GetFrameCountForCluster(0)); EXPECT_EQ(5u, parser.GetFrameCountForCluster(0));
EXPECT_EQ(3, parser.GetFrameCountForCluster(1)); EXPECT_EQ(3u, parser.GetFrameCountForCluster(1));
EXPECT_FALSE(File::Open(TemplateFileName(1).c_str(), "r")); EXPECT_FALSE(File::Open(TemplateFileName(1).c_str(), "r"));
} }

View File

@ -30,8 +30,45 @@ namespace shaka {
namespace media { namespace media {
namespace webm { namespace webm {
namespace { namespace {
int64_t kTimecodeScale = 1000000; const int64_t kTimecodeScale = 1000000;
int64_t kSecondsToNs = 1000000000L; const int64_t kSecondsToNs = 1000000000L;
// Round to closest integer.
uint64_t Round(double value) {
return static_cast<uint64_t>(value + 0.5);
}
// There are three different kinds of timestamp here:
// (1) ISO-BMFF timestamp (seconds scaled by ISO-BMFF timescale)
// This is used in our MediaSample and StreamInfo structures.
// (2) WebM timecode (seconds scaled by kSecondsToNs / WebM timecode scale)
// This is used in most WebM structures.
// (3) Nanoseconds (seconds scaled by kSecondsToNs)
// This is used in some WebM structures, e.g. Frame.
// We use Nanoseconds as intermediate format here for conversion, in
// uint64_t/int64_t, which is sufficient to represent a time as large as 292
// years.
uint64_t BmffTimestampToNs(uint64_t timestamp, uint64_t time_scale) {
// Casting to double is needed otherwise kSecondsToNs * timestamp may overflow
// uint64_t/int64_t.
return Round(static_cast<double>(timestamp) / time_scale * kSecondsToNs);
}
uint64_t NsToBmffTimestamp(uint64_t ns, uint64_t time_scale) {
// Casting to double is needed otherwise ns * time_scale may overflow
// uint64_t/int64_t.
return Round(static_cast<double>(ns) / kSecondsToNs * time_scale);
}
uint64_t NsToWebMTimecode(uint64_t ns, uint64_t timecode_scale) {
return ns / timecode_scale;
}
uint64_t WebMTimecodeToNs(uint64_t timecode, uint64_t timecode_scale) {
return timecode * timecode_scale;
}
} // namespace } // namespace
Segmenter::Segmenter(const MuxerOptions& options) : options_(options) {} Segmenter::Segmenter(const MuxerOptions& options) : options_(options) {}
@ -112,7 +149,7 @@ Status Segmenter::Initialize(StreamInfo* info,
Status Segmenter::Finalize() { Status Segmenter::Finalize() {
uint64_t duration = uint64_t duration =
prev_sample_->pts() - first_timestamp_ + prev_sample_->duration(); prev_sample_->pts() - first_timestamp_ + prev_sample_->duration();
segment_info_.set_duration(FromBMFFTimescale(duration)); segment_info_.set_duration(FromBmffTimestamp(duration));
return DoFinalize(); return DoFinalize();
} }
@ -150,8 +187,8 @@ Status Segmenter::AddSample(std::shared_ptr<MediaSample> sample) {
return Status::OK; return Status::OK;
} }
Status Segmenter::FinalizeSegment(uint64_t start_timescale, Status Segmenter::FinalizeSegment(uint64_t start_timestamp,
uint64_t duration_timescale, uint64_t duration_timestamp,
bool is_subsegment) { bool is_subsegment) {
if (is_subsegment) if (is_subsegment)
new_subsegment_ = true; new_subsegment_ = true;
@ -160,22 +197,22 @@ Status Segmenter::FinalizeSegment(uint64_t start_timescale,
return WriteFrame(true /* write duration */); return WriteFrame(true /* write duration */);
} }
float Segmenter::GetDuration() const { float Segmenter::GetDurationInSeconds() const {
return static_cast<float>(segment_info_.duration()) * return WebMTimecodeToNs(segment_info_.duration(),
segment_info_.timecode_scale() / kSecondsToNs; segment_info_.timecode_scale()) /
static_cast<double>(kSecondsToNs);
} }
uint64_t Segmenter::FromBMFFTimescale(uint64_t time_timescale) { uint64_t Segmenter::FromBmffTimestamp(uint64_t bmff_timestamp) {
// Convert the time from BMFF time_code to WebM timecode scale. return NsToWebMTimecode(
const int64_t time_ns = BmffTimestampToNs(bmff_timestamp, info_->time_scale()),
kSecondsToNs * time_timescale / info_->time_scale(); segment_info_.timecode_scale());
return time_ns / segment_info_.timecode_scale();
} }
uint64_t Segmenter::FromWebMTimecode(uint64_t time_webm_timecode) { uint64_t Segmenter::FromWebMTimecode(uint64_t webm_timecode) {
// Convert the time to BMFF time_code from WebM timecode scale. return NsToBmffTimestamp(
const int64_t time_ns = time_webm_timecode * segment_info_.timecode_scale(); WebMTimecodeToNs(webm_timecode, segment_info_.timecode_scale()),
return time_ns * info_->time_scale() / kSecondsToNs; info_->time_scale());
} }
Status Segmenter::WriteSegmentHeader(uint64_t file_size, MkvWriter* writer) { Status Segmenter::WriteSegmentHeader(uint64_t file_size, MkvWriter* writer) {
@ -334,12 +371,12 @@ Status Segmenter::WriteFrame(bool write_duration) {
} }
if (write_duration) { if (write_duration) {
const uint64_t duration_ns = frame.set_duration(
prev_sample_->duration() * kSecondsToNs / info_->time_scale(); BmffTimestampToNs(prev_sample_->duration(), info_->time_scale()));
frame.set_duration(duration_ns);
} }
frame.set_is_key(prev_sample_->is_key_frame()); frame.set_is_key(prev_sample_->is_key_frame());
frame.set_timestamp(prev_sample_->pts() * kSecondsToNs / info_->time_scale()); frame.set_timestamp(
BmffTimestampToNs(prev_sample_->pts(), info_->time_scale()));
frame.set_track_number(track_id_); frame.set_track_number(track_id_);
if (prev_sample_->side_data_size() > 0) { if (prev_sample_->side_data_size() > 0) {
@ -359,15 +396,14 @@ Status Segmenter::WriteFrame(bool write_duration) {
} }
if (!prev_sample_->is_key_frame() && !frame.CanBeSimpleBlock()) { if (!prev_sample_->is_key_frame() && !frame.CanBeSimpleBlock()) {
const int64_t timestamp_ns = frame.set_reference_block_timestamp(
reference_frame_timestamp_ * kSecondsToNs / info_->time_scale(); BmffTimestampToNs(reference_frame_timestamp_, info_->time_scale()));
frame.set_reference_block_timestamp(timestamp_ns);
} }
// GetRelativeTimecode will return -1 if the relative timecode is too large // GetRelativeTimecode will return -1 if the relative timecode is too large
// to fit in the frame. // to fit in the frame.
if (cluster_->GetRelativeTimecode(frame.timestamp() / if (cluster_->GetRelativeTimecode(NsToWebMTimecode(
cluster_->timecode_scale()) < 0) { frame.timestamp(), cluster_->timecode_scale())) < 0) {
const double segment_duration = const double segment_duration =
static_cast<double>(frame.timestamp()) / kSecondsToNs; static_cast<double>(frame.timestamp()) / kSecondsToNs;
LOG(ERROR) << "Error adding sample to segment: segment too large, " LOG(ERROR) << "Error adding sample to segment: segment too large, "

View File

@ -52,8 +52,8 @@ class Segmenter {
Status AddSample(std::shared_ptr<MediaSample> sample); Status AddSample(std::shared_ptr<MediaSample> sample);
/// Finalize the (sub)segment. /// Finalize the (sub)segment.
virtual Status FinalizeSegment(uint64_t start_timescale, virtual Status FinalizeSegment(uint64_t start_timestamp,
uint64_t duration_timescale, uint64_t duration_timestamp,
bool is_subsegment) = 0; bool is_subsegment) = 0;
/// @return true if there is an initialization range, while setting @a start /// @return true if there is an initialization range, while setting @a start
@ -65,14 +65,13 @@ class Segmenter {
virtual bool GetIndexRangeStartAndEnd(uint64_t* start, uint64_t* end) = 0; virtual bool GetIndexRangeStartAndEnd(uint64_t* start, uint64_t* end) = 0;
/// @return The total length, in seconds, of segmented media files. /// @return The total length, in seconds, of segmented media files.
float GetDuration() const; float GetDurationInSeconds() const;
protected: protected:
/// Converts the given time in ISO BMFF timescale to the current WebM /// Converts the given time in ISO BMFF timestamp to WebM timecode.
/// timecode. uint64_t FromBmffTimestamp(uint64_t bmff_timestamp);
uint64_t FromBMFFTimescale(uint64_t time_timescale); /// Converts the given time in WebM timecode to ISO BMFF timestamp.
/// Converts the given time in WebM timecode to ISO BMFF timescale. uint64_t FromWebMTimecode(uint64_t webm_timecode);
uint64_t FromWebMTimecode(uint64_t time_webm_timecode);
/// Writes the Segment header to @a writer. /// Writes the Segment header to @a writer.
Status WriteSegmentHeader(uint64_t file_size, MkvWriter* writer); Status WriteSegmentHeader(uint64_t file_size, MkvWriter* writer);
/// Creates a Cluster object with the given parameters. /// Creates a Cluster object with the given parameters.
@ -110,7 +109,7 @@ class Segmenter {
// In single-segment mode, a Cluster is a segment and there is no subsegment. // In single-segment mode, a Cluster is a segment and there is no subsegment.
// In multi-segment mode, a new file is a segment and the clusters in the file // In multi-segment mode, a new file is a segment and the clusters in the file
// are subsegments. // are subsegments.
virtual Status NewSegment(uint64_t start_timescale, bool is_subsegment) = 0; virtual Status NewSegment(uint64_t start_timestamp, bool is_subsegment) = 0;
// Store the previous sample so we know which one is the last frame. // Store the previous sample so we know which one is the last frame.
std::shared_ptr<MediaSample> prev_sample_; std::shared_ptr<MediaSample> prev_sample_;

View File

@ -22,8 +22,7 @@ const uint8_t kTestMediaSampleSideData[] = {
0x73, 0x69, 0x64, 0x65, 0x00}; 0x73, 0x69, 0x64, 0x65, 0x00};
const int kTrackId = 1; const int kTrackId = 1;
const uint32_t kTimeScale = 1000; const uint64_t kDurationInSeconds = 8;
const uint64_t kDuration = 8000;
const Codec kCodec = kCodecVP8; const Codec kCodec = kCodecVP8;
const std::string kCodecString = "vp8"; const std::string kCodecString = "vp8";
const std::string kLanguage = "en"; const std::string kLanguage = "en";
@ -42,7 +41,7 @@ void SegmentTestBase::SetUp() {
SetPackagerVersionForTesting("test"); SetPackagerVersionForTesting("test");
output_file_name_ = std::string(kMemoryFilePrefix) + "output-file.webm"; output_file_name_ = std::string(kMemoryFilePrefix) + "output-file.webm";
cur_time_timescale_ = 0; cur_timestamp_ = 0;
} }
void SegmentTestBase::TearDown() { void SegmentTestBase::TearDown() {
@ -64,11 +63,11 @@ std::shared_ptr<MediaSample> SegmentTestBase::CreateSample(
sample = MediaSample::CopyFrom(kTestMediaSampleData, sample = MediaSample::CopyFrom(kTestMediaSampleData,
sizeof(kTestMediaSampleData), is_key_frame); sizeof(kTestMediaSampleData), is_key_frame);
} }
sample->set_dts(cur_time_timescale_); sample->set_dts(cur_timestamp_);
sample->set_pts(cur_time_timescale_); sample->set_pts(cur_timestamp_);
sample->set_duration(duration); sample->set_duration(duration);
cur_time_timescale_ += duration; cur_timestamp_ += duration;
return sample; return sample;
} }
@ -81,24 +80,26 @@ MuxerOptions SegmentTestBase::CreateMuxerOptions() const {
return ret; return ret;
} }
VideoStreamInfo* SegmentTestBase::CreateVideoStreamInfo() const { VideoStreamInfo* SegmentTestBase::CreateVideoStreamInfo(
uint32_t time_scale) const {
return new VideoStreamInfo( return new VideoStreamInfo(
kTrackId, kTimeScale, kDuration, kCodec, H26xStreamFormat::kUnSpecified, kTrackId, time_scale, kDurationInSeconds * time_scale, kCodec,
kCodecString, NULL, 0, kWidth, kHeight, kPixelWidth, kPixelHeight, H26xStreamFormat::kUnSpecified, kCodecString, NULL, 0, kWidth, kHeight,
kTrickPlayFactor, kNaluLengthSize, kLanguage, false); kPixelWidth, kPixelHeight, kTrickPlayFactor, kNaluLengthSize, kLanguage,
false);
} }
std::string SegmentTestBase::OutputFileName() const { std::string SegmentTestBase::OutputFileName() const {
return output_file_name_; return output_file_name_;
} }
SegmentTestBase::ClusterParser::ClusterParser() : in_cluster_(false) {} SegmentTestBase::ClusterParser::ClusterParser() {}
SegmentTestBase::ClusterParser::~ClusterParser() {} SegmentTestBase::ClusterParser::~ClusterParser() {}
void SegmentTestBase::ClusterParser::PopulateFromCluster( void SegmentTestBase::ClusterParser::PopulateFromCluster(
const std::string& file_name) { const std::string& file_name) {
cluster_sizes_.clear(); frame_timecodes_.clear();
std::string file_contents; std::string file_contents;
ASSERT_TRUE(File::ReadFileToString(file_name.c_str(), &file_contents)); ASSERT_TRUE(File::ReadFileToString(file_name.c_str(), &file_contents));
@ -118,7 +119,7 @@ void SegmentTestBase::ClusterParser::PopulateFromCluster(
void SegmentTestBase::ClusterParser::PopulateFromSegment( void SegmentTestBase::ClusterParser::PopulateFromSegment(
const std::string& file_name) { const std::string& file_name) {
cluster_sizes_.clear(); frame_timecodes_.clear();
std::string file_contents; std::string file_contents;
ASSERT_TRUE(File::ReadFileToString(file_name.c_str(), &file_contents)); ASSERT_TRUE(File::ReadFileToString(file_name.c_str(), &file_contents));
@ -133,13 +134,22 @@ void SegmentTestBase::ClusterParser::PopulateFromSegment(
0, segment_parser.Parse(data + offset, static_cast<int>(size) - offset)); 0, segment_parser.Parse(data + offset, static_cast<int>(size) - offset));
} }
int SegmentTestBase::ClusterParser::GetFrameCountForCluster(size_t i) const { size_t SegmentTestBase::ClusterParser::GetFrameCountForCluster(
DCHECK(i < cluster_sizes_.size()); size_t cluster_index) const {
return cluster_sizes_[i]; DCHECK_LT(cluster_index, frame_timecodes_.size());
return frame_timecodes_[cluster_index].size();
}
int64_t SegmentTestBase::ClusterParser::GetFrameTimecode(
size_t cluster_index,
size_t frame_index) const {
DCHECK_LT(cluster_index, frame_timecodes_.size());
DCHECK_LT(frame_index, frame_timecodes_[cluster_index].size());
return frame_timecodes_[cluster_index][frame_index];
} }
size_t SegmentTestBase::ClusterParser::cluster_count() const { size_t SegmentTestBase::ClusterParser::cluster_count() const {
return cluster_sizes_.size(); return frame_timecodes_.size();
} }
WebMParserClient* SegmentTestBase::ClusterParser::OnListStart(int id) { WebMParserClient* SegmentTestBase::ClusterParser::OnListStart(int id) {
@ -147,7 +157,8 @@ WebMParserClient* SegmentTestBase::ClusterParser::OnListStart(int id) {
if (in_cluster_) if (in_cluster_)
return NULL; return NULL;
cluster_sizes_.push_back(0); frame_timecodes_.emplace_back();
cluster_timecode_ = -1;
in_cluster_ = true; in_cluster_ = true;
} }
@ -165,6 +176,8 @@ bool SegmentTestBase::ClusterParser::OnListEnd(int id) {
} }
bool SegmentTestBase::ClusterParser::OnUInt(int id, int64_t val) { bool SegmentTestBase::ClusterParser::OnUInt(int id, int64_t val) {
if (id == kWebMIdTimecode)
cluster_timecode_ = val;
return true; return true;
} }
@ -173,10 +186,15 @@ bool SegmentTestBase::ClusterParser::OnFloat(int id, double val) {
} }
bool SegmentTestBase::ClusterParser::OnBinary(int id, bool SegmentTestBase::ClusterParser::OnBinary(int id,
const uint8_t* data, const uint8_t* data,
int size) { int size) {
if (in_cluster_ && (id == kWebMIdSimpleBlock || id == kWebMIdBlock)) { if (in_cluster_ && (id == kWebMIdSimpleBlock || id == kWebMIdBlock)) {
cluster_sizes_.back()++; if (cluster_timecode_ == -1) {
LOG(WARNING) << "Cluster timecode not yet available";
return false;
}
int timecode = data[1] << 8 | data[2];
frame_timecodes_.back().push_back(cluster_timecode_ + timecode);
} }
return true; return true;

View File

@ -62,7 +62,7 @@ class SegmentTestBase : public ::testing::Test {
/// Creates a Muxer options object for testing. /// Creates a Muxer options object for testing.
MuxerOptions CreateMuxerOptions() const; MuxerOptions CreateMuxerOptions() const;
/// Creates a video stream info object for testing. /// Creates a video stream info object for testing.
VideoStreamInfo* CreateVideoStreamInfo() const; VideoStreamInfo* CreateVideoStreamInfo(uint32_t time_scale) const;
/// Gets the file name of the current output file. /// Gets the file name of the current output file.
std::string OutputFileName() const; std::string OutputFileName() const;
@ -81,7 +81,8 @@ class SegmentTestBase : public ::testing::Test {
void PopulateFromCluster(const std::string& file_name); void PopulateFromCluster(const std::string& file_name);
void PopulateFromSegment(const std::string& file_name); void PopulateFromSegment(const std::string& file_name);
int GetFrameCountForCluster(size_t i) const; size_t GetFrameCountForCluster(size_t cluster_index) const;
int64_t GetFrameTimecode(size_t cluster_index, size_t frame_index) const;
size_t cluster_count() const; size_t cluster_count() const;
@ -95,14 +96,18 @@ class SegmentTestBase : public ::testing::Test {
bool OnString(int id, const std::string& str) override; bool OnString(int id, const std::string& str) override;
private: private:
std::vector<int> cluster_sizes_; int64_t cluster_timecode_ = -1;
bool in_cluster_; // frame_timecodes_[cluster_index][frame_index].
std::vector<std::vector<int64_t>> frame_timecodes_;
bool in_cluster_ = false;
}; };
protected: protected:
void set_cur_timestamp(uint64_t timestamp) { cur_timestamp_ = timestamp; }
std::string output_file_name_; std::string output_file_name_;
std::string segment_template_; std::string segment_template_;
uint64_t cur_time_timescale_; uint64_t cur_timestamp_;
bool single_segment_; bool single_segment_;
}; };

View File

@ -18,11 +18,11 @@ SingleSegmentSegmenter::SingleSegmentSegmenter(const MuxerOptions& options)
SingleSegmentSegmenter::~SingleSegmentSegmenter() {} SingleSegmentSegmenter::~SingleSegmentSegmenter() {}
Status SingleSegmentSegmenter::FinalizeSegment(uint64_t start_timescale, Status SingleSegmentSegmenter::FinalizeSegment(uint64_t start_timestamp,
uint64_t duration_timescale, uint64_t duration_timestamp,
bool is_subsegment) { bool is_subsegment) {
Status status = Segmenter::FinalizeSegment(start_timescale, Status status = Segmenter::FinalizeSegment(start_timestamp,
duration_timescale, is_subsegment); duration_timestamp, is_subsegment);
if (!status.ok()) if (!status.ok())
return status; return status;
// No-op for subsegment in single segment mode. // No-op for subsegment in single segment mode.
@ -83,23 +83,23 @@ Status SingleSegmentSegmenter::DoFinalize() {
return status; return status;
} }
Status SingleSegmentSegmenter::NewSegment(uint64_t start_timescale, Status SingleSegmentSegmenter::NewSegment(uint64_t start_timestamp,
bool is_subsegment) { bool is_subsegment) {
// No-op for subsegment in single segment mode. // No-op for subsegment in single segment mode.
if (is_subsegment) if (is_subsegment)
return Status::OK; return Status::OK;
// Create a new Cue point. // Create a new Cue point.
uint64_t position = writer_->Position(); uint64_t position = writer_->Position();
uint64_t start_webm_timecode = FromBMFFTimescale(start_timescale); uint64_t start_timecode = FromBmffTimestamp(start_timestamp);
mkvmuxer::CuePoint* cue_point = new mkvmuxer::CuePoint; mkvmuxer::CuePoint* cue_point = new mkvmuxer::CuePoint;
cue_point->set_time(start_webm_timecode); cue_point->set_time(start_timecode);
cue_point->set_track(track_id()); cue_point->set_track(track_id());
cue_point->set_cluster_pos(position - segment_payload_pos()); cue_point->set_cluster_pos(position - segment_payload_pos());
if (!cues()->AddCue(cue_point)) if (!cues()->AddCue(cue_point))
return Status(error::INTERNAL_ERROR, "Error adding CuePoint."); return Status(error::INTERNAL_ERROR, "Error adding CuePoint.");
return SetCluster(start_webm_timecode, position, writer_.get()); return SetCluster(start_timecode, position, writer_.get());
} }
} // namespace webm } // namespace webm

View File

@ -30,8 +30,8 @@ class SingleSegmentSegmenter : public Segmenter {
/// @name Segmenter implementation overrides. /// @name Segmenter implementation overrides.
/// @{ /// @{
Status FinalizeSegment(uint64_t start_timescale, Status FinalizeSegment(uint64_t start_timestamp,
uint64_t duration_timescale, uint64_t duration_timestamp,
bool is_subsegment) override; bool is_subsegment) override;
bool GetInitRangeStartAndEnd(uint64_t* start, uint64_t* end) override; bool GetInitRangeStartAndEnd(uint64_t* start, uint64_t* end) override;
bool GetIndexRangeStartAndEnd(uint64_t* start, uint64_t* end) override; bool GetIndexRangeStartAndEnd(uint64_t* start, uint64_t* end) override;
@ -53,7 +53,7 @@ class SingleSegmentSegmenter : public Segmenter {
private: private:
// Segmenter implementation overrides. // Segmenter implementation overrides.
Status NewSegment(uint64_t start_timescale, bool is_subsegment) override; Status NewSegment(uint64_t start_timestamp, bool is_subsegment) override;
std::unique_ptr<MkvWriter> writer_; std::unique_ptr<MkvWriter> writer_;
uint64_t init_end_; uint64_t init_end_;

View File

@ -12,7 +12,10 @@ namespace shaka {
namespace media { namespace media {
namespace { namespace {
const uint64_t kDuration = 1000; const uint32_t kTimeScale = 1000000;
const uint32_t kTimecodeScale = 1000000;
const int64_t kSecondsToNs = 1000000000L;
const uint64_t kDuration = 1000000;
const bool kSubsegment = true; const bool kSubsegment = true;
const uint8_t kBasicSupportData[] = { const uint8_t kBasicSupportData[] = {
@ -135,7 +138,7 @@ const uint8_t kBasicSupportData[] = {
class SingleSegmentSegmenterTest : public SegmentTestBase { class SingleSegmentSegmenterTest : public SegmentTestBase {
public: public:
SingleSegmentSegmenterTest() : info_(CreateVideoStreamInfo()) {} SingleSegmentSegmenterTest() : info_(CreateVideoStreamInfo(kTimeScale)) {}
protected: protected:
void InitializeSegmenter(const MuxerOptions& options) { void InitializeSegmenter(const MuxerOptions& options) {
@ -186,8 +189,8 @@ TEST_F(SingleSegmentSegmenterTest, SplitsClustersOnSegment) {
ClusterParser parser; ClusterParser parser;
ASSERT_NO_FATAL_FAILURE(parser.PopulateFromSegment(OutputFileName())); ASSERT_NO_FATAL_FAILURE(parser.PopulateFromSegment(OutputFileName()));
ASSERT_EQ(2u, parser.cluster_count()); ASSERT_EQ(2u, parser.cluster_count());
EXPECT_EQ(5, parser.GetFrameCountForCluster(0)); EXPECT_EQ(5u, parser.GetFrameCountForCluster(0));
EXPECT_EQ(3, parser.GetFrameCountForCluster(1)); EXPECT_EQ(3u, parser.GetFrameCountForCluster(1));
} }
TEST_F(SingleSegmentSegmenterTest, IgnoresSubsegment) { TEST_F(SingleSegmentSegmenterTest, IgnoresSubsegment) {
@ -209,7 +212,79 @@ TEST_F(SingleSegmentSegmenterTest, IgnoresSubsegment) {
ClusterParser parser; ClusterParser parser;
ASSERT_NO_FATAL_FAILURE(parser.PopulateFromSegment(OutputFileName())); ASSERT_NO_FATAL_FAILURE(parser.PopulateFromSegment(OutputFileName()));
ASSERT_EQ(1u, parser.cluster_count()); ASSERT_EQ(1u, parser.cluster_count());
EXPECT_EQ(8, parser.GetFrameCountForCluster(0)); EXPECT_EQ(8u, parser.GetFrameCountForCluster(0));
}
TEST_F(SingleSegmentSegmenterTest, LargeTimestamp) {
MuxerOptions options = CreateMuxerOptions();
ASSERT_NO_FATAL_FAILURE(InitializeSegmenter(options));
// 3 hrs. It will overflow int64_t if multiplied by kSecondsToNs.
const int64_t kLargeTimestamp = 3ll * 3600 * kTimeScale;
set_cur_timestamp(kLargeTimestamp);
// Write the samples to the Segmenter.
for (int i = 0; i < 5; i++) {
const SideDataFlag side_data_flag =
i == 3 ? kGenerateSideData : kNoSideData;
std::shared_ptr<MediaSample> sample =
CreateSample(kKeyFrame, kDuration, side_data_flag);
ASSERT_OK(segmenter_->AddSample(sample));
}
ASSERT_OK(segmenter_->FinalizeSegment(kLargeTimestamp, 5 * kDuration,
!kSubsegment));
ASSERT_OK(segmenter_->Finalize());
// Verify the resulting data.
ClusterParser parser;
ASSERT_NO_FATAL_FAILURE(parser.PopulateFromSegment(OutputFileName()));
ASSERT_EQ(1u, parser.cluster_count());
ASSERT_EQ(5u, parser.GetFrameCountForCluster(0));
const int64_t kClusterTimescode =
kLargeTimestamp / kTimeScale * kSecondsToNs / kTimecodeScale;
const int64_t kAdditionalTimecodePerSample =
kDuration / kTimeScale * kSecondsToNs / kTimecodeScale;
for (int i = 0; i < 5; i++) {
EXPECT_EQ(kClusterTimescode + kAdditionalTimecodePerSample * i,
parser.GetFrameTimecode(0, i));
}
}
TEST_F(SingleSegmentSegmenterTest, ReallyLargeTimestamp) {
MuxerOptions options = CreateMuxerOptions();
ASSERT_NO_FATAL_FAILURE(InitializeSegmenter(options));
// 10 years.
const int64_t kReallyLargeTimestamp = 10ll * 365 * 24 * 3600 * kTimeScale;
set_cur_timestamp(kReallyLargeTimestamp);
// Write the samples to the Segmenter.
for (int i = 0; i < 5; i++) {
const SideDataFlag side_data_flag =
i == 3 ? kGenerateSideData : kNoSideData;
std::shared_ptr<MediaSample> sample =
CreateSample(kKeyFrame, kDuration, side_data_flag);
ASSERT_OK(segmenter_->AddSample(sample));
}
ASSERT_OK(segmenter_->FinalizeSegment(kReallyLargeTimestamp, 5 * kDuration,
!kSubsegment));
ASSERT_OK(segmenter_->Finalize());
// Verify the resulting data.
ClusterParser parser;
ASSERT_NO_FATAL_FAILURE(parser.PopulateFromSegment(OutputFileName()));
ASSERT_EQ(1u, parser.cluster_count());
ASSERT_EQ(5u, parser.GetFrameCountForCluster(0));
const int64_t kClusterTimescode =
kReallyLargeTimestamp / kTimeScale * kSecondsToNs / kTimecodeScale;
const int64_t kAdditionalTimecodePerSample =
kDuration / kTimeScale * kSecondsToNs / kTimecodeScale;
for (int i = 0; i < 5; i++) {
EXPECT_EQ(kClusterTimescode + kAdditionalTimecodePerSample * i,
parser.GetFrameTimecode(0, i));
}
} }
} // namespace media } // namespace media

View File

@ -105,7 +105,7 @@ void WebMMuxer::FireOnMediaEndEvent() {
const bool has_index_range = segmenter_->GetIndexRangeStartAndEnd( const bool has_index_range = segmenter_->GetIndexRangeStartAndEnd(
&index_range_start, &index_range_end); &index_range_start, &index_range_end);
const float duration_seconds = segmenter_->GetDuration(); const float duration_seconds = segmenter_->GetDurationInSeconds();
const int64_t file_size = const int64_t file_size =
File::GetFileSize(options().output_file_name.c_str()); File::GetFileSize(options().output_file_name.c_str());