diff --git a/packager/media/event/muxer_listener_internal.cc b/packager/media/event/muxer_listener_internal.cc index e12a5dd194..ca6cc5e7c3 100644 --- a/packager/media/event/muxer_listener_internal.cc +++ b/packager/media/event/muxer_listener_internal.cc @@ -86,6 +86,12 @@ void AddVideoInfo(const VideoStreamInfo* video_stream_info, video_info->set_height(video_stream_info->height()); video_info->set_time_scale(video_stream_info->time_scale()); + if (video_stream_info->pixel_width() > 0) + video_info->set_pixel_width(video_stream_info->pixel_width()); + + if (video_stream_info->pixel_height() > 0) + video_info->set_pixel_height(video_stream_info->pixel_height()); + const std::vector& extra_data = video_stream_info->extra_data(); if (!extra_data.empty()) { video_info->set_decoder_config(&extra_data[0], extra_data.size()); diff --git a/packager/media/event/vod_media_info_dump_muxer_listener_unittest.cc b/packager/media/event/vod_media_info_dump_muxer_listener_unittest.cc index 137cd228f6..20d37f44a9 100644 --- a/packager/media/event/vod_media_info_dump_muxer_listener_unittest.cc +++ b/packager/media/event/vod_media_info_dump_muxer_listener_unittest.cc @@ -35,6 +35,8 @@ struct VideoStreamInfoParameters { std::string language; uint16_t width; uint16_t height; + uint32_t pixel_width; + uint32_t pixel_height; uint8_t nalu_length_size; std::vector extra_data; bool is_encrypted; @@ -63,10 +65,8 @@ scoped_refptr CreateVideoStreamInfo( param.language, param.width, param.height, - // TODO(rkuroiwa): Once MedianInfo proto change that - // adds pizel_{width,height} lands, add tests. - 0, // No pixel width. - 0, // No pixel height. + param.pixel_width, + param.pixel_height, 0, // trick_play_rate param.nalu_length_size, vector_as_array(¶m.extra_data), @@ -85,16 +85,18 @@ VideoStreamInfoParameters GetDefaultVideoStreamInfoParams() { const char* kLanuageUndefined = "und"; const uint16_t kWidth = 720; const uint16_t kHeight = 480; + const uint32_t kPixelWidth = 1; + const uint32_t kPixelHeight = 1; const uint8_t kNaluLengthSize = 1; const std::vector kExtraData; const bool kEncryptedFlag = false; VideoStreamInfoParameters param = { kTrackId, kTimeScale, kVideoStreamDuration, kH264Codec, - VideoStreamInfo::GetCodecString( - kCodecH264, kH264Profile, kH264CompatibleProfile, kH264Level), - kLanuageUndefined, kWidth, kHeight, kNaluLengthSize, kExtraData, - kEncryptedFlag}; + VideoStreamInfo::GetCodecString(kCodecH264, kH264Profile, + kH264CompatibleProfile, kH264Level), + kLanuageUndefined, kWidth, kHeight, kPixelWidth, kPixelHeight, + kNaluLengthSize, kExtraData, kEncryptedFlag}; return param; } @@ -224,10 +226,12 @@ TEST_F(VodMediaInfoDumpMuxerListenerTest, UnencryptedStream_Normal) { const char kExpectedProtobufOutput[] = "bandwidth: 7620\n" "video_info {\n" - " codec: \"avc1.010101\"\n" + " codec: 'avc1.010101'\n" " width: 720\n" " height: 480\n" " time_scale: 10\n" + " pixel_width: 1\n" + " pixel_height: 1\n" "}\n" "init_range {\n" " begin: 0\n" @@ -239,7 +243,7 @@ TEST_F(VodMediaInfoDumpMuxerListenerTest, UnencryptedStream_Normal) { "}\n" "reference_time_scale: 1000\n" "container_type: 1\n" - "media_file_name: \"test_output_file_name.mp4\"\n" + "media_file_name: 'test_output_file_name.mp4'\n" "media_duration_seconds: 10.5\n"; ASSERT_NO_FATAL_FAILURE(ExpectTempFileToEqual(kExpectedProtobufOutput)); } @@ -249,26 +253,26 @@ TEST_F(VodMediaInfoDumpMuxerListenerTest, EncryptedStream_Normal) { scoped_refptr stream_info = CreateVideoStreamInfo(GetDefaultVideoStreamInfoParams()); - FireOnMediaStartWithDefaultMuxerOptions(*stream_info, kEnableEncryption); - OnMediaEndParameters media_end_param = GetDefaultOnMediaEndParams(); FireOnMediaEndWithParams(media_end_param); const char kExpectedProtobufOutput[] = "bandwidth: 7620\n" "video_info {\n" - " codec: \"avc1.010101\"\n" + " codec: 'avc1.010101'\n" " width: 720\n" " height: 480\n" " time_scale: 10\n" + " pixel_width: 1\n" + " pixel_height: 1\n" "}\n" "content_protections {\n" - " scheme_id_uri: \"urn:mpeg:dash:mp4protection:2011\"\n" - " value: \"cenc\"\n" + " scheme_id_uri: 'urn:mpeg:dash:mp4protection:2011'\n" + " value: 'cenc'\n" "}\n" "content_protections {\n" - " scheme_id_uri: \"http://foo.com/bar\"\n" + " scheme_id_uri: 'http://foo.com/bar'\n" "}\n" "init_range {\n" " begin: 0\n" @@ -280,7 +284,44 @@ TEST_F(VodMediaInfoDumpMuxerListenerTest, EncryptedStream_Normal) { "}\n" "reference_time_scale: 1000\n" "container_type: 1\n" - "media_file_name: \"test_output_file_name.mp4\"\n" + "media_file_name: 'test_output_file_name.mp4'\n" + "media_duration_seconds: 10.5\n"; + ASSERT_NO_FATAL_FAILURE(ExpectTempFileToEqual(kExpectedProtobufOutput)); +} + +// Verify that VideoStreamInfo with non-0 pixel_{width,height} is set in the +// generated MediaInfo. +TEST_F(VodMediaInfoDumpMuxerListenerTest, CheckPixelWidthAndHeightSet) { + VideoStreamInfoParameters params = GetDefaultVideoStreamInfoParams(); + params.pixel_width = 8; + params.pixel_height = 9; + + scoped_refptr stream_info = CreateVideoStreamInfo(params); + FireOnMediaStartWithDefaultMuxerOptions(*stream_info, !kEnableEncryption); + OnMediaEndParameters media_end_param = GetDefaultOnMediaEndParams(); + FireOnMediaEndWithParams(media_end_param); + + const char kExpectedProtobufOutput[] = + "bandwidth: 7620\n" + "video_info {\n" + " codec: 'avc1.010101'\n" + " width: 720\n" + " height: 480\n" + " time_scale: 10\n" + " pixel_width: 8\n" + " pixel_height: 9\n" + "}\n" + "init_range {\n" + " begin: 0\n" + " end: 120\n" + "}\n" + "index_range {\n" + " begin: 121\n" + " end: 221\n" + "}\n" + "reference_time_scale: 1000\n" + "container_type: 1\n" + "media_file_name: 'test_output_file_name.mp4'\n" "media_duration_seconds: 10.5\n"; ASSERT_NO_FATAL_FAILURE(ExpectTempFileToEqual(kExpectedProtobufOutput)); } diff --git a/packager/mpd/base/mpd_builder.cc b/packager/mpd/base/mpd_builder.cc index b5e809097b..3f8ed98ef9 100644 --- a/packager/mpd/base/mpd_builder.cc +++ b/packager/mpd/base/mpd_builder.cc @@ -212,6 +212,65 @@ bool HasRequiredVideoFields(const MediaInfo_VideoInfo& video_info) { return true; } +// Euclidean algorithm. +// gcd(a,0) = a +// gcd(a,b) = gcd(b, a % b) +uint32_t GreatestCommonDivisor(uint32_t a, uint32_t b) { + while (b != 0) { + const uint32_t new_b = a % b; + a = b; + b = new_b; + } + return a; +} + +// Returns the picture aspect ratio string e.g. "16:9", "4:3". +std::string GetPictureAspectRatio(uint32_t width, uint32_t height, + uint32_t pixel_width, uint32_t pixel_height) { + const uint32_t scaled_width = pixel_width * width; + const uint32_t scaled_height = pixel_height * height; + const uint32_t gcd = GreatestCommonDivisor(scaled_width, scaled_height); + DCHECK_NE(gcd, 0u) << "GCD of width*pix_width (" << scaled_width + << ") and height*pix_height (" << scaled_height + << ") is 0."; + + const uint32_t par_x = scaled_width / gcd; + const uint32_t par_y = scaled_height / gcd; + return base::IntToString(par_x) + ":" + base::IntToString(par_y); +} + +// Adds an entry to picture_aspect_ratio if the size of picture_aspect_ratio is +// less than 2 and video_info has both pixel width and pixel height. +void AddPictureAspectRatio( + const MediaInfo::VideoInfo& video_info, + std::set* picture_aspect_ratio) { + // If there are more than one entries in picture_aspect_ratio, the @par + // attribute cannot be set, so skip. + if (picture_aspect_ratio->size() > 1) + return; + + if (video_info.width() == 0 || video_info.height() == 0 || + video_info.pixel_width() == 0 || video_info.pixel_height() == 0) { + // If there is even one Representation without a @sar attribute, @par cannot + // be calculated. + // Just populate the set with at least 2 bogus strings so that further call + // to this function will bail out immediately. + picture_aspect_ratio->insert("bogus"); + picture_aspect_ratio->insert("entries"); + return; + } + + const std::string par = GetPictureAspectRatio( + video_info.width(), video_info.height(), + video_info.pixel_width(), video_info.pixel_height()); + DVLOG(1) << "Setting par as: " << par + << " for video with width: " << video_info.width() + << " height: " << video_info.height() + << " pixel_width: " << video_info.pixel_width() << " pixel_height; " + << video_info.pixel_height(); + picture_aspect_ratio->insert(par); +} + // Spooky static initialization/cleanup of libxml. class LibXmlInitializer { public: @@ -536,6 +595,8 @@ Representation* AdaptationSet::AddRepresentation(const MediaInfo& media_info) { base::IntToString(video_info.time_scale()) + "/" + base::IntToString(video_info.frame_duration()); } + + AddPictureAspectRatio(video_info, &picture_aspect_ratio_); } if (media_info.has_video_info()) { @@ -600,6 +661,9 @@ xml::ScopedXmlPtr::type AdaptationSet::GetXml() { adaptation_set.SetStringAttribute("maxFrameRate", video_frame_rates_.rbegin()->second); } + + if (picture_aspect_ratio_.size() == 1) + adaptation_set.SetStringAttribute("par", *picture_aspect_ratio_.begin()); return adaptation_set.PassScopedPtr(); } diff --git a/packager/mpd/base/mpd_builder.h b/packager/mpd/base/mpd_builder.h index 110bf4f621..6176b62406 100644 --- a/packager/mpd/base/mpd_builder.h +++ b/packager/mpd/base/mpd_builder.h @@ -230,6 +230,16 @@ class AdaptationSet { // Determined by examining the MediaInfo passed to AddRepresentation(). std::string content_type_; + // This does not have to be a set, it could be a list or vector because all we + // really care is whether there is more than one entry. + // Contains one entry if all the Representations have the same picture aspect + // ratio (@par attribute for AdaptationSet). + // There will be more than one entry if there are multiple picture aspect + // ratios. + // The @par attribute should only be set if there is exactly one entry + // in this set. + std::set picture_aspect_ratio_; + DISALLOW_COPY_AND_ASSIGN(AdaptationSet); }; diff --git a/packager/mpd/base/mpd_builder_unittest.cc b/packager/mpd/base/mpd_builder_unittest.cc index d70a1429bc..8d683072bf 100644 --- a/packager/mpd/base/mpd_builder_unittest.cc +++ b/packager/mpd/base/mpd_builder_unittest.cc @@ -209,7 +209,8 @@ class SegmentTemplateTest : public DynamicMpdBuilderTest { "type=\"dynamic\" profiles=\"urn:mpeg:dash:profile:isoff-live:2011\">\n" " \n" " \n" + " frameRate=\"10/5\" contentType=\"video\"" + " par=\"3:2\">\n" " \n" @@ -292,7 +293,8 @@ class TimeShiftBufferDepthTest : public SegmentTemplateTest { "timeShiftBufferDepth=\"PT%dS\">\n" " \n" " \n" + " frameRate=\"10/2\" contentType=\"video\"" + " par=\"3:2\">\n" " \n" @@ -540,6 +542,125 @@ TEST_F(CommonMpdBuilderTest, AdapatationSetMaxFrameRate) { ExpectAttributeNotSet("frameRate", adaptation_set_xml.get())); } +// Verify that if the picture aspect ratio of all the Representations are the +// same, @par attribute is present. +TEST_F(CommonMpdBuilderTest, AdaptationSetParAllSame) { + const char k720pVideoInfo[] = + "video_info {\n" + " codec: 'avc1'\n" + " width: 1280\n" + " height: 720\n" + " time_scale: 3000\n" + " frame_duration: 100\n" + " pixel_width: 1\n" + " pixel_height: 1\n" + "}\n" + "container_type: 1\n"; + const char k1080pVideoInfo[] = + "video_info {\n" + " codec: 'avc1'\n" + " width: 1920\n" + " height: 1080\n" + " time_scale: 3000\n" + " frame_duration: 100\n" + " pixel_width: 1\n" + " pixel_height: 1\n" + "}\n" + "container_type: 1\n"; + + // Note that this has non-1 pixel width and height. + // Which makes the par 16:9. + const char k360pVideoInfo[] = + "video_info {\n" + " codec: 'avc1'\n" + " width: 720\n" + " height: 360\n" + " time_scale: 3000\n" + " frame_duration: 100\n" + " pixel_width: 8\n" + " pixel_height: 9\n" + "}\n" + "container_type: 1\n"; + + AdaptationSet* video_adaptation_set = mpd_.AddAdaptationSet(""); + ASSERT_TRUE(video_adaptation_set); + ASSERT_TRUE(video_adaptation_set->AddRepresentation( + ConvertToMediaInfo(k720pVideoInfo))); + ASSERT_TRUE(video_adaptation_set->AddRepresentation( + ConvertToMediaInfo(k1080pVideoInfo))); + ASSERT_TRUE(video_adaptation_set->AddRepresentation( + ConvertToMediaInfo(k360pVideoInfo))); + + xml::ScopedXmlPtr::type adaptation_set_xml( + video_adaptation_set->GetXml()); + EXPECT_NO_FATAL_FAILURE(ExpectAttributeEqString("par", "16:9", + adaptation_set_xml.get())); +} + +// Verify that adding Representations with different par will generate +// AdaptationSet without @par. +TEST_F(CommonMpdBuilderTest, AdaptationSetParDifferent) { + const char k16by9VideoInfo[] = + "video_info {\n" + " codec: 'avc1'\n" + " width: 1280\n" + " height: 720\n" + " time_scale: 3000\n" + " frame_duration: 100\n" + " pixel_width: 1\n" + " pixel_height: 1\n" + "}\n" + "container_type: 1\n"; + // Note that 720:360 is 2:1 where as 720p (above) is 16:9. + const char k2by1VideoInfo[] = + "video_info {\n" + " codec: 'avc1'\n" + " width: 720\n" + " height: 360\n" + " time_scale: 3000\n" + " frame_duration: 100\n" + " pixel_width: 1\n" + " pixel_height: 1\n" + "}\n" + "container_type: 1\n"; + + AdaptationSet* video_adaptation_set = mpd_.AddAdaptationSet(""); + ASSERT_TRUE(video_adaptation_set); + ASSERT_TRUE(video_adaptation_set->AddRepresentation( + ConvertToMediaInfo(k16by9VideoInfo))); + ASSERT_TRUE(video_adaptation_set->AddRepresentation( + ConvertToMediaInfo(k2by1VideoInfo))); + + xml::ScopedXmlPtr::type adaptation_set_xml( + video_adaptation_set->GetXml()); + EXPECT_NO_FATAL_FAILURE( + ExpectAttributeNotSet("par", adaptation_set_xml.get())); +} + +// Verify that adding Representation without pixel_width and pixel_height will +// not set @par. +TEST_F(CommonMpdBuilderTest, AdaptationSetParUnknown) { + const char kUknownPixelWidthAndHeight[] = + "video_info {\n" + " codec: 'avc1'\n" + " width: 1280\n" + " height: 720\n" + " time_scale: 3000\n" + " frame_duration: 100\n" + "}\n" + "container_type: 1\n"; + + AdaptationSet* video_adaptation_set = mpd_.AddAdaptationSet(""); + ASSERT_TRUE(video_adaptation_set); + ASSERT_TRUE(video_adaptation_set->AddRepresentation( + ConvertToMediaInfo(kUknownPixelWidthAndHeight))); + + xml::ScopedXmlPtr::type adaptation_set_xml( + video_adaptation_set->GetXml()); + EXPECT_NO_FATAL_FAILURE( + ExpectAttributeNotSet("par", adaptation_set_xml.get())); +} + // Catch the case where it ends up wrong if integer division is used to check // the frame rate. // IOW, A/B != C/D but when using integer division A/B == C/D. diff --git a/packager/mpd/base/xml/xml_node.cc b/packager/mpd/base/xml/xml_node.cc index d0167ae551..12bbda4a9b 100644 --- a/packager/mpd/base/xml/xml_node.cc +++ b/packager/mpd/base/xml/xml_node.cc @@ -330,13 +330,17 @@ bool RepresentationXmlNode::AddVideoInfo(const VideoInfo& video_info) { return false; } + if (video_info.has_pixel_width() && video_info.has_pixel_height()) { + SetStringAttribute("sar", base::IntToString(video_info.pixel_width()) + + ":" + + base::IntToString(video_info.pixel_height())); + } + SetIntegerAttribute("width", video_info.width()); SetIntegerAttribute("height", video_info.height()); SetStringAttribute("frameRate", base::IntToString(video_info.time_scale()) + "/" + base::IntToString(video_info.frame_duration())); - SetStringAttribute("sar", base::IntToString(video_info.pixel_width()) + ":" + - base::IntToString(video_info.pixel_height())); return true; } diff --git a/packager/mpd/test/data/audio_media_info1_video_media_info1_expected_mpd_output.txt b/packager/mpd/test/data/audio_media_info1_video_media_info1_expected_mpd_output.txt index 5ca173fe99..9b34c03422 100644 --- a/packager/mpd/test/data/audio_media_info1_video_media_info1_expected_mpd_output.txt +++ b/packager/mpd/test/data/audio_media_info1_video_media_info1_expected_mpd_output.txt @@ -1,7 +1,7 @@ - + test_output_file_name1.mp4 diff --git a/packager/mpd/test/data/dynamic_normal_mpd.txt b/packager/mpd/test/data/dynamic_normal_mpd.txt index 86fc4402c1..7d1b868cee 100644 --- a/packager/mpd/test/data/dynamic_normal_mpd.txt +++ b/packager/mpd/test/data/dynamic_normal_mpd.txt @@ -1,7 +1,7 @@ - + diff --git a/packager/mpd/test/data/language_audio_media_info_expected_output.txt b/packager/mpd/test/data/language_audio_media_info_expected_output.txt index 98c7be5a13..3b21dd5494 100644 --- a/packager/mpd/test/data/language_audio_media_info_expected_output.txt +++ b/packager/mpd/test/data/language_audio_media_info_expected_output.txt @@ -1,7 +1,7 @@ - + test_output_file_name1.mp4 diff --git a/packager/mpd/test/data/video_media_info1_expected_mpd_output.txt b/packager/mpd/test/data/video_media_info1_expected_mpd_output.txt index 50239a3174..22b68f4714 100644 --- a/packager/mpd/test/data/video_media_info1_expected_mpd_output.txt +++ b/packager/mpd/test/data/video_media_info1_expected_mpd_output.txt @@ -1,7 +1,7 @@ - + test_output_file_name1.mp4