// Copyright 2014 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 "base/file_util.h" #include "base/logging.h" #include "base/strings/string_number_conversions.h" #include "base/strings/stringprintf.h" #include "mpd/base/mpd_builder.h" #include "mpd/base/mpd_utils.h" #include "mpd/test/mpd_builder_test_helper.h" #include "mpd/test/xml_compare.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/libxml/src/include/libxml/xmlstring.h" namespace dash_packager { namespace { // Get 'id' attribute from |node|, convert it to std::string and convert it to a // number. void ExpectXmlElementIdEqual(xmlNodePtr node, uint32 id) { const char kId[] = "id"; xml::ScopedXmlPtr::type id_attribute_xml_str( xmlGetProp(node, BAD_CAST kId)); ASSERT_TRUE(id_attribute_xml_str); unsigned id_attribute_unsigned = 0; std::string id_attribute_str = reinterpret_cast(id_attribute_xml_str.get()); ASSERT_TRUE(base::StringToUint(id_attribute_str, &id_attribute_unsigned)); ASSERT_EQ(id, id_attribute_unsigned); } // Using template to support both AdaptationSet and Representation. template void CheckIdEqual(uint32 expected_id, T* node) { ASSERT_EQ(expected_id, node->id()); // Also check if the XML generated by libxml2 has the correct id attribute. xml::ScopedXmlPtr::type node_xml(node->GetXml()); ASSERT_NO_FATAL_FAILURE(ExpectXmlElementIdEqual(node_xml.get(), expected_id)); } } // namespace template class MpdBuilderTest: public ::testing::Test { public: MpdBuilderTest() : mpd_(type, MpdOptions()), representation_() {} virtual ~MpdBuilderTest() {} void CheckMpd(const std::string& expected_output_file) { std::string mpd_doc; ASSERT_TRUE(mpd_.ToString(&mpd_doc)); ASSERT_TRUE(ValidateMpdSchema(mpd_doc)); ASSERT_NO_FATAL_FAILURE( ExpectMpdToEqualExpectedOutputFile(mpd_doc, expected_output_file)); } protected: void AddRepresentation(const MediaInfo& media_info) { AdaptationSet* adaptation_set = mpd_.AddAdaptationSet(); ASSERT_TRUE(adaptation_set); Representation* representation = adaptation_set->AddRepresentation(media_info); ASSERT_TRUE(representation); representation_ = representation; } MpdBuilder mpd_; // We usually need only one representation. Representation* representation_; // Owned by |mpd_|. private: DISALLOW_COPY_AND_ASSIGN(MpdBuilderTest); }; class StaticMpdBuilderTest : public MpdBuilderTest {}; class DynamicMpdBuilderTest : public MpdBuilderTest { public: virtual ~DynamicMpdBuilderTest() {} // Anchors availabilityStartTime so that the test result doesn't depend on the // current time. virtual void SetUp() { mpd_.options_.availability_start_time = "2011-12-25T12:30:00"; } std::string GetDefaultMediaInfo() { const char kMediaInfo[] = "video_info {\n" " codec: \"avc1.010101\"\n" " width: 720\n" " height: 480\n" " time_scale: 10\n" "}\n" "reference_time_scale: %lu\n" "container_type: 1\n" "init_segment_name: \"init.mp4\"\n" "segment_template: \"$Time$.mp4\"\n"; return base::StringPrintf(kMediaInfo, DefaultTimeScale()); } uint64 DefaultTimeScale() const { return 1000; }; }; class SegmentTemplateTest : public DynamicMpdBuilderTest { public: SegmentTemplateTest() : bandwidth_estimator_(BandwidthEstimator::kUseAllBlocks) {} virtual ~SegmentTemplateTest() {} virtual void SetUp() { DynamicMpdBuilderTest::SetUp(); ASSERT_NO_FATAL_FAILURE(AddRepresentationWithDefaultMediaInfo()); } void AddSegments(uint64 start_time, uint64 duration, uint64 size, uint64 repeat) { DCHECK(representation_); const char kSElementTemplate[] = "\n"; const char kSElementTemplateWithoutR[] = "\n"; SegmentInfo s = {start_time, duration, repeat}; segment_infos_for_expected_out_.push_back(s); if (repeat == 0) { expected_s_elements_ += base::StringPrintf(kSElementTemplateWithoutR, start_time, duration); } else { expected_s_elements_ += base::StringPrintf(kSElementTemplate, start_time, duration, repeat); } for (uint64 i = 0; i < repeat + 1; ++i) { representation_->AddNewSegment(start_time, duration, size); start_time += duration; bandwidth_estimator_.AddBlock( size, static_cast(duration) / DefaultTimeScale()); } } protected: void AddRepresentationWithDefaultMediaInfo() { ASSERT_NO_FATAL_FAILURE( AddRepresentation(ConvertToMediaInfo(GetDefaultMediaInfo()))); } std::string TemplateOutputInsertSElementsAndBandwidth( const std::string& s_elements_string, uint64 bandwidth) { const char kOutputTemplate[] = "\n" "\n" " \n" " \n" " \n" " \n" " \n%s" " \n" " \n" " \n" " \n" " \n" "\n"; return base::StringPrintf( kOutputTemplate, bandwidth, s_elements_string.data()); } void CheckMpdAgainstExpectedResult() { std::string mpd_doc; ASSERT_TRUE(mpd_.ToString(&mpd_doc)); ASSERT_TRUE(ValidateMpdSchema(mpd_doc)); const std::string& expected_output = TemplateOutputInsertSElementsAndBandwidth( expected_s_elements_, bandwidth_estimator_.Estimate()); ASSERT_TRUE(XmlEqual(expected_output, mpd_doc)); } private: std::list segment_infos_for_expected_out_; std::string expected_s_elements_; BandwidthEstimator bandwidth_estimator_; }; TEST_F(StaticMpdBuilderTest, CheckAdaptationSetId) { base::AtomicSequenceNumber sequence_counter; const uint32 kAdaptationSetId = 42; AdaptationSet adaptation_set(kAdaptationSetId, &sequence_counter); ASSERT_NO_FATAL_FAILURE(CheckIdEqual(kAdaptationSetId, &adaptation_set)); } TEST_F(StaticMpdBuilderTest, CheckRepresentationId) { const MediaInfo video_media_info = GetTestMediaInfo(kFileNameVideoMediaInfo1); const uint32 kRepresentationId = 1; Representation representation(video_media_info, kRepresentationId); EXPECT_TRUE(representation.Init()); ASSERT_NO_FATAL_FAILURE(CheckIdEqual(kRepresentationId, &representation)); } // Add one video check the output. TEST_F(StaticMpdBuilderTest, Video) { MediaInfo video_media_info = GetTestMediaInfo(kFileNameVideoMediaInfo1); ASSERT_NO_FATAL_FAILURE(AddRepresentation(video_media_info)); EXPECT_NO_FATAL_FAILURE(CheckMpd(kFileNameExpectedMpdOutputVideo1)); } // Add both video and audio and check the output. TEST_F(StaticMpdBuilderTest, VideoAndAudio) { MediaInfo video_media_info = GetTestMediaInfo(kFileNameVideoMediaInfo1); MediaInfo audio_media_info = GetTestMediaInfo(kFileNameAudioMediaInfo1); // The order matters here to check against expected output. AdaptationSet* video_adaptation_set = mpd_.AddAdaptationSet(); ASSERT_TRUE(video_adaptation_set); AdaptationSet* audio_adaptation_set = mpd_.AddAdaptationSet(); ASSERT_TRUE(audio_adaptation_set); Representation* audio_representation = audio_adaptation_set->AddRepresentation(audio_media_info); ASSERT_TRUE(audio_representation); Representation* video_representation = video_adaptation_set->AddRepresentation(video_media_info); ASSERT_TRUE(video_representation); EXPECT_NO_FATAL_FAILURE(CheckMpd(kFileNameExpectedMpdOutputAudio1AndVideo1)); } // MPD schema has strict ordering. AudioChannelConfiguration must appear before // ContentProtection. TEST_F(StaticMpdBuilderTest, AudioChannelConfigurationWithContentProtection) { MediaInfo encrypted_audio_media_info = GetTestMediaInfo(kFileNameEncytpedAudioMediaInfo); AdaptationSet* audio_adaptation_set = mpd_.AddAdaptationSet(); ASSERT_TRUE(audio_adaptation_set); Representation* audio_representation = audio_adaptation_set->AddRepresentation(encrypted_audio_media_info); ASSERT_TRUE(audio_representation); EXPECT_NO_FATAL_FAILURE(CheckMpd(kFileNameExpectedMpdOutputEncryptedAudio)); } // Static profile requires bandwidth to be set because it has no other way to // get the bandwidth for the Representation. TEST_F(StaticMpdBuilderTest, MediaInfoMissingBandwidth) { MediaInfo video_media_info = GetTestMediaInfo(kFileNameVideoMediaInfo1); video_media_info.clear_bandwidth(); AddRepresentation(video_media_info); std::string mpd_doc; ASSERT_FALSE(mpd_.ToString(&mpd_doc)); } // Check whether the attributes are set correctly for dynamic element. TEST_F(DynamicMpdBuilderTest, CheckMpdAttributes) { static const char kExpectedOutput[] = "\n" "\n" " \n" "\n"; std::string mpd_doc; ASSERT_TRUE(mpd_.ToString(&mpd_doc)); ASSERT_EQ(kExpectedOutput, mpd_doc); } // Estimate the bandwidth given the info from AddNewSegment(). TEST_F(SegmentTemplateTest, OneSegmentNormal) { const uint64 kStartTime = 0; const uint64 kDuration = 10; const uint64 kSize = 128; AddSegments(kStartTime, kDuration, kSize, 0); // TODO(rkuroiwa): Clean up the test/data directory. It's a mess. EXPECT_NO_FATAL_FAILURE(CheckMpd(kFileNameExpectedMpdOutputDynamicNormal)); } TEST_F(SegmentTemplateTest, NormalRepeatedSegmentDuration) { const uint64 kSize = 256; uint64 start_time = 0; uint64 duration = 40000; uint64 repeat = 2; AddSegments(start_time, duration, kSize, repeat); start_time += duration * (repeat + 1); duration = 54321; repeat = 0; AddSegments(start_time, duration, kSize, repeat); start_time += duration * (repeat + 1); duration = 12345; repeat = 0; AddSegments(start_time, duration, kSize, repeat); ASSERT_NO_FATAL_FAILURE(CheckMpdAgainstExpectedResult()); } TEST_F(SegmentTemplateTest, RepeatedSegmentsFromNonZeroStartTime) { const uint64 kSize = 100000; uint64 start_time = 0; uint64 duration = 100000; uint64 repeat = 2; AddSegments(start_time, duration, kSize, repeat); start_time += duration * (repeat + 1); duration = 20000; repeat = 3; AddSegments(start_time, duration, kSize, repeat); start_time += duration * (repeat + 1); duration = 32123; repeat = 3; AddSegments(start_time, duration, kSize, repeat); ASSERT_NO_FATAL_FAILURE(CheckMpdAgainstExpectedResult()); } // Segments not starting from 0. // Start time is 10. Make sure r gets set correctly. TEST_F(SegmentTemplateTest, NonZeroStartTime) { const uint64 kStartTime = 10; const uint64 kDuration = 22000; const uint64 kSize = 123456; const uint64 kRepeat = 1; AddSegments(kStartTime, kDuration, kSize, kRepeat); ASSERT_NO_FATAL_FAILURE(CheckMpdAgainstExpectedResult()); } // There is a gap in the segments, but still valid. TEST_F(SegmentTemplateTest, NonContiguousLiveInfo) { const uint64 kStartTime = 10; const uint64 kDuration = 22000; const uint64 kSize = 123456; const uint64 kRepeat = 0; AddSegments(kStartTime, kDuration, kSize, kRepeat); const uint64 kStartTimeOffset = 100; AddSegments(kDuration + kStartTimeOffset, kDuration, kSize, kRepeat); ASSERT_NO_FATAL_FAILURE(CheckMpdAgainstExpectedResult()); } // Add segments out of order. Segments that start before the previous segment // cannot be added. TEST_F(SegmentTemplateTest, OutOfOrder) { const uint64 kEarlierStartTime = 0; const uint64 kLaterStartTime = 1000; const uint64 kDuration = 1000; const uint64 kSize = 123456; const uint64 kRepeat = 0; AddSegments(kLaterStartTime, kDuration, kSize, kRepeat); EXPECT_DEBUG_DEATH(AddSegments(kEarlierStartTime, kDuration, kSize, kRepeat), ""); ASSERT_NO_FATAL_FAILURE(CheckMpdAgainstExpectedResult()); } // No segments should be overlapping. TEST_F(SegmentTemplateTest, OverlappingSegments) { const uint64 kEarlierStartTime = 0; const uint64 kDuration = 1000; const uint64 kSize = 123456; const uint64 kRepeat = 0; const uint64 kOverlappingSegmentStartTime = kDuration / 2; CHECK_GT(kDuration, kOverlappingSegmentStartTime); AddSegments(kEarlierStartTime, kDuration, kSize, kRepeat); EXPECT_DEBUG_DEATH( AddSegments(kOverlappingSegmentStartTime, kDuration, kSize, kRepeat), ""); ASSERT_NO_FATAL_FAILURE(CheckMpdAgainstExpectedResult()); } // Some segments can be overlapped due to rounding errors. As long as it falls // in the range of rounding error defined inside MpdBuilder, the segment gets // accepted. TEST_F(SegmentTemplateTest, OverlappingSegmentsWithinErrorRange) { const uint64 kEarlierStartTime = 0; const uint64 kDuration = 1000; const uint64 kSize = 123456; const uint64 kRepeat = 0; const uint64 kOverlappingSegmentStartTime = kDuration - 1; CHECK_GT(kDuration, kOverlappingSegmentStartTime); AddSegments(kEarlierStartTime, kDuration, kSize, kRepeat); AddSegments(kOverlappingSegmentStartTime, kDuration, kSize, kRepeat); ASSERT_NO_FATAL_FAILURE(CheckMpdAgainstExpectedResult()); } } // namespace dash_packager