Text support for MpdBuilder
Change-Id: I75e1da391356e9edfcb520941029341941c31332
This commit is contained in:
parent
c577e6132f
commit
8c53995335
|
@ -77,11 +77,19 @@ bool DashIopMpdNotifier::NotifyNewContainer(const MediaInfo& media_info,
|
||||||
std::string lang;
|
std::string lang;
|
||||||
if (media_info.has_audio_info()) {
|
if (media_info.has_audio_info()) {
|
||||||
lang = media_info.audio_info().language();
|
lang = media_info.audio_info().language();
|
||||||
|
} else if (media_info.has_text_info()) {
|
||||||
|
lang = media_info.text_info().language();
|
||||||
}
|
}
|
||||||
|
|
||||||
AdaptationSet* adaptation_set =
|
AdaptationSet* adaptation_set =
|
||||||
GetAdaptationSetForMediaInfo(media_info, content_type, lang);
|
GetAdaptationSetForMediaInfo(media_info, content_type, lang);
|
||||||
DCHECK(adaptation_set);
|
DCHECK(adaptation_set);
|
||||||
|
if (media_info.has_text_info()) {
|
||||||
|
// IOP requires all AdaptationSets to have (sub)segmentAlignment set to
|
||||||
|
// true, so carelessly set it to true.
|
||||||
|
// In practice it doesn't really make sense to adapt between text tracks.
|
||||||
|
adaptation_set->ForceSetSegmentAlignment(true);
|
||||||
|
}
|
||||||
|
|
||||||
MediaInfo adjusted_media_info(media_info);
|
MediaInfo adjusted_media_info(media_info);
|
||||||
MpdBuilder::MakePathsRelativeToMpd(output_path_, &adjusted_media_info);
|
MpdBuilder::MakePathsRelativeToMpd(output_path_, &adjusted_media_info);
|
||||||
|
|
|
@ -135,7 +135,9 @@ class DashIopMpdNotifierTest
|
||||||
const MpdOptions empty_mpd_option_;
|
const MpdOptions empty_mpd_option_;
|
||||||
const std::vector<std::string> empty_base_urls_;
|
const std::vector<std::string> empty_base_urls_;
|
||||||
|
|
||||||
// Default AdaptationSet mock.
|
// Default mocks that can be used for the tests.
|
||||||
|
// IOW, if a test only requires one instance of
|
||||||
|
// Mock{AdaptationSet,Representation}, these can be used.
|
||||||
scoped_ptr<MockAdaptationSet> default_mock_adaptation_set_;
|
scoped_ptr<MockAdaptationSet> default_mock_adaptation_set_;
|
||||||
scoped_ptr<MockRepresentation> default_mock_representation_;
|
scoped_ptr<MockRepresentation> default_mock_representation_;
|
||||||
|
|
||||||
|
@ -181,6 +183,38 @@ TEST_P(DashIopMpdNotifierTest, NotifyNewContainer) {
|
||||||
EXPECT_TRUE(notifier.Flush());
|
EXPECT_TRUE(notifier.Flush());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify that if the MediaInfo contains text information, then
|
||||||
|
// MpdBuilder::ForceSetSegmentAlignment() is called.
|
||||||
|
TEST_P(DashIopMpdNotifierTest, NotifyNewTextContainer) {
|
||||||
|
const char kTextMediaInfo[] =
|
||||||
|
"text_info {\n"
|
||||||
|
" format: 'ttml'\n"
|
||||||
|
" language: 'en'\n"
|
||||||
|
"}\n"
|
||||||
|
"container_type: CONTAINER_TEXT\n";
|
||||||
|
DashIopMpdNotifier notifier(dash_profile(), empty_mpd_option_,
|
||||||
|
empty_base_urls_, output_path_);
|
||||||
|
|
||||||
|
scoped_ptr<MockMpdBuilder> mock_mpd_builder(new MockMpdBuilder(mpd_type()));
|
||||||
|
|
||||||
|
EXPECT_CALL(*mock_mpd_builder, AddAdaptationSet(StrEq("en")))
|
||||||
|
.WillOnce(Return(default_mock_adaptation_set_.get()));
|
||||||
|
EXPECT_CALL(*default_mock_adaptation_set_, AddRole(_)).Times(0);
|
||||||
|
EXPECT_CALL(*default_mock_adaptation_set_, ForceSetSegmentAlignment(true));
|
||||||
|
EXPECT_CALL(*default_mock_adaptation_set_, AddRepresentation(_))
|
||||||
|
.WillOnce(Return(default_mock_representation_.get()));
|
||||||
|
|
||||||
|
// This is for the Flush() below but adding expectation here because the next
|
||||||
|
// lines Pass() the pointer.
|
||||||
|
EXPECT_CALL(*mock_mpd_builder, ToString(_)).WillOnce(Return(true));
|
||||||
|
|
||||||
|
uint32_t unused_container_id;
|
||||||
|
SetMpdBuilder(¬ifier, mock_mpd_builder.Pass());
|
||||||
|
EXPECT_TRUE(notifier.NotifyNewContainer(ConvertToMediaInfo(kTextMediaInfo),
|
||||||
|
&unused_container_id));
|
||||||
|
EXPECT_TRUE(notifier.Flush());
|
||||||
|
}
|
||||||
|
|
||||||
// Verify VOD NotifyNewContainer() operation works with different
|
// Verify VOD NotifyNewContainer() operation works with different
|
||||||
// MediaInfo::ProtectedContent.
|
// MediaInfo::ProtectedContent.
|
||||||
// Two AdaptationSets should be created.
|
// Two AdaptationSets should be created.
|
||||||
|
|
|
@ -16,6 +16,7 @@ message MediaInfo {
|
||||||
CONTAINER_MP4 = 1;
|
CONTAINER_MP4 = 1;
|
||||||
CONTAINER_MPEG2_TS= 2;
|
CONTAINER_MPEG2_TS= 2;
|
||||||
CONTAINER_WEBM = 3;
|
CONTAINER_WEBM = 3;
|
||||||
|
CONTAINER_TEXT = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message VideoInfo {
|
message VideoInfo {
|
||||||
|
@ -51,8 +52,14 @@ message MediaInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
message TextInfo {
|
message TextInfo {
|
||||||
|
enum TextType {
|
||||||
|
UNKNOWN = 0;
|
||||||
|
CAPTION = 1;
|
||||||
|
SUBTITLE = 2;
|
||||||
|
}
|
||||||
optional string format = 1;
|
optional string format = 1;
|
||||||
optional string language = 2;
|
optional string language = 2;
|
||||||
|
optional TextType type = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ProtectedContent {
|
message ProtectedContent {
|
||||||
|
|
|
@ -38,6 +38,7 @@ class MockAdaptationSet : public AdaptationSet {
|
||||||
MOCK_METHOD2(UpdateContentProtectionPssh,
|
MOCK_METHOD2(UpdateContentProtectionPssh,
|
||||||
void(const std::string& drm_uuid, const std::string& pssh));
|
void(const std::string& drm_uuid, const std::string& pssh));
|
||||||
MOCK_METHOD1(AddRole, void(AdaptationSet::Role role));
|
MOCK_METHOD1(AddRole, void(AdaptationSet::Role role));
|
||||||
|
MOCK_METHOD1(ForceSetSegmentAlignment, void(bool segment_alignment));
|
||||||
|
|
||||||
MOCK_METHOD1(SetGroup, void(int group_number));
|
MOCK_METHOD1(SetGroup, void(int group_number));
|
||||||
MOCK_CONST_METHOD0(Group, int());
|
MOCK_CONST_METHOD0(Group, int());
|
||||||
|
|
|
@ -39,6 +39,22 @@ namespace {
|
||||||
|
|
||||||
const int kAdaptationSetGroupNotSet = -1;
|
const int kAdaptationSetGroupNotSet = -1;
|
||||||
|
|
||||||
|
AdaptationSet::Role MediaInfoTextTypeToRole(MediaInfo::TextInfo::TextType type) {
|
||||||
|
switch (type) {
|
||||||
|
case MediaInfo::TextInfo::UNKNOWN:
|
||||||
|
LOG(WARNING) << "Unknown text type, assuming subtitle.";
|
||||||
|
return AdaptationSet::kRoleSubtitle;
|
||||||
|
case MediaInfo::TextInfo::CAPTION:
|
||||||
|
return AdaptationSet::kRoleCaption;
|
||||||
|
case MediaInfo::TextInfo::SUBTITLE:
|
||||||
|
return AdaptationSet::kRoleSubtitle;
|
||||||
|
default:
|
||||||
|
NOTREACHED() << "Unknown MediaInfo TextType: " << type
|
||||||
|
<< " assuming subtitle.";
|
||||||
|
return AdaptationSet::kRoleSubtitle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::string GetMimeType(const std::string& prefix,
|
std::string GetMimeType(const std::string& prefix,
|
||||||
MediaInfo::ContainerType container_type) {
|
MediaInfo::ContainerType container_type) {
|
||||||
switch (container_type) {
|
switch (container_type) {
|
||||||
|
@ -54,7 +70,7 @@ std::string GetMimeType(const std::string& prefix,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unsupported container types should be rejected/handled by the caller.
|
// Unsupported container types should be rejected/handled by the caller.
|
||||||
NOTREACHED() << "Unrecognized container type: " << container_type;
|
LOG(ERROR) << "Unrecognized container type: " << container_type;
|
||||||
return std::string();
|
return std::string();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -684,6 +700,13 @@ Representation* AdaptationSet::AddRepresentation(const MediaInfo& media_info) {
|
||||||
content_type_ = "video";
|
content_type_ = "video";
|
||||||
} else if (media_info.has_audio_info()) {
|
} else if (media_info.has_audio_info()) {
|
||||||
content_type_ = "audio";
|
content_type_ = "audio";
|
||||||
|
} else if (media_info.has_text_info()) {
|
||||||
|
content_type_ = "text";
|
||||||
|
|
||||||
|
if (media_info.text_info().has_type() &&
|
||||||
|
(media_info.text_info().type() != MediaInfo::TextInfo::UNKNOWN)) {
|
||||||
|
roles_.insert(MediaInfoTextTypeToRole(media_info.text_info().type()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
representations_.push_back(representation.get());
|
representations_.push_back(representation.get());
|
||||||
|
@ -998,20 +1021,20 @@ Representation::Representation(
|
||||||
Representation::~Representation() {}
|
Representation::~Representation() {}
|
||||||
|
|
||||||
bool Representation::Init() {
|
bool Representation::Init() {
|
||||||
codecs_ = GetCodecs(media_info_);
|
if (!AtLeastOneTrue(media_info_.has_video_info(),
|
||||||
if (codecs_.empty()) {
|
media_info_.has_audio_info(),
|
||||||
LOG(ERROR) << "Missing codec info in MediaInfo.";
|
media_info_.has_text_info())) {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool has_video_info = media_info_.has_video_info();
|
|
||||||
const bool has_audio_info = media_info_.has_audio_info();
|
|
||||||
|
|
||||||
if (!has_video_info && !has_audio_info) {
|
|
||||||
// This is an error. Segment information can be in AdaptationSet, Period, or
|
// This is an error. Segment information can be in AdaptationSet, Period, or
|
||||||
// MPD but the interface does not provide a way to set them.
|
// MPD but the interface does not provide a way to set them.
|
||||||
// See 5.3.9.1 ISO 23009-1:2012 for segment info.
|
// See 5.3.9.1 ISO 23009-1:2012 for segment info.
|
||||||
LOG(ERROR) << "Representation needs video or audio.";
|
LOG(ERROR) << "Representation needs one of video, audio, or text.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MoreThanOneTrue(media_info_.has_video_info(),
|
||||||
|
media_info_.has_audio_info(),
|
||||||
|
media_info_.has_text_info())) {
|
||||||
|
LOG(ERROR) << "Only one of VideoInfo, AudioInfo, or TextInfo can be set.";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1020,18 +1043,22 @@ bool Representation::Init() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For mimetypes, this checks the video and then audio. Usually when there is
|
if (media_info_.has_video_info()) {
|
||||||
// audio + video, we take video/<type>.
|
|
||||||
if (has_video_info) {
|
|
||||||
mime_type_ = GetVideoMimeType();
|
mime_type_ = GetVideoMimeType();
|
||||||
if (!HasRequiredVideoFields(media_info_.video_info())) {
|
if (!HasRequiredVideoFields(media_info_.video_info())) {
|
||||||
LOG(ERROR) << "Missing required fields to create a video Representation.";
|
LOG(ERROR) << "Missing required fields to create a video Representation.";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (has_audio_info) {
|
} else if (media_info_.has_audio_info()) {
|
||||||
mime_type_ = GetAudioMimeType();
|
mime_type_ = GetAudioMimeType();
|
||||||
|
} else if (media_info_.has_text_info()) {
|
||||||
|
mime_type_ = GetTextMimeType();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mime_type_.empty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
codecs_ = GetCodecs(media_info_);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1110,7 +1137,8 @@ xml::ScopedXmlPtr<xmlNode>::type Representation::GetXml() {
|
||||||
// Mandatory fields for Representation.
|
// Mandatory fields for Representation.
|
||||||
representation.SetId(id_);
|
representation.SetId(id_);
|
||||||
representation.SetIntegerAttribute("bandwidth", bandwidth);
|
representation.SetIntegerAttribute("bandwidth", bandwidth);
|
||||||
representation.SetStringAttribute("codecs", codecs_);
|
if (!codecs_.empty())
|
||||||
|
representation.SetStringAttribute("codecs", codecs_);
|
||||||
representation.SetStringAttribute("mimeType", mime_type_);
|
representation.SetStringAttribute("mimeType", mime_type_);
|
||||||
|
|
||||||
const bool has_video_info = media_info_.has_video_info();
|
const bool has_video_info = media_info_.has_video_info();
|
||||||
|
@ -1285,6 +1313,35 @@ std::string Representation::GetAudioMimeType() const {
|
||||||
return GetMimeType("audio", media_info_.container_type());
|
return GetMimeType("audio", media_info_.container_type());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string Representation::GetTextMimeType() const {
|
||||||
|
CHECK(media_info_.has_text_info());
|
||||||
|
if (media_info_.text_info().format() == "ttml") {
|
||||||
|
switch (media_info_.container_type()) {
|
||||||
|
case MediaInfo::CONTAINER_TEXT:
|
||||||
|
return "application/ttml+xml";
|
||||||
|
case MediaInfo::CONTAINER_MP4:
|
||||||
|
return "application/mp4";
|
||||||
|
default:
|
||||||
|
LOG(ERROR) << "Failed to determine MIME type for TTML container: "
|
||||||
|
<< media_info_.container_type();
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (media_info_.text_info().format() == "vtt") {
|
||||||
|
if (media_info_.container_type() == MediaInfo::CONTAINER_TEXT) {
|
||||||
|
return "text/vtt";
|
||||||
|
}
|
||||||
|
LOG(ERROR) << "Failed to determine MIME type for VTT container: "
|
||||||
|
<< media_info_.container_type();
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG(ERROR) << "Cannot determine MIME type for format: "
|
||||||
|
<< media_info_.text_info().format()
|
||||||
|
<< " container: " << media_info_.container_type();
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
bool Representation::GetEarliestTimestamp(double* timestamp_seconds) {
|
bool Representation::GetEarliestTimestamp(double* timestamp_seconds) {
|
||||||
DCHECK(timestamp_seconds);
|
DCHECK(timestamp_seconds);
|
||||||
|
|
||||||
|
|
|
@ -172,7 +172,8 @@ class AdaptationSet {
|
||||||
|
|
||||||
/// Create a Representation instance using @a media_info.
|
/// Create a Representation instance using @a media_info.
|
||||||
/// @param media_info is a MediaInfo object used to initialize the returned
|
/// @param media_info is a MediaInfo object used to initialize the returned
|
||||||
/// Representation instance.
|
/// Representation instance. It may contain only one of VideoInfo,
|
||||||
|
/// AudioInfo, or TextInfo, i.e. VideoInfo XOR AudioInfo XOR TextInfo.
|
||||||
/// @return On success, returns a pointer to Representation. Otherwise returns
|
/// @return On success, returns a pointer to Representation. Otherwise returns
|
||||||
/// NULL. The returned pointer is owned by the AdaptationSet instance.
|
/// NULL. The returned pointer is owned by the AdaptationSet instance.
|
||||||
virtual Representation* AddRepresentation(const MediaInfo& media_info);
|
virtual Representation* AddRepresentation(const MediaInfo& media_info);
|
||||||
|
@ -220,7 +221,7 @@ class AdaptationSet {
|
||||||
/// for the AdaptationSet.
|
/// for the AdaptationSet.
|
||||||
/// @param segment_alignment is the value used for (sub)segmentAlignment
|
/// @param segment_alignment is the value used for (sub)segmentAlignment
|
||||||
/// attribute.
|
/// attribute.
|
||||||
void ForceSetSegmentAlignment(bool segment_alignment);
|
virtual void ForceSetSegmentAlignment(bool segment_alignment);
|
||||||
|
|
||||||
/// Sets the AdaptationSet@group attribute.
|
/// Sets the AdaptationSet@group attribute.
|
||||||
/// Passing a negative value to this method will unset the attribute.
|
/// Passing a negative value to this method will unset the attribute.
|
||||||
|
@ -517,11 +518,14 @@ class Representation {
|
||||||
// strings.
|
// strings.
|
||||||
std::string GetVideoMimeType() const;
|
std::string GetVideoMimeType() const;
|
||||||
std::string GetAudioMimeType() const;
|
std::string GetAudioMimeType() const;
|
||||||
|
std::string GetTextMimeType() const;
|
||||||
|
|
||||||
// Gets the earliest, normalized segment timestamp. Returns true if
|
// Gets the earliest, normalized segment timestamp. Returns true if
|
||||||
// successful, false otherwise.
|
// successful, false otherwise.
|
||||||
bool GetEarliestTimestamp(double* timestamp_seconds);
|
bool GetEarliestTimestamp(double* timestamp_seconds);
|
||||||
|
|
||||||
|
// Init() checks that only one of VideoInfo, AudioInfo, or TextInfo is set. So
|
||||||
|
// any logic using this can assume only one set.
|
||||||
MediaInfo media_info_;
|
MediaInfo media_info_;
|
||||||
std::list<ContentProtectionElement> content_protection_elements_;
|
std::list<ContentProtectionElement> content_protection_elements_;
|
||||||
std::list<SegmentInfo> segment_infos_;
|
std::list<SegmentInfo> segment_infos_;
|
||||||
|
|
|
@ -414,6 +414,42 @@ TEST_F(CommonMpdBuilderTest, ValidMediaInfo) {
|
||||||
EXPECT_TRUE(representation->Init());
|
EXPECT_TRUE(representation->Init());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify that if VideoInfo, AudioInfo, or TextInfo is not set, Init() fails.
|
||||||
|
TEST_F(CommonMpdBuilderTest, VideoAudioTextInfoNotSet) {
|
||||||
|
const char kTestMediaInfo[] = "container_type: 1";
|
||||||
|
|
||||||
|
auto representation =
|
||||||
|
CreateRepresentation(ConvertToMediaInfo(kTestMediaInfo), MpdOptions(),
|
||||||
|
kAnyRepresentationId, NoListener());
|
||||||
|
EXPECT_FALSE(representation->Init());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that if more than one of VideoInfo, AudioInfo, or TextInfo is set,
|
||||||
|
// then Init() fails.
|
||||||
|
TEST_F(CommonMpdBuilderTest, VideoAndAudioInfoSet) {
|
||||||
|
const char kTestMediaInfo[] =
|
||||||
|
"video_info {\n"
|
||||||
|
" codec: 'avc1'\n"
|
||||||
|
" height: 480\n"
|
||||||
|
" time_scale: 10\n"
|
||||||
|
" frame_duration: 10\n"
|
||||||
|
" pixel_width: 1\n"
|
||||||
|
" pixel_height: 1\n"
|
||||||
|
"}\n"
|
||||||
|
"audio_info {\n"
|
||||||
|
" codec: 'mp4a.40.2'\n"
|
||||||
|
" sampling_frequency: 44100\n"
|
||||||
|
" time_scale: 1200\n"
|
||||||
|
" num_channels: 2\n"
|
||||||
|
"}\n"
|
||||||
|
"container_type: CONTAINER_MP4\n";
|
||||||
|
|
||||||
|
auto representation =
|
||||||
|
CreateRepresentation(ConvertToMediaInfo(kTestMediaInfo), MpdOptions(),
|
||||||
|
kAnyRepresentationId, NoListener());
|
||||||
|
EXPECT_FALSE(representation->Init());
|
||||||
|
}
|
||||||
|
|
||||||
// Verify that Representation::Init() fails if a required field is missing.
|
// Verify that Representation::Init() fails if a required field is missing.
|
||||||
TEST_F(CommonMpdBuilderTest, InvalidMediaInfo) {
|
TEST_F(CommonMpdBuilderTest, InvalidMediaInfo) {
|
||||||
// Missing width.
|
// Missing width.
|
||||||
|
@ -538,16 +574,15 @@ TEST_F(CommonMpdBuilderTest, CheckAdaptationSetVideoContentType) {
|
||||||
" pixel_width: 1\n"
|
" pixel_width: 1\n"
|
||||||
" pixel_height: 1\n"
|
" pixel_height: 1\n"
|
||||||
"}\n"
|
"}\n"
|
||||||
"container_type: 1\n";
|
"container_type: CONTAINER_MP4\n";
|
||||||
|
|
||||||
auto adaptation_set =
|
auto adaptation_set =
|
||||||
CreateAdaptationSet(kAnyAdaptationSetId, "", MpdOptions(),
|
CreateAdaptationSet(kAnyAdaptationSetId, "", MpdOptions(),
|
||||||
MpdBuilder::kStatic, &sequence_counter);
|
MpdBuilder::kStatic, &sequence_counter);
|
||||||
adaptation_set->AddRepresentation(ConvertToMediaInfo(kVideoMediaInfo));
|
adaptation_set->AddRepresentation(ConvertToMediaInfo(kVideoMediaInfo));
|
||||||
|
|
||||||
xml::ScopedXmlPtr<xmlNode>::type node_xml(adaptation_set->GetXml());
|
EXPECT_NO_FATAL_FAILURE(ExpectAttributeEqString(
|
||||||
EXPECT_NO_FATAL_FAILURE(
|
"contentType", "video", adaptation_set->GetXml().get()));
|
||||||
ExpectAttributeEqString("contentType", "video", node_xml.get()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify that content type is set correctly if audio info is present in
|
// Verify that content type is set correctly if audio info is present in
|
||||||
|
@ -561,40 +596,100 @@ TEST_F(CommonMpdBuilderTest, CheckAdaptationSetAudioContentType) {
|
||||||
" time_scale: 1200\n"
|
" time_scale: 1200\n"
|
||||||
" num_channels: 2\n"
|
" num_channels: 2\n"
|
||||||
"}\n"
|
"}\n"
|
||||||
"container_type: 1\n";
|
"container_type: CONTAINER_MP4\n";
|
||||||
|
|
||||||
auto adaptation_set =
|
auto adaptation_set =
|
||||||
CreateAdaptationSet(kAnyAdaptationSetId, "", MpdOptions(),
|
CreateAdaptationSet(kAnyAdaptationSetId, "", MpdOptions(),
|
||||||
MpdBuilder::kStatic, &sequence_counter);
|
MpdBuilder::kStatic, &sequence_counter);
|
||||||
adaptation_set->AddRepresentation(ConvertToMediaInfo(kAudioMediaInfo));
|
adaptation_set->AddRepresentation(ConvertToMediaInfo(kAudioMediaInfo));
|
||||||
|
|
||||||
xml::ScopedXmlPtr<xmlNode>::type node_xml(adaptation_set->GetXml());
|
EXPECT_NO_FATAL_FAILURE(ExpectAttributeEqString(
|
||||||
EXPECT_NO_FATAL_FAILURE(
|
"contentType", "audio", adaptation_set->GetXml().get()));
|
||||||
ExpectAttributeEqString("contentType", "audio", node_xml.get()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify that content type is set correctly if text info is present in
|
// Verify that content type is set correctly if text info is present in
|
||||||
// MediaInfo.
|
// MediaInfo.
|
||||||
// TODO(rkuroiwa): Enable this once text support is implemented.
|
TEST_F(CommonMpdBuilderTest, CheckAdaptationSetTextContentType) {
|
||||||
// This fails because it fails to get the codec, therefore Representation
|
|
||||||
// creation fails.
|
|
||||||
TEST_F(CommonMpdBuilderTest, DISABLED_CheckAdaptationSetTextContentType) {
|
|
||||||
base::AtomicSequenceNumber sequence_counter;
|
base::AtomicSequenceNumber sequence_counter;
|
||||||
const char kTextMediaInfo[] =
|
const char kTextMediaInfo[] =
|
||||||
"text_info {\n"
|
"text_info {\n"
|
||||||
" format: 'ttml'\n"
|
" format: 'ttml'\n"
|
||||||
" language: 'en'\n"
|
" language: 'en'\n"
|
||||||
"}\n"
|
"}\n"
|
||||||
"container_type: 1\n";
|
"container_type: CONTAINER_TEXT\n";
|
||||||
|
|
||||||
auto adaptation_set =
|
auto adaptation_set =
|
||||||
CreateAdaptationSet(kAnyAdaptationSetId, "", MpdOptions(),
|
CreateAdaptationSet(kAnyAdaptationSetId, "", MpdOptions(),
|
||||||
MpdBuilder::kStatic, &sequence_counter);
|
MpdBuilder::kStatic, &sequence_counter);
|
||||||
adaptation_set->AddRepresentation(ConvertToMediaInfo(kTextMediaInfo));
|
adaptation_set->AddRepresentation(ConvertToMediaInfo(kTextMediaInfo));
|
||||||
|
|
||||||
|
EXPECT_NO_FATAL_FAILURE(ExpectAttributeEqString(
|
||||||
|
"contentType", "text", adaptation_set->GetXml().get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(CommonMpdBuilderTest, TtmlXmlMimeType) {
|
||||||
|
const char kTtmlXmlMediaInfo[] =
|
||||||
|
"text_info {\n"
|
||||||
|
" format: 'ttml'\n"
|
||||||
|
"}\n"
|
||||||
|
"container_type: CONTAINER_TEXT\n";
|
||||||
|
|
||||||
|
auto representation =
|
||||||
|
CreateRepresentation(ConvertToMediaInfo(kTtmlXmlMediaInfo), MpdOptions(),
|
||||||
|
kAnyRepresentationId, NoListener());
|
||||||
|
EXPECT_TRUE(representation->Init());
|
||||||
|
EXPECT_NO_FATAL_FAILURE(ExpectAttributeEqString(
|
||||||
|
"mimeType", "application/ttml+xml", representation->GetXml().get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(CommonMpdBuilderTest, TtmlMp4MimeType) {
|
||||||
|
const char kTtmlMp4MediaInfo[] =
|
||||||
|
"text_info {\n"
|
||||||
|
" format: 'ttml'\n"
|
||||||
|
"}\n"
|
||||||
|
"container_type: CONTAINER_MP4\n";
|
||||||
|
|
||||||
|
auto representation =
|
||||||
|
CreateRepresentation(ConvertToMediaInfo(kTtmlMp4MediaInfo), MpdOptions(),
|
||||||
|
kAnyRepresentationId, NoListener()).Pass();
|
||||||
|
EXPECT_TRUE(representation->Init());
|
||||||
|
EXPECT_NO_FATAL_FAILURE(ExpectAttributeEqString(
|
||||||
|
"mimeType", "application/mp4", representation->GetXml().get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(CommonMpdBuilderTest, WebVttMimeType) {
|
||||||
|
const char kWebVttMediaInfo[] =
|
||||||
|
"text_info {\n"
|
||||||
|
" format: 'vtt'\n"
|
||||||
|
"}\n"
|
||||||
|
"container_type: CONTAINER_TEXT\n";
|
||||||
|
|
||||||
|
auto representation =
|
||||||
|
CreateRepresentation(ConvertToMediaInfo(kWebVttMediaInfo), MpdOptions(),
|
||||||
|
kAnyRepresentationId, NoListener()).Pass();
|
||||||
|
EXPECT_TRUE(representation->Init());
|
||||||
|
EXPECT_NO_FATAL_FAILURE(ExpectAttributeEqString(
|
||||||
|
"mimeType", "text/vtt", representation->GetXml().get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that language passed to the constructor sets the @lang field is set.
|
||||||
|
TEST_F(CommonMpdBuilderTest, CheckLanguageAttributeSet) {
|
||||||
|
base::AtomicSequenceNumber sequence_counter;
|
||||||
|
// The media info doesn't really matter as long as it is valid.
|
||||||
|
const char kTextMediaInfo[] =
|
||||||
|
"text_info {\n"
|
||||||
|
" format: 'ttml'\n"
|
||||||
|
"}\n"
|
||||||
|
"container_type: CONTAINER_TEXT\n";
|
||||||
|
|
||||||
|
auto adaptation_set =
|
||||||
|
CreateAdaptationSet(kAnyAdaptationSetId, "en", MpdOptions(),
|
||||||
|
MpdBuilder::kStatic, &sequence_counter);
|
||||||
|
adaptation_set->AddRepresentation(ConvertToMediaInfo(kTextMediaInfo));
|
||||||
|
|
||||||
xml::ScopedXmlPtr<xmlNode>::type node_xml(adaptation_set->GetXml());
|
xml::ScopedXmlPtr<xmlNode>::type node_xml(adaptation_set->GetXml());
|
||||||
EXPECT_NO_FATAL_FAILURE(
|
EXPECT_NO_FATAL_FAILURE(
|
||||||
ExpectAttributeEqString("contentType", "text", node_xml.get()));
|
ExpectAttributeEqString("lang", "en", node_xml.get()));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(CommonMpdBuilderTest, CheckAdaptationSetId) {
|
TEST_F(CommonMpdBuilderTest, CheckAdaptationSetId) {
|
||||||
|
@ -1621,6 +1716,54 @@ TEST_F(StaticMpdBuilderTest, WriteToFile) {
|
||||||
EXPECT_TRUE(DeleteFile(file_path, kNonRecursive));
|
EXPECT_TRUE(DeleteFile(file_path, kNonRecursive));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify that a text path works.
|
||||||
|
TEST_F(StaticMpdBuilderTest, Text) {
|
||||||
|
const char kTextMediaInfo[] =
|
||||||
|
"text_info {\n"
|
||||||
|
" format: 'ttml'\n"
|
||||||
|
" language: 'en'\n"
|
||||||
|
" type: SUBTITLE\n"
|
||||||
|
"}\n"
|
||||||
|
"media_duration_seconds: 35\n"
|
||||||
|
"bandwidth: 1000\n"
|
||||||
|
"media_file_name: 'subtitle.xml'\n"
|
||||||
|
"container_type: CONTAINER_TEXT\n";
|
||||||
|
|
||||||
|
const char kExpectedOutput[] =
|
||||||
|
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
||||||
|
"<MPD xmlns=\"urn:mpeg:DASH:schema:MPD:2011\""
|
||||||
|
" xmlns:cenc=\"urn:mpeg:cenc:2013\""
|
||||||
|
" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""
|
||||||
|
" xmlns:xlink=\"http://www.w3.org/1999/xlink\""
|
||||||
|
" xsi:schemaLocation=\"urn:mpeg:DASH:schema:MPD:2011 DASH-MPD.xsd\""
|
||||||
|
" minBufferTime=\"PT2S\" type=\"static\""
|
||||||
|
" profiles=\"urn:mpeg:dash:profile:isoff-on-demand:2011\""
|
||||||
|
" mediaPresentationDuration=\"PT35S\">"
|
||||||
|
" <Period>"
|
||||||
|
" <AdaptationSet id=\"0\" contentType=\"text\" lang=\"en\">"
|
||||||
|
" <Role schemeIdUri=\"urn:mpeg:dash:role:2011\""
|
||||||
|
" value=\"subtitle\"/>\n"
|
||||||
|
" <Representation id=\"0\" bandwidth=\"1000\""
|
||||||
|
" mimeType=\"application/ttml+xml\">"
|
||||||
|
" <BaseURL>subtitle.xml</BaseURL>"
|
||||||
|
" </Representation>"
|
||||||
|
" </AdaptationSet>"
|
||||||
|
" </Period>"
|
||||||
|
"</MPD>";
|
||||||
|
|
||||||
|
AdaptationSet* text_adaptation_set = mpd_.AddAdaptationSet("en");
|
||||||
|
ASSERT_TRUE(text_adaptation_set);
|
||||||
|
|
||||||
|
Representation* text_representation = text_adaptation_set->AddRepresentation(
|
||||||
|
ConvertToMediaInfo(kTextMediaInfo));
|
||||||
|
ASSERT_TRUE(text_representation);
|
||||||
|
|
||||||
|
std::string mpd_output;
|
||||||
|
ASSERT_TRUE(mpd_.ToString(&mpd_output));
|
||||||
|
ASSERT_TRUE(ValidateMpdSchema(mpd_output));
|
||||||
|
EXPECT_TRUE(XmlEqual(kExpectedOutput, mpd_output));
|
||||||
|
}
|
||||||
|
|
||||||
// Check whether the attributes are set correctly for dynamic <MPD> element.
|
// Check whether the attributes are set correctly for dynamic <MPD> element.
|
||||||
// This test must use ASSERT_EQ for comparison because XmlEqual() cannot
|
// This test must use ASSERT_EQ for comparison because XmlEqual() cannot
|
||||||
// handle namespaces correctly yet.
|
// handle namespaces correctly yet.
|
||||||
|
|
|
@ -13,6 +13,24 @@
|
||||||
#include "packager/mpd/base/xml/scoped_xml_ptr.h"
|
#include "packager/mpd/base/xml/scoped_xml_ptr.h"
|
||||||
|
|
||||||
namespace edash_packager {
|
namespace edash_packager {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::string TextCodecString(
|
||||||
|
const edash_packager::MediaInfo& media_info) {
|
||||||
|
CHECK(media_info.has_text_info());
|
||||||
|
const std::string& format = media_info.text_info().format();
|
||||||
|
// DASH IOP mentions that the codec for ttml in mp4 is stpp.
|
||||||
|
if (format == "ttml" &&
|
||||||
|
(media_info.container_type() == MediaInfo::CONTAINER_MP4)) {
|
||||||
|
return "stpp";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise codec doesn't need to be specified, e.g. vtt and ttml+xml are
|
||||||
|
// obvious from the mime type.
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
bool HasVODOnlyFields(const MediaInfo& media_info) {
|
bool HasVODOnlyFields(const MediaInfo& media_info) {
|
||||||
return media_info.has_init_range() || media_info.has_index_range() ||
|
return media_info.has_init_range() || media_info.has_index_range() ||
|
||||||
|
@ -40,22 +58,19 @@ void RemoveDuplicateAttributes(
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string GetCodecs(const MediaInfo& media_info) {
|
std::string GetCodecs(const MediaInfo& media_info) {
|
||||||
std::string video_codec;
|
CHECK(OnlyOneTrue(media_info.has_video_info(), media_info.has_audio_info(),
|
||||||
|
media_info.has_text_info()));
|
||||||
|
|
||||||
if (media_info.has_video_info())
|
if (media_info.has_video_info())
|
||||||
video_codec = media_info.video_info().codec();
|
return media_info.video_info().codec();
|
||||||
|
|
||||||
std::string audio_codec;
|
|
||||||
if (media_info.has_audio_info())
|
if (media_info.has_audio_info())
|
||||||
audio_codec = media_info.audio_info().codec();
|
return media_info.audio_info().codec();
|
||||||
|
|
||||||
if (!video_codec.empty() && !audio_codec.empty()) {
|
if (media_info.has_text_info())
|
||||||
return video_codec + "," + audio_codec;
|
return TextCodecString(media_info);
|
||||||
} else if (!video_codec.empty()) {
|
|
||||||
return video_codec;
|
|
||||||
} else if (!audio_codec.empty()) {
|
|
||||||
return audio_codec;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
NOTREACHED();
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,6 +49,8 @@ bool SimpleMpdNotifier::NotifyNewContainer(const MediaInfo& media_info,
|
||||||
std::string lang;
|
std::string lang;
|
||||||
if (media_info.has_audio_info()) {
|
if (media_info.has_audio_info()) {
|
||||||
lang = media_info.audio_info().language();
|
lang = media_info.audio_info().language();
|
||||||
|
} else if (media_info.has_text_info()) {
|
||||||
|
lang = media_info.text_info().language();
|
||||||
}
|
}
|
||||||
AdaptationSet** adaptation_set = &adaptation_set_map_[content_type][lang];
|
AdaptationSet** adaptation_set = &adaptation_set_map_[content_type][lang];
|
||||||
if (*adaptation_set == NULL)
|
if (*adaptation_set == NULL)
|
||||||
|
|
Loading…
Reference in New Issue