| /* |
| * Copyright 2012 Google Inc. All Rights Reserved. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| #include "media/filters/shell_mp4_map.h" |
| |
| #include <stdlib.h> // for rand and srand |
| |
| #include <algorithm> // for std::min |
| #include <set> |
| #include <sstream> |
| #include <vector> |
| |
| #include "lb_platform.h" |
| #include "media/base/shell_buffer_factory.h" |
| #include "media/base/mock_shell_data_source_reader.h" |
| #include "media/filters/shell_mp4_parser.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| using ::LB::Platform::load_uint32_big_endian; |
| using ::LB::Platform::store_uint32_big_endian; |
| using ::LB::Platform::store_uint64_big_endian; |
| |
| using ::media::kAtomType_co64; |
| using ::media::kAtomType_ctts; |
| using ::media::kAtomType_stco; |
| using ::media::kAtomType_stsc; |
| using ::media::kAtomType_stss; |
| using ::media::kAtomType_stsz; |
| using ::media::kAtomType_stts; |
| using ::media::kEntrySize_co64; |
| using ::media::kEntrySize_ctts; |
| using ::media::kEntrySize_stco; |
| using ::media::kEntrySize_stsc; |
| using ::media::kEntrySize_stss; |
| using ::media::kEntrySize_stsz; |
| using ::media::kEntrySize_stts; |
| using ::media::MockShellDataSourceReader; |
| using ::media::ShellBufferFactory; |
| using ::media::ShellMP4Map; |
| |
| using ::testing::_; |
| using ::testing::AllOf; |
| using ::testing::AnyNumber; |
| using ::testing::DoAll; |
| using ::testing::Ge; |
| using ::testing::Invoke; |
| using ::testing::Lt; |
| using ::testing::Return; |
| using ::testing::SetArrayArgument; |
| |
| namespace { |
| |
| int RandomRange(int min, int max) { |
| return min + rand() % (max - min + 1); |
| } |
| |
| // Data structure represent a sample inside stbl. It has redundant data for |
| // easy access. |
| struct Sample { |
| bool is_key_frame; |
| int size; |
| int offset; |
| int chunk_index; |
| int dts_duration; |
| int dts; |
| int cts; |
| }; |
| |
| typedef std::vector<Sample> SampleVector; |
| |
| class SampleTable { |
| public: |
| // All ranges are inclusive at both ends. |
| // Set range of composition timestamp to [0, 0] to disable ctts. |
| SampleTable(unsigned int seed, |
| int num_of_samples, |
| int min_sample_size, |
| int max_sample_size, |
| int min_samples_per_chunk, |
| int max_samples_per_chunk, |
| int min_key_frame_gap, |
| int max_key_frame_gap, |
| int min_sample_decode_timestamp_offset, |
| int max_sample_decode_timestamp_offset, |
| int min_sample_composition_timestamp_offset, |
| int max_sample_composition_timestamp_offset) |
| : read_count_(0), read_bytes_(0) { |
| srand(seed); |
| CHECK_GT(num_of_samples, 0); |
| CHECK_GT(min_sample_size, 0); |
| CHECK_LE(min_sample_size, max_sample_size); |
| CHECK_GT(min_samples_per_chunk, 0); |
| CHECK_LE(min_samples_per_chunk, max_samples_per_chunk); |
| CHECK_GT(min_key_frame_gap, 0); |
| CHECK_LE(min_key_frame_gap, max_key_frame_gap); |
| CHECK_GT(min_sample_decode_timestamp_offset, 0); |
| CHECK_LE(min_sample_decode_timestamp_offset, |
| max_sample_decode_timestamp_offset); |
| CHECK_LE(min_sample_composition_timestamp_offset, |
| max_sample_composition_timestamp_offset); |
| |
| samples_.resize(num_of_samples); |
| |
| int remaining_sample_in_chunk = 0; |
| int current_chunk_index = -1; |
| int next_key_frame = 0; |
| |
| for (int i = 0; i < num_of_samples; ++i) { |
| samples_[i].size = RandomRange(min_sample_size, max_sample_size); |
| samples_[i].offset = |
| i == 0 ? rand() : samples_[i - 1].offset + samples_[i - 1].size; |
| samples_[i].dts = |
| i == 0 ? 0 : samples_[i - 1].dts + samples_[i - 1].dts_duration; |
| samples_[i].dts_duration = |
| RandomRange(min_sample_decode_timestamp_offset, |
| max_sample_decode_timestamp_offset); |
| ; |
| samples_[i].cts = samples_[i].dts + |
| RandomRange(min_sample_composition_timestamp_offset, |
| max_sample_composition_timestamp_offset); |
| if (!remaining_sample_in_chunk) { |
| ++current_chunk_index; |
| remaining_sample_in_chunk = |
| RandomRange(min_samples_per_chunk, max_samples_per_chunk); |
| } |
| |
| if (i >= next_key_frame) { |
| samples_[i].is_key_frame = true; |
| next_key_frame += RandomRange(min_key_frame_gap, max_key_frame_gap); |
| } else { |
| samples_[i].is_key_frame = false; |
| } |
| |
| samples_[i].chunk_index = current_chunk_index; |
| --remaining_sample_in_chunk; |
| } |
| |
| PopulateBoxes(); |
| } |
| |
| int64 GetBoxOffset(uint32 atom_type) const { |
| switch (atom_type) { |
| case kAtomType_stsz: |
| return stsz_offset_; |
| case kAtomType_stco: |
| return stco_offset_; |
| case kAtomType_co64: |
| return co64_offset_; |
| case kAtomType_stsc: |
| return stsc_offset_; |
| case kAtomType_ctts: |
| return ctts_offset_; |
| case kAtomType_stts: |
| return stts_offset_; |
| case kAtomType_stss: |
| return stss_offset_; |
| default: |
| NOTREACHED(); |
| return 0; |
| } |
| } |
| |
| int64 GetBoxSize(uint32 atom_type) const { |
| switch (atom_type) { |
| case kAtomType_stsz: |
| return stsz_.size(); |
| case kAtomType_stco: |
| return stco_.size(); |
| case kAtomType_co64: |
| return co64_.size(); |
| case kAtomType_stsc: |
| return stsc_.size(); |
| case kAtomType_ctts: |
| return ctts_.size(); |
| case kAtomType_stts: |
| return stts_.size(); |
| case kAtomType_stss: |
| return stss_.size(); |
| default: |
| NOTREACHED(); |
| return 0; |
| } |
| } |
| |
| const uint8_t* GetBoxData(uint32 atom_type) const { |
| return &combined_[0] + GetBoxOffset(atom_type) - file_offset_; |
| } |
| |
| size_t sample_count() const { return samples_.size(); } |
| const Sample& sample(int i) const { return samples_.at(i); } |
| |
| size_t keyframe_count() const { return (stss_.size() - 8) / kEntrySize_stss; } |
| |
| int BlockingRead(int64 position, int size, uint8* data) { |
| CHECK_GE(position, file_offset_); |
| CHECK_LE(position + size, file_offset_ + combined_.size()); |
| uint32 offset = position - file_offset_; |
| memcpy(data, &combined_[0] + offset, size); |
| ++read_count_; |
| read_bytes_ += size; |
| return size; |
| } |
| |
| void ClearReadStatistics() { |
| read_count_ = 0; |
| read_bytes_ = 0; |
| } |
| |
| int read_count() const { return read_count_; } |
| int read_bytes() const { return read_bytes_; } |
| |
| void Dump() const { |
| std::stringstream ss; |
| uint32 boxes[] = {kAtomType_stsz, kAtomType_stco, kAtomType_co64, |
| kAtomType_stsc, kAtomType_ctts, kAtomType_stts, |
| kAtomType_stss}; |
| const char* names[] = {"stsz", "stco", "co64", "stsc", |
| "ctts", "stts", "stss"}; |
| for (uint32 i = 0; i < sizeof(boxes) / sizeof(*boxes); ++i) { |
| ss << "\n======================== " << names[i] |
| << " ========================\n"; |
| int64 size = GetBoxSize(boxes[i]); |
| const uint8_t* data = GetBoxData(boxes[i]); |
| for (int64 j = 0; j < size; ++j) { |
| ss << static_cast<unsigned int>(data[j]) << ' '; |
| if (j != 0 && j % 32 == 0) |
| ss << '\n'; |
| } |
| } |
| LOG(INFO) << ss.str(); |
| } |
| |
| private: |
| SampleVector samples_; |
| |
| int64 file_offset_; |
| int64 stsz_offset_; |
| int64 stco_offset_; |
| int64 co64_offset_; |
| int64 stsc_offset_; |
| int64 ctts_offset_; |
| int64 stts_offset_; |
| int64 stss_offset_; |
| |
| std::vector<uint8_t> combined_; |
| std::vector<uint8_t> stsz_; |
| std::vector<uint8_t> stco_; |
| std::vector<uint8_t> co64_; |
| std::vector<uint8_t> stsc_; |
| std::vector<uint8_t> ctts_; |
| std::vector<uint8_t> stts_; |
| std::vector<uint8_t> stss_; |
| |
| int read_count_; |
| int read_bytes_; |
| |
| static void inc_uint32_big_endian(uint8_t* data) { |
| uint32 value = load_uint32_big_endian(data); |
| store_uint32_big_endian(value + 1, data); |
| } |
| |
| void PopulateBoxes() { |
| CHECK(!samples_.empty()); |
| bool all_key_frames = true; |
| bool all_ctts_offset_is_zero = true; |
| bool all_sample_has_same_size = true; |
| |
| for (SampleVector::const_iterator iter = samples_.begin(); |
| iter != samples_.end(); ++iter) { |
| if (!iter->is_key_frame) |
| all_key_frames = false; |
| if (iter->dts != iter->cts) |
| all_ctts_offset_is_zero = false; |
| if (iter->size != samples_[0].size) |
| all_sample_has_same_size = false; |
| } |
| |
| // populate the stsz box: 4 bytes flags + 4 bytes default size |
| // + 4 bytes count + size table if default size is 0 |
| if (all_sample_has_same_size) { |
| stsz_.resize(12); |
| store_uint32_big_endian(samples_[0].size, &stsz_[4]); |
| } else { |
| stsz_.resize(samples_.size() * kEntrySize_stsz + 12); |
| store_uint32_big_endian(0, &stsz_[4]); |
| |
| uint8_t* table_offset = &stsz_[12]; |
| |
| for (SampleVector::const_iterator iter = samples_.begin(); |
| iter != samples_.end(); ++iter) { |
| store_uint32_big_endian(iter->size, table_offset); |
| table_offset += kEntrySize_stsz; |
| } |
| } |
| store_uint32_big_endian(samples_.size(), &stsz_[8]); |
| |
| // populate stco, co64 and stsc |
| // stco = 4 bytes count + (4 bytes offset)* |
| // co64 = 4 bytes count + (8 bytes offset)* |
| // stsc = 4 bytes count + (4 bytes chunk index + 4 bytes sample per chunk |
| // + 4 bytes sample description index)* |
| stco_.resize(8); |
| co64_.resize(8); |
| stsc_.resize(8); |
| uint32 chunk_offset = samples_[0].offset; |
| int current_chunk_index = -1; |
| for (SampleVector::const_iterator iter = samples_.begin(); |
| iter != samples_.end(); ++iter) { |
| if (current_chunk_index != iter->chunk_index) { |
| current_chunk_index = iter->chunk_index; |
| stco_.resize(stco_.size() + kEntrySize_stco); |
| store_uint32_big_endian(chunk_offset, |
| &stco_[stco_.size() - kEntrySize_stco]); |
| co64_.resize(co64_.size() + kEntrySize_co64); |
| store_uint64_big_endian(chunk_offset, |
| &co64_[co64_.size() - kEntrySize_co64]); |
| stsc_.resize(stsc_.size() + kEntrySize_stsc); |
| store_uint32_big_endian(current_chunk_index + 1, // start from 1 |
| &stsc_[stsc_.size() - kEntrySize_stsc]); |
| } |
| inc_uint32_big_endian(&stsc_[stsc_.size() - kEntrySize_stsc + 4]); |
| chunk_offset += iter->size; |
| } |
| store_uint32_big_endian((stco_.size() - 8) / kEntrySize_stco, &stco_[4]); |
| store_uint32_big_endian((co64_.size() - 8) / kEntrySize_co64, &co64_[4]); |
| store_uint32_big_endian((stsc_.size() - 8) / kEntrySize_stsc, &stsc_[4]); |
| |
| // populate stts and ctts. |
| // stts = 4 bytes count + (4 bytes sample count + 4 bytes sample delta)* |
| // ctts = 4 bytes count + (4 bytes sample count + 4 bytes sample offset)* |
| // the offset is to stts. |
| stts_.resize(8); |
| ctts_.resize(all_ctts_offset_is_zero ? 0 : 8); |
| int32 last_stts_duration = -1; |
| int32 last_ctts_offset = samples_[0].cts - samples_[0].dts - 1; |
| |
| for (size_t i = 0; i < samples_.size(); ++i) { |
| int32 ctts_offset = samples_[i].cts - samples_[i].dts; |
| if (last_stts_duration != samples_[i].dts_duration) { |
| stts_.resize(stts_.size() + kEntrySize_stts); |
| last_stts_duration = samples_[i].dts_duration; |
| store_uint32_big_endian(last_stts_duration, &stts_[stts_.size() - 4]); |
| } |
| inc_uint32_big_endian(&stts_[stts_.size() - 8]); |
| if (!all_ctts_offset_is_zero) { |
| if (last_ctts_offset != ctts_offset) { |
| ctts_.resize(ctts_.size() + kEntrySize_ctts); |
| store_uint32_big_endian(ctts_offset, &ctts_[ctts_.size() - 4]); |
| last_ctts_offset = ctts_offset; |
| } |
| inc_uint32_big_endian(&ctts_[ctts_.size() - 8]); |
| } |
| } |
| store_uint32_big_endian((stts_.size() - 8) / kEntrySize_stts, &stts_[4]); |
| if (!all_ctts_offset_is_zero) |
| store_uint32_big_endian((ctts_.size() - 8) / kEntrySize_ctts, &ctts_[4]); |
| |
| // populate stss box |
| // stss = 4 bytes count + (4 bytes sample index)* |
| if (!all_key_frames) { |
| stss_.resize(8); |
| for (size_t i = 0; i < samples_.size(); ++i) { |
| if (samples_[i].is_key_frame) { |
| stss_.resize(stss_.size() + kEntrySize_stss); |
| store_uint32_big_endian(i + 1, |
| &stss_[stss_.size() - kEntrySize_stss]); |
| } |
| } |
| store_uint32_big_endian((stss_.size() - 8) / kEntrySize_stss, &stss_[4]); |
| } |
| |
| const int kGarbageSize = 1024; |
| std::vector<uint8_t> garbage; |
| garbage.reserve(kGarbageSize); |
| for (int i = 0; i < kGarbageSize; ++i) |
| garbage.push_back(RandomRange(0xef, 0xfe)); |
| combined_.insert(combined_.end(), garbage.begin(), garbage.end()); |
| combined_.insert(combined_.end(), stsz_.begin(), stsz_.end()); |
| combined_.insert(combined_.end(), garbage.begin(), garbage.end()); |
| combined_.insert(combined_.end(), stco_.begin(), stco_.end()); |
| combined_.insert(combined_.end(), garbage.begin(), garbage.end()); |
| combined_.insert(combined_.end(), co64_.begin(), co64_.end()); |
| combined_.insert(combined_.end(), garbage.begin(), garbage.end()); |
| combined_.insert(combined_.end(), stsc_.begin(), stsc_.end()); |
| combined_.insert(combined_.end(), garbage.begin(), garbage.end()); |
| combined_.insert(combined_.end(), ctts_.begin(), ctts_.end()); |
| combined_.insert(combined_.end(), garbage.begin(), garbage.end()); |
| combined_.insert(combined_.end(), stts_.begin(), stts_.end()); |
| combined_.insert(combined_.end(), garbage.begin(), garbage.end()); |
| combined_.insert(combined_.end(), stss_.begin(), stss_.end()); |
| combined_.insert(combined_.end(), garbage.begin(), garbage.end()); |
| |
| file_offset_ = abs(rand()); |
| stsz_offset_ = file_offset_ + kGarbageSize; |
| stco_offset_ = stsz_offset_ + stsz_.size() + kGarbageSize; |
| co64_offset_ = stco_offset_ + stco_.size() + kGarbageSize; |
| stsc_offset_ = co64_offset_ + co64_.size() + kGarbageSize; |
| ctts_offset_ = stsc_offset_ + stsc_.size() + kGarbageSize; |
| stts_offset_ = ctts_offset_ + ctts_.size() + kGarbageSize; |
| stss_offset_ = stts_offset_ + stts_.size() + kGarbageSize; |
| } |
| }; |
| |
| class ShellMP4MapTest : public testing::Test { |
| protected: |
| ShellMP4MapTest() { |
| // we create and destroy buffer factory after each test to detect any |
| // leaked reference-counted objects or pending callbacks |
| ShellBufferFactory::Initialize(); |
| // make a new mock reader |
| reader_ = new ::testing::NiceMock<MockShellDataSourceReader>(); |
| // make a new map with a mock reader. |
| map_ = new ShellMP4Map(reader_); |
| } |
| |
| virtual ~ShellMP4MapTest() { |
| // wipe out the map or ShellBufferFactory may complain of unfreed allocs |
| DCHECK(map_->HasOneRef()); |
| map_ = NULL; |
| |
| reader_->Stop(base::Closure()); |
| DCHECK(reader_->HasOneRef()); |
| reader_ = NULL; |
| |
| ShellBufferFactory::Terminate(); |
| } |
| |
| void ResetMap() { map_ = new ShellMP4Map(reader_); } |
| |
| void CreateTestSampleTable(unsigned int seed, |
| int num_of_samples, |
| int min_sample_size, |
| int max_sample_size, |
| int min_samples_per_chunk, |
| int max_samples_per_chunk, |
| int min_key_frame_gap, |
| int max_key_frame_gap, |
| int min_sample_decode_timestamp_offset, |
| int max_sample_decode_timestamp_offset, |
| int min_sample_composition_timestamp_offset, |
| int max_sample_composition_timestamp_offset) { |
| sample_table_.reset(new SampleTable( |
| seed, num_of_samples, min_sample_size, max_sample_size, |
| min_samples_per_chunk, max_samples_per_chunk, min_key_frame_gap, |
| max_key_frame_gap, min_sample_decode_timestamp_offset, |
| max_sample_decode_timestamp_offset, |
| min_sample_composition_timestamp_offset, |
| max_sample_composition_timestamp_offset)); |
| ON_CALL(*reader_, BlockingRead(_, _, _)) |
| .WillByDefault(Invoke(sample_table_.get(), &SampleTable::BlockingRead)); |
| } |
| |
| void SetTestTable(uint32 four_cc, uint32 cache_size_entries) { |
| map_->SetAtom(four_cc, sample_table_->GetBoxOffset(four_cc), |
| sample_table_->GetBoxSize(four_cc), cache_size_entries, |
| sample_table_->GetBoxData(four_cc)); |
| } |
| |
| const Sample& GetTestSample(uint32 sample_number) const { |
| return sample_table_->sample(sample_number); |
| } |
| |
| // ==== Test Fixture Members |
| scoped_refptr<ShellMP4Map> map_; |
| scoped_refptr<MockShellDataSourceReader> reader_; |
| scoped_ptr<SampleTable> sample_table_; |
| }; |
| |
| // ==== SetAtom() Tests ======================================================== |
| /* |
| TEST_F(ShellMP4MapTest, SetAtomWithZeroDefaultSize) { |
| // SetAtom() should fail with a zero default size on an stsc. |
| NOTIMPLEMENTED(); |
| } |
| */ |
| // ==== GetSize() Tests ======================================================== |
| |
| TEST_F(ShellMP4MapTest, GetSizeWithDefaultSize) { |
| CreateTestSampleTable(100, 1000, 0xb0df00d, 0xb0df00d, 5, 10, 5, 10, 10, 20, |
| 10, 20); |
| sample_table_->ClearReadStatistics(); |
| |
| for (int i = 13; i < 21; ++i) { |
| ResetMap(); |
| SetTestTable(kAtomType_stsz, i); |
| |
| uint32 returned_size; |
| ASSERT_TRUE(map_->GetSize(0, returned_size)); |
| ASSERT_EQ(returned_size, 0xb0df00d); |
| ASSERT_FALSE(map_->GetSize(2000, returned_size)); |
| ASSERT_TRUE(map_->GetSize(2, returned_size)); |
| ASSERT_EQ(returned_size, 0xb0df00d); |
| ASSERT_TRUE(map_->GetSize(120, returned_size)); |
| ASSERT_EQ(returned_size, 0xb0df00d); |
| } |
| |
| ASSERT_EQ(sample_table_->read_count(), 0); |
| } |
| |
| TEST_F(ShellMP4MapTest, GetSizeIterationWithHugeCache) { |
| for (int max_sample_size = 10; max_sample_size < 20; ++max_sample_size) { |
| CreateTestSampleTable(200 + max_sample_size, 1000, 10, max_sample_size, 5, |
| 10, 5, 10, 10, 20, 10, 20); |
| for (int i = 1500; i < 10000; i = i * 2 + 1) { |
| ResetMap(); |
| sample_table_->ClearReadStatistics(); |
| SetTestTable(kAtomType_stsz, i); |
| |
| for (uint32 j = 0; j < sample_table_->sample_count(); j++) { |
| uint32 map_reported_size = 0; |
| ASSERT_TRUE(map_->GetSize(j, map_reported_size)); |
| uint32 table_size = GetTestSample(j).size; |
| // reported size should match table size |
| ASSERT_EQ(map_reported_size, table_size); |
| } |
| |
| // call to a sample past the size of the table should fail |
| uint32 failed_size = 0; |
| ASSERT_FALSE(map_->GetSize(sample_table_->sample_count(), failed_size)); |
| ASSERT_LE(sample_table_->read_count(), 1); |
| ASSERT_LE(sample_table_->read_bytes(), |
| sample_table_->GetBoxSize(kAtomType_stsz)); |
| } |
| } |
| } |
| |
| TEST_F(ShellMP4MapTest, GetSizeIterationTinyCache) { |
| for (int max_sample_size = 10; max_sample_size < 20; ++max_sample_size) { |
| CreateTestSampleTable(300 + max_sample_size, 1000, 10, max_sample_size, 5, |
| 10, 5, 10, 10, 20, 10, 20); |
| for (int i = 5; i < 12; ++i) { |
| ResetMap(); |
| SetTestTable(kAtomType_stsz, i); |
| sample_table_->ClearReadStatistics(); |
| for (uint32 j = 0; j < sample_table_->sample_count(); j++) { |
| uint32 map_reported_size = 0; |
| ASSERT_TRUE(map_->GetSize(j, map_reported_size)); |
| uint32 table_size = GetTestSample(j).size; |
| ASSERT_EQ(map_reported_size, table_size); |
| } |
| ASSERT_LE(sample_table_->read_count(), |
| sample_table_->sample_count() / i + 1); |
| if (sample_table_->read_count()) |
| ASSERT_LE(sample_table_->read_bytes() / sample_table_->read_count(), |
| (i + 1) * kEntrySize_stsz); |
| // call to sample past the table size should still faile |
| uint32 failed_size = 0; |
| ASSERT_FALSE(map_->GetSize(sample_table_->sample_count(), failed_size)); |
| } |
| } |
| } |
| |
| TEST_F(ShellMP4MapTest, GetSizeRandomAccess) { |
| CreateTestSampleTable(101, 2000, 20, 24, 5, 10, 5, 10, 10, 20, 10, 20); |
| for (int i = 24; i < 27; ++i) { |
| ResetMap(); |
| SetTestTable(kAtomType_stsz, i); |
| sample_table_->ClearReadStatistics(); |
| // test first sample query somewhere later in the table, sample 105 |
| uint32 map_reported_size = 0; |
| ASSERT_TRUE(map_->GetSize(i * 4 + 5, map_reported_size)); |
| uint32 table_size = GetTestSample(i * 4 + 5).size; |
| ASSERT_EQ(map_reported_size, table_size); |
| ASSERT_EQ(sample_table_->read_count(), 1); |
| ASSERT_LE(sample_table_->read_bytes(), (i + 1) * kEntrySize_stsz); |
| |
| // now jump back to sample 0 |
| sample_table_->ClearReadStatistics(); |
| ASSERT_TRUE(map_->GetSize(0, map_reported_size)); |
| table_size = GetTestSample(0).size; |
| ASSERT_EQ(map_reported_size, table_size); |
| ASSERT_EQ(sample_table_->read_count(), 1); |
| ASSERT_LE(sample_table_->read_bytes(), (i + 1) * kEntrySize_stsz); |
| |
| // now seek well past the end, this query should fail but not break |
| // subsequent queries or issue a recache |
| ASSERT_FALSE( |
| map_->GetSize(sample_table_->sample_count(), map_reported_size)); |
| |
| // a query back within the first table should not cause recache |
| ASSERT_TRUE(map_->GetSize(10, map_reported_size)); |
| table_size = GetTestSample(10).size; |
| ASSERT_EQ(map_reported_size, table_size); |
| |
| // check sample queries right on cache boundaries out-of-order |
| sample_table_->ClearReadStatistics(); |
| ASSERT_TRUE(map_->GetSize(2 * i, map_reported_size)); |
| table_size = GetTestSample(2 * i).size; |
| ASSERT_EQ(map_reported_size, table_size); |
| ASSERT_EQ(sample_table_->read_count(), 1); |
| ASSERT_TRUE(map_->GetSize(2 * i - 1, map_reported_size)); |
| table_size = GetTestSample(2 * i - 1).size; |
| ASSERT_EQ(map_reported_size, table_size); |
| ASSERT_TRUE(map_->GetSize(2 * i - 2, map_reported_size)); |
| table_size = GetTestSample(2 * i - 2).size; |
| ASSERT_EQ(map_reported_size, table_size); |
| ASSERT_EQ(sample_table_->read_count(), 2); |
| } |
| } |
| |
| // ==== GetOffset() Tests ====================================================== |
| |
| TEST_F(ShellMP4MapTest, GetOffsetIterationHugeCache) { |
| for (int coindex = 0; coindex < 2; ++coindex) { |
| CreateTestSampleTable(102 + coindex, 1000, 20, 25, 5, 10, 5, 10, 10, 20, 10, |
| 20); |
| ResetMap(); |
| SetTestTable(kAtomType_stsz, 1000); |
| SetTestTable(kAtomType_stsc, 1000); |
| SetTestTable(coindex ? kAtomType_stco : kAtomType_co64, 1000); |
| |
| // no expectations on reader_, all tables should now be in memory |
| for (uint32 i = 0; i < sample_table_->sample_count(); ++i) { |
| uint64 map_reported_offset = 0; |
| ASSERT_TRUE(map_->GetOffset(i, map_reported_offset)); |
| uint64 table_offset = GetTestSample(i).offset; |
| ASSERT_EQ(map_reported_offset, table_offset); |
| } |
| |
| // calls to sample numbers outside file range should fail non-fatally |
| uint64 failed_offset; |
| ASSERT_FALSE(map_->GetOffset(sample_table_->sample_count(), failed_offset)); |
| } |
| } |
| |
| TEST_F(ShellMP4MapTest, GetOffsetIterationTinyCache) { |
| for (int coindex = 0; coindex < 2; ++coindex) { |
| CreateTestSampleTable(103, 30, 20, 25, 5, 10, 5, 10, 10, 20, 10, 20); |
| for (int i = 1; i < 12; ++i) { |
| ResetMap(); |
| SetTestTable(kAtomType_stsz, i); |
| SetTestTable(kAtomType_stsc, i); |
| SetTestTable(coindex ? kAtomType_stco : kAtomType_co64, i); |
| |
| // iterate through all samples in range |
| for (uint32 j = 0; j < sample_table_->sample_count(); j += 2) { |
| uint64 map_reported_offset = 0; |
| ASSERT_TRUE(map_->GetOffset(j, map_reported_offset)); |
| uint64 table_offset = GetTestSample(j).offset; |
| ASSERT_EQ(map_reported_offset, table_offset); |
| } |
| |
| // calls to sample numbers outside file range should fail non-fatally |
| uint64 failed_offset; |
| ASSERT_FALSE( |
| map_->GetOffset(sample_table_->sample_count(), failed_offset)); |
| } |
| } |
| } |
| |
| // Random access within cache should just result in correct re-integration |
| // through the stsc. |
| TEST_F(ShellMP4MapTest, GetOffsetRandomAccessHugeCache) { |
| for (int coindex = 0; coindex < 2; ++coindex) { |
| CreateTestSampleTable(104, 300, 20, 25, 5, 10, 5, 10, 10, 20, 10, 20); |
| ResetMap(); |
| SetTestTable(kAtomType_stsz, 300); |
| SetTestTable(kAtomType_stsc, 300); |
| SetTestTable(coindex ? kAtomType_stco : kAtomType_co64, 300); |
| |
| for (int i = 0; i < 1000; ++i) { |
| uint32 sample_number = rand() % sample_table_->sample_count(); |
| uint64 map_reported_offset = 0; |
| ASSERT_TRUE(map_->GetOffset(sample_number, map_reported_offset)); |
| uint64 table_offset = GetTestSample(sample_number).offset; |
| ASSERT_EQ(map_reported_offset, table_offset); |
| } |
| } |
| } |
| |
| // Random access across cache boundaries should not break computation of |
| // offsets. |
| TEST_F(ShellMP4MapTest, GetOffsetRandomAccessTinyCache) { |
| for (int coindex = 0; coindex < 2; ++coindex) { |
| CreateTestSampleTable(105, 300, 20, 25, 5, 10, 5, 10, 10, 20, 10, 20); |
| ResetMap(); |
| SetTestTable(kAtomType_stsz, 7); |
| SetTestTable(kAtomType_stsc, 7); |
| SetTestTable(coindex ? kAtomType_stco : kAtomType_co64, 7); |
| |
| // calls to sample numbers outside file range should fail non-fatally |
| uint64 failed_offset; |
| ASSERT_FALSE(map_->GetOffset(sample_table_->sample_count(), failed_offset)); |
| |
| // second sample in the file |
| uint32 sample_number = 1; |
| uint64 map_reported_offset = 0; |
| ASSERT_TRUE(map_->GetOffset(sample_number, map_reported_offset)); |
| uint64 table_offset = GetTestSample(sample_number).offset; |
| ASSERT_EQ(map_reported_offset, table_offset); |
| |
| for (int i = 1; i < 15; ++i) { |
| ResetMap(); |
| SetTestTable(kAtomType_stsz, 7); |
| SetTestTable(kAtomType_stsc, 7); |
| SetTestTable(kAtomType_stco, 7); |
| |
| sample_number = sample_table_->sample_count() - i; |
| ASSERT_TRUE(map_->GetOffset(sample_number, map_reported_offset)); |
| table_offset = GetTestSample(sample_number).offset; |
| ASSERT_EQ(map_reported_offset, table_offset); |
| |
| sample_number--; |
| ASSERT_TRUE(map_->GetOffset(sample_number, map_reported_offset)); |
| table_offset = GetTestSample(sample_number).offset; |
| ASSERT_EQ(map_reported_offset, table_offset); |
| |
| // now iterate through a few samples in the middle |
| sample_number /= 2; |
| for (int j = 0; j < 40; j++) { |
| ASSERT_TRUE(map_->GetOffset(sample_number + j, map_reported_offset)); |
| table_offset = GetTestSample(sample_number + j).offset; |
| ASSERT_EQ(map_reported_offset, table_offset); |
| } |
| |
| // now iterate backwards from the same starting point |
| for (int j = 0; j < 40; j++) { |
| ASSERT_TRUE(map_->GetOffset(sample_number - j, map_reported_offset)); |
| table_offset = GetTestSample(sample_number - j).offset; |
| ASSERT_EQ(map_reported_offset, table_offset); |
| } |
| } |
| } |
| } |
| |
| TEST_F(ShellMP4MapTest, GetOffsetRandomAccessWithDefaultSize) { |
| for (int coindex = 0; coindex < 2; ++coindex) { |
| CreateTestSampleTable(106, 300, 20, 20, 5, 10, 5, 10, 10, 20, 10, 20); |
| ResetMap(); |
| SetTestTable(kAtomType_stsz, 7); |
| SetTestTable(kAtomType_stsc, 7); |
| SetTestTable(coindex ? kAtomType_stco : kAtomType_co64, 7); |
| |
| // Calculating offset of an out-of-range sample should still return an |
| // error. |
| uint64 map_reported_offset = 0; |
| ASSERT_FALSE(map_->GetOffset(sample_table_->sample_count() + 2, |
| map_reported_offset)); |
| |
| // First sample in file should still work, though. |
| ASSERT_TRUE(map_->GetOffset(0, map_reported_offset)); |
| uint64 table_offset = GetTestSample(0).offset; |
| ASSERT_EQ(map_reported_offset, table_offset); |
| |
| // Last sample should also work. |
| ASSERT_TRUE(map_->GetOffset(sample_table_->sample_count() - 1, |
| map_reported_offset)); |
| table_offset = GetTestSample(sample_table_->sample_count() - 1).offset; |
| ASSERT_EQ(map_reported_offset, table_offset); |
| |
| // Skip by 3 through the file a few times |
| for (int i = 0; i < sample_table_->sample_count(); ++i) { |
| int sample_index = (i * 3) % sample_table_->sample_count(); |
| ASSERT_TRUE(map_->GetOffset(sample_index, map_reported_offset)); |
| table_offset = GetTestSample(sample_index).offset; |
| ASSERT_EQ(map_reported_offset, table_offset); |
| } |
| } |
| } |
| |
| // ==== GetDuration() Tests ==================================================== |
| |
| TEST_F(ShellMP4MapTest, GetDurationIteration) { |
| CreateTestSampleTable(107, 60, 20, 25, 5, 10, 5, 10, 10, 20, 10, 20); |
| ResetMap(); |
| SetTestTable(kAtomType_stts, 2); |
| |
| for (uint32 i = 0; i < sample_table_->sample_count(); ++i) { |
| uint32 map_reported_duration = 0; |
| ASSERT_TRUE(map_->GetDuration(i, map_reported_duration)); |
| uint32 table_duration = GetTestSample(i).dts_duration; |
| ASSERT_EQ(map_reported_duration, table_duration); |
| } |
| |
| // entries past end of table should fail |
| uint32 failed_duration = 0; |
| ASSERT_FALSE( |
| map_->GetDuration(sample_table_->sample_count(), failed_duration)); |
| } |
| |
| TEST_F(ShellMP4MapTest, GetDurationRandomAccess) { |
| CreateTestSampleTable(108, 60, 20, 25, 5, 10, 5, 10, 10, 20, 10, 20); |
| ResetMap(); |
| SetTestTable(kAtomType_stts, 3); |
| |
| // first sample in table |
| uint32 map_reported_duration = 0; |
| ASSERT_TRUE(map_->GetDuration(0, map_reported_duration)); |
| uint32 table_duration = GetTestSample(0).dts_duration; |
| ASSERT_EQ(map_reported_duration, table_duration); |
| |
| // last sample in table |
| ASSERT_TRUE(map_->GetDuration(sample_table_->sample_count() - 1, |
| map_reported_duration)); |
| table_duration = |
| GetTestSample(sample_table_->sample_count() - 1).dts_duration; |
| ASSERT_EQ(map_reported_duration, table_duration); |
| |
| // sample just past end should fail |
| ASSERT_FALSE( |
| map_->GetDuration(sample_table_->sample_count(), map_reported_duration)); |
| |
| // but shouldn't break other sample lookups |
| ASSERT_TRUE(map_->GetDuration(2, map_reported_duration)); |
| table_duration = GetTestSample(2).dts_duration; |
| ASSERT_EQ(map_reported_duration, table_duration); |
| |
| // now iterate backwards through entire table back to first sample |
| for (int i = sample_table_->sample_count() - 1; i >= 1; i--) { |
| ASSERT_TRUE(map_->GetDuration(i, map_reported_duration)); |
| table_duration = GetTestSample(i).dts_duration; |
| ASSERT_EQ(map_reported_duration, table_duration); |
| } |
| } |
| |
| // ==== GetTimestamp() Tests =================================================== |
| |
| TEST_F(ShellMP4MapTest, GetTimestampIterationNoCompositionTime) { |
| CreateTestSampleTable(109, 60, 20, 25, 5, 10, 5, 10, 10, 20, 10, 20); |
| ResetMap(); |
| SetTestTable(kAtomType_stts, 7); |
| |
| for (uint32 i = 0; i < sample_table_->sample_count(); ++i) { |
| uint64 map_reported_timestamp = 0; |
| ASSERT_TRUE(map_->GetTimestamp(i, map_reported_timestamp)); |
| uint64 table_timestamp = GetTestSample(i).dts; |
| ASSERT_EQ(map_reported_timestamp, table_timestamp); |
| } |
| |
| // entries past end of table should fail |
| uint64 failed_timestamp = 0; |
| ASSERT_FALSE( |
| map_->GetTimestamp(sample_table_->sample_count(), failed_timestamp)); |
| } |
| |
| TEST_F(ShellMP4MapTest, GetTimestampRandomAccessNoCompositionTime) { |
| CreateTestSampleTable(110, 60, 20, 25, 5, 10, 5, 10, 10, 20, 10, 20); |
| ResetMap(); |
| SetTestTable(kAtomType_stts, 10); |
| |
| // skip by sevens through the file, seven times |
| for (int i = 0; i < sample_table_->sample_count(); ++i) { |
| uint32 sample_number = (i * 7) % sample_table_->sample_count(); |
| uint64 map_reported_timestamp = 0; |
| ASSERT_TRUE(map_->GetTimestamp(sample_number, map_reported_timestamp)); |
| uint64 table_timestamp = GetTestSample(sample_number).dts; |
| ASSERT_EQ(map_reported_timestamp, table_timestamp); |
| } |
| |
| // check a failed entry |
| uint64 failed_timestamp = 0; |
| ASSERT_FALSE( |
| map_->GetTimestamp(sample_table_->sample_count() * 2, failed_timestamp)); |
| |
| // should still be able to recover with valid input, this time skip by 21s |
| // backward through the file 21 times |
| for (int i = sample_table_->sample_count() - 1; i >= 0; i--) { |
| uint32 sample_number = (i * 21) % sample_table_->sample_count(); |
| uint64 map_reported_timestamp = 0; |
| ASSERT_TRUE(map_->GetTimestamp(sample_number, map_reported_timestamp)); |
| uint64 table_timestamp = GetTestSample(sample_number).dts; |
| ASSERT_EQ(map_reported_timestamp, table_timestamp); |
| } |
| } |
| |
| TEST_F(ShellMP4MapTest, GetTimestampIteration) { |
| CreateTestSampleTable(111, 300, 20, 25, 5, 10, 5, 10, 10, 20, 10, 20); |
| for (int i = 1; i < 20; ++i) { |
| ResetMap(); |
| SetTestTable(kAtomType_ctts, i); |
| SetTestTable(kAtomType_stts, i); |
| |
| for (int j = 0; j < sample_table_->sample_count(); ++j) { |
| uint64 map_reported_timestamp = 0; |
| ASSERT_TRUE(map_->GetTimestamp(j, map_reported_timestamp)); |
| uint64 table_timestamp = GetTestSample(j).cts; |
| ASSERT_EQ(map_reported_timestamp, table_timestamp); |
| |
| uint32 map_reported_duration = 0; |
| ASSERT_TRUE(map_->GetDuration(j, map_reported_duration)); |
| uint32 table_duration = GetTestSample(j).dts_duration; |
| ASSERT_EQ(map_reported_duration, table_duration); |
| } |
| } |
| } |
| |
| TEST_F(ShellMP4MapTest, GetTimestampRandomAccess) { |
| CreateTestSampleTable(112, 300, 20, 25, 5, 10, 5, 10, 10, 20, 10, 20); |
| for (int i = 1; i < 20; ++i) { |
| ResetMap(); |
| SetTestTable(kAtomType_ctts, i); |
| SetTestTable(kAtomType_stts, i); |
| |
| for (int j = 0; j < 100; ++j) { |
| uint32 sample_number = rand() % sample_table_->sample_count(); |
| uint64 map_reported_timestamp = 0; |
| ASSERT_TRUE(map_->GetTimestamp(sample_number, map_reported_timestamp)); |
| uint64 table_timestamp = GetTestSample(sample_number).cts; |
| ASSERT_EQ(map_reported_timestamp, table_timestamp); |
| |
| uint32 map_reported_duration = 0; |
| ASSERT_TRUE(map_->GetDuration(sample_number, map_reported_duration)); |
| uint32 table_duration = GetTestSample(sample_number).dts_duration; |
| ASSERT_EQ(map_reported_duration, table_duration); |
| } |
| } |
| } |
| |
| // ==== GetIsKeyframe() Tests ================================================== |
| |
| // the map should consider every valid sample number a keyframe without an stss |
| TEST_F(ShellMP4MapTest, GetIsKeyframeNoKeyframeTable) { |
| ResetMap(); |
| bool is_keyframe_out = false; |
| ASSERT_TRUE(map_->GetIsKeyframe(100, is_keyframe_out)); |
| ASSERT_TRUE(is_keyframe_out); |
| |
| is_keyframe_out = false; |
| ASSERT_TRUE(map_->GetIsKeyframe(5, is_keyframe_out)); |
| ASSERT_TRUE(is_keyframe_out); |
| |
| for (int i = 17; i < 174; i += 3) { |
| is_keyframe_out = false; |
| ASSERT_TRUE(map_->GetIsKeyframe(i, is_keyframe_out)); |
| ASSERT_TRUE(is_keyframe_out); |
| } |
| } |
| |
| TEST_F(ShellMP4MapTest, GetIsKeyframeIteration) { |
| CreateTestSampleTable(113, 1000, 0xb0df00d, 0xb0df00d, 5, 10, 5, 10, 10, 20, |
| 10, 20); |
| ResetMap(); |
| sample_table_->ClearReadStatistics(); |
| SetTestTable(kAtomType_stss, sample_table_->keyframe_count() / 2 + 5); |
| |
| for (uint32 i = 0; i < sample_table_->sample_count(); ++i) { |
| bool map_is_keyframe_out = false; |
| ASSERT_TRUE(map_->GetIsKeyframe(i, map_is_keyframe_out)); |
| bool table_is_keyframe = GetTestSample(i).is_key_frame; |
| ASSERT_EQ(map_is_keyframe_out, table_is_keyframe); |
| } |
| } |
| |
| TEST_F(ShellMP4MapTest, GetIsKeyframeRandomAccess) { |
| CreateTestSampleTable(114, 1000, 0xb0df00d, 0xb0df00d, 5, 10, 5, 10, 10, 20, |
| 10, 20); |
| ResetMap(); |
| sample_table_->ClearReadStatistics(); |
| SetTestTable(kAtomType_stss, sample_table_->keyframe_count() / 2 + 5); |
| |
| // pick a keyframe about halfway |
| uint32 sample_number = sample_table_->sample_count() / 2; |
| while (!GetTestSample(sample_number).is_key_frame) |
| ++sample_number; |
| // sample one past it should not be a keyframe |
| bool map_is_keyframe_out = false; |
| ASSERT_TRUE(map_->GetIsKeyframe(sample_number + 1, map_is_keyframe_out)); |
| ASSERT_FALSE(map_is_keyframe_out); |
| // sample one before keyframe should not be a keyframe either |
| ASSERT_TRUE(map_->GetIsKeyframe(sample_number - 1, map_is_keyframe_out)); |
| ASSERT_FALSE(map_is_keyframe_out); |
| // however it should be a keyframe |
| ASSERT_TRUE(map_->GetIsKeyframe(sample_number, map_is_keyframe_out)); |
| ASSERT_TRUE(map_is_keyframe_out); |
| |
| // first keyframe |
| sample_number = 0; |
| while (!GetTestSample(sample_number).is_key_frame) |
| ++sample_number; |
| // next sample should not be a keyframe |
| ASSERT_TRUE(map_->GetIsKeyframe(sample_number + 1, map_is_keyframe_out)); |
| ASSERT_FALSE(map_is_keyframe_out); |
| // but it should be |
| ASSERT_TRUE(map_->GetIsKeyframe(sample_number, map_is_keyframe_out)); |
| ASSERT_TRUE(map_is_keyframe_out); |
| |
| // iterate backwards from end of file to beginning |
| for (int i = sample_table_->sample_count() - 1; i >= 0; --i) { |
| ASSERT_TRUE(map_->GetIsKeyframe(i, map_is_keyframe_out)); |
| ASSERT_EQ(map_is_keyframe_out, GetTestSample(i).is_key_frame); |
| } |
| |
| // iterate backwards through keyframes only |
| for (int i = sample_table_->sample_count() - 1; i >= 0; --i) { |
| if (GetTestSample(i).is_key_frame) { |
| ASSERT_TRUE(map_->GetIsKeyframe(i, map_is_keyframe_out)); |
| ASSERT_TRUE(map_is_keyframe_out); |
| } |
| } |
| |
| // iterate forwards but skip all keyframes |
| for (int i = sample_table_->sample_count() - 1; i >= 0; --i) { |
| if (!GetTestSample(i).is_key_frame) { |
| ASSERT_TRUE(map_->GetIsKeyframe(i, map_is_keyframe_out)); |
| ASSERT_FALSE(map_is_keyframe_out); |
| } |
| } |
| |
| ResetMap(); |
| sample_table_->ClearReadStatistics(); |
| SetTestTable(kAtomType_stss, 7); |
| |
| // random access |
| for (int i = 0; i < 1000; ++i) { |
| sample_number = rand() % sample_table_->sample_count(); |
| ASSERT_TRUE(map_->GetIsKeyframe(sample_number, map_is_keyframe_out)); |
| ASSERT_EQ(map_is_keyframe_out, GetTestSample(sample_number).is_key_frame); |
| } |
| } |
| |
| // ==== GetKeyframe() Tests ==================================================== |
| |
| // every frame should be returned as a keyframe. This tests if our computation |
| // of timestamps => sample numbers is equivalent to sample numbers => timestamps |
| TEST_F(ShellMP4MapTest, GetKeyframeNoKeyframeTableIteration) { |
| CreateTestSampleTable(115, 30, 20, 25, 5, 10, 5, 10, 10, 20, 10, 20); |
| ResetMap(); |
| SetTestTable(kAtomType_stts, 7); |
| |
| for (int i = 0; i < sample_table_->sample_count(); ++i) { |
| // get actual timestamp and duration of this sample |
| uint64 sample_timestamp = GetTestSample(i).dts; |
| uint32 sample_duration = GetTestSample(i).dts_duration; |
| // add a bit of time to sample timestamp, but keep time within this frame |
| sample_timestamp += i % sample_duration; |
| uint32 map_keyframe = 0; |
| ASSERT_TRUE(map_->GetKeyframe(sample_timestamp, map_keyframe)); |
| ASSERT_EQ(map_keyframe, i); |
| } |
| } |
| |
| TEST_F(ShellMP4MapTest, GetKeyframeNoKeyframeTableRandomAccess) { |
| CreateTestSampleTable(116, 30, 20, 25, 5, 10, 5, 10, 10, 20, 10, 20); |
| ResetMap(); |
| SetTestTable(kAtomType_stts, 5); |
| |
| // backwards through the middle third of samples |
| for (int i = (sample_table_->sample_count() * 2) / 3; |
| i >= sample_table_->sample_count() / 3; --i) { |
| uint64 sample_timestamp = GetTestSample(i).dts; |
| uint32 sample_duration = GetTestSample(i).dts_duration; |
| sample_timestamp += sample_duration - 1 - (i % sample_duration); |
| uint32 map_keyframe = 0; |
| ASSERT_TRUE(map_->GetKeyframe(sample_timestamp, map_keyframe)); |
| ASSERT_EQ(map_keyframe, i); |
| } |
| |
| // highest valid timestamp in file |
| uint64 highest_timestamp = |
| GetTestSample(sample_table_->sample_count() - 1).dts; |
| highest_timestamp += |
| GetTestSample(sample_table_->sample_count() - 1).dts_duration - 1; |
| uint32 map_keyframe = 0; |
| ASSERT_TRUE(map_->GetKeyframe(highest_timestamp, map_keyframe)); |
| ASSERT_EQ(map_keyframe, sample_table_->sample_count() - 1); |
| |
| // lowest valid timestamp in file |
| ASSERT_TRUE(map_->GetKeyframe(0, map_keyframe)); |
| ASSERT_EQ(map_keyframe, 0); |
| |
| // should fail on higher timestamps |
| ASSERT_FALSE(map_->GetKeyframe(highest_timestamp + 1, map_keyframe)); |
| } |
| |
| // GetKeyframe is not normally called iteratively, so we test random access |
| TEST_F(ShellMP4MapTest, GetKeyframe) { |
| CreateTestSampleTable(117, 60, 20, 25, 5, 10, 5, 10, 10, 20, 10, 20); |
| ResetMap(); |
| SetTestTable(kAtomType_stss, 3); |
| SetTestTable(kAtomType_stts, 7); |
| |
| // find first keyframe in file, should be first frame |
| uint32 map_keyframe = 0; |
| ASSERT_TRUE(map_->GetKeyframe(0, map_keyframe)); |
| ASSERT_EQ(map_keyframe, 0); |
| |
| // find a first quarter keyframe in file |
| uint32 qtr_keyframe = sample_table_->sample_count() / 4; |
| while (!GetTestSample(qtr_keyframe).is_key_frame) |
| ++qtr_keyframe; |
| uint32 next_keyframe = qtr_keyframe + 1; |
| while (!GetTestSample(next_keyframe).is_key_frame) |
| ++next_keyframe; |
| uint32 prev_keyframe = qtr_keyframe - 1; |
| while (!GetTestSample(prev_keyframe).is_key_frame) |
| --prev_keyframe; |
| uint32 last_keyframe = sample_table_->sample_count() - 1; |
| while (!GetTestSample(last_keyframe).is_key_frame) |
| --last_keyframe; |
| // midway between this keyframe and the next one |
| uint32 test_frame = qtr_keyframe + ((next_keyframe - qtr_keyframe) / 2); |
| // get time for this frame |
| uint64 test_frame_timestamp = GetTestSample(test_frame).dts; |
| // get duration for this frame |
| uint32 test_frame_duration = GetTestSample(test_frame).dts_duration; |
| // midway through this frame |
| test_frame_timestamp += test_frame_duration / 2; |
| // find lower bound keyframe, should be qtr_keyframe |
| ASSERT_TRUE(map_->GetKeyframe(test_frame_timestamp, map_keyframe)); |
| ASSERT_EQ(map_keyframe, qtr_keyframe); |
| |
| // timestamp one tick before qtr_keyframe should find previous keyframe |
| test_frame_timestamp = GetTestSample(qtr_keyframe).dts - 1; |
| ASSERT_TRUE(map_->GetKeyframe(test_frame_timestamp, map_keyframe)); |
| ASSERT_EQ(map_keyframe, prev_keyframe); |
| |
| // very highest timestamp in file should return last keyframe |
| uint64 highest_timestamp = |
| GetTestSample(sample_table_->sample_count() - 1).dts; |
| highest_timestamp += |
| GetTestSample(sample_table_->sample_count() - 1).dts_duration - 1; |
| ASSERT_TRUE(map_->GetKeyframe(highest_timestamp, map_keyframe)); |
| ASSERT_EQ(map_keyframe, last_keyframe); |
| } |
| |
| } // namespace |