Text support for MpdBuilder

Change-Id: I75e1da391356e9edfcb520941029341941c31332
This commit is contained in:
Rintaro Kuroiwa 2015-10-29 13:58:36 -07:00
parent c577e6132f
commit 8c53995335
9 changed files with 316 additions and 45 deletions

View File

@ -77,11 +77,19 @@ bool DashIopMpdNotifier::NotifyNewContainer(const MediaInfo& media_info,
std::string lang;
if (media_info.has_audio_info()) {
lang = media_info.audio_info().language();
} else if (media_info.has_text_info()) {
lang = media_info.text_info().language();
}
AdaptationSet* adaptation_set =
GetAdaptationSetForMediaInfo(media_info, content_type, lang);
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);
MpdBuilder::MakePathsRelativeToMpd(output_path_, &adjusted_media_info);

View File

@ -135,7 +135,9 @@ class DashIopMpdNotifierTest
const MpdOptions empty_mpd_option_;
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<MockRepresentation> default_mock_representation_;
@ -181,6 +183,38 @@ TEST_P(DashIopMpdNotifierTest, NotifyNewContainer) {
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(&notifier, mock_mpd_builder.Pass());
EXPECT_TRUE(notifier.NotifyNewContainer(ConvertToMediaInfo(kTextMediaInfo),
&unused_container_id));
EXPECT_TRUE(notifier.Flush());
}
// Verify VOD NotifyNewContainer() operation works with different
// MediaInfo::ProtectedContent.
// Two AdaptationSets should be created.

View File

@ -16,6 +16,7 @@ message MediaInfo {
CONTAINER_MP4 = 1;
CONTAINER_MPEG2_TS= 2;
CONTAINER_WEBM = 3;
CONTAINER_TEXT = 4;
}
message VideoInfo {
@ -51,8 +52,14 @@ message MediaInfo {
}
message TextInfo {
enum TextType {
UNKNOWN = 0;
CAPTION = 1;
SUBTITLE = 2;
}
optional string format = 1;
optional string language = 2;
optional TextType type = 3;
}
message ProtectedContent {

View File

@ -38,6 +38,7 @@ class MockAdaptationSet : public AdaptationSet {
MOCK_METHOD2(UpdateContentProtectionPssh,
void(const std::string& drm_uuid, const std::string& pssh));
MOCK_METHOD1(AddRole, void(AdaptationSet::Role role));
MOCK_METHOD1(ForceSetSegmentAlignment, void(bool segment_alignment));
MOCK_METHOD1(SetGroup, void(int group_number));
MOCK_CONST_METHOD0(Group, int());

View File

@ -39,6 +39,22 @@ namespace {
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,
MediaInfo::ContainerType 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.
NOTREACHED() << "Unrecognized container type: " << container_type;
LOG(ERROR) << "Unrecognized container type: " << container_type;
return std::string();
}
@ -684,6 +700,13 @@ Representation* AdaptationSet::AddRepresentation(const MediaInfo& media_info) {
content_type_ = "video";
} else if (media_info.has_audio_info()) {
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());
@ -998,20 +1021,20 @@ Representation::Representation(
Representation::~Representation() {}
bool Representation::Init() {
codecs_ = GetCodecs(media_info_);
if (codecs_.empty()) {
LOG(ERROR) << "Missing codec info in MediaInfo.";
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) {
if (!AtLeastOneTrue(media_info_.has_video_info(),
media_info_.has_audio_info(),
media_info_.has_text_info())) {
// This is an error. Segment information can be in AdaptationSet, Period, or
// MPD but the interface does not provide a way to set them.
// 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;
}
@ -1020,18 +1043,22 @@ bool Representation::Init() {
return false;
}
// For mimetypes, this checks the video and then audio. Usually when there is
// audio + video, we take video/<type>.
if (has_video_info) {
if (media_info_.has_video_info()) {
mime_type_ = GetVideoMimeType();
if (!HasRequiredVideoFields(media_info_.video_info())) {
LOG(ERROR) << "Missing required fields to create a video Representation.";
return false;
}
} else if (has_audio_info) {
} else if (media_info_.has_audio_info()) {
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;
}
@ -1110,6 +1137,7 @@ xml::ScopedXmlPtr<xmlNode>::type Representation::GetXml() {
// Mandatory fields for Representation.
representation.SetId(id_);
representation.SetIntegerAttribute("bandwidth", bandwidth);
if (!codecs_.empty())
representation.SetStringAttribute("codecs", codecs_);
representation.SetStringAttribute("mimeType", mime_type_);
@ -1285,6 +1313,35 @@ std::string Representation::GetAudioMimeType() const {
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) {
DCHECK(timestamp_seconds);

View File

@ -172,7 +172,8 @@ class AdaptationSet {
/// Create a Representation instance using @a media_info.
/// @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
/// NULL. The returned pointer is owned by the AdaptationSet instance.
virtual Representation* AddRepresentation(const MediaInfo& media_info);
@ -220,7 +221,7 @@ class AdaptationSet {
/// for the AdaptationSet.
/// @param segment_alignment is the value used for (sub)segmentAlignment
/// attribute.
void ForceSetSegmentAlignment(bool segment_alignment);
virtual void ForceSetSegmentAlignment(bool segment_alignment);
/// Sets the AdaptationSet@group attribute.
/// Passing a negative value to this method will unset the attribute.
@ -517,11 +518,14 @@ class Representation {
// strings.
std::string GetVideoMimeType() const;
std::string GetAudioMimeType() const;
std::string GetTextMimeType() const;
// Gets the earliest, normalized segment timestamp. Returns true if
// successful, false otherwise.
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_;
std::list<ContentProtectionElement> content_protection_elements_;
std::list<SegmentInfo> segment_infos_;

View File

@ -414,6 +414,42 @@ TEST_F(CommonMpdBuilderTest, ValidMediaInfo) {
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.
TEST_F(CommonMpdBuilderTest, InvalidMediaInfo) {
// Missing width.
@ -538,16 +574,15 @@ TEST_F(CommonMpdBuilderTest, CheckAdaptationSetVideoContentType) {
" pixel_width: 1\n"
" pixel_height: 1\n"
"}\n"
"container_type: 1\n";
"container_type: CONTAINER_MP4\n";
auto adaptation_set =
CreateAdaptationSet(kAnyAdaptationSetId, "", MpdOptions(),
MpdBuilder::kStatic, &sequence_counter);
adaptation_set->AddRepresentation(ConvertToMediaInfo(kVideoMediaInfo));
xml::ScopedXmlPtr<xmlNode>::type node_xml(adaptation_set->GetXml());
EXPECT_NO_FATAL_FAILURE(
ExpectAttributeEqString("contentType", "video", node_xml.get()));
EXPECT_NO_FATAL_FAILURE(ExpectAttributeEqString(
"contentType", "video", adaptation_set->GetXml().get()));
}
// 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"
" num_channels: 2\n"
"}\n"
"container_type: 1\n";
"container_type: CONTAINER_MP4\n";
auto adaptation_set =
CreateAdaptationSet(kAnyAdaptationSetId, "", MpdOptions(),
MpdBuilder::kStatic, &sequence_counter);
adaptation_set->AddRepresentation(ConvertToMediaInfo(kAudioMediaInfo));
xml::ScopedXmlPtr<xmlNode>::type node_xml(adaptation_set->GetXml());
EXPECT_NO_FATAL_FAILURE(
ExpectAttributeEqString("contentType", "audio", node_xml.get()));
EXPECT_NO_FATAL_FAILURE(ExpectAttributeEqString(
"contentType", "audio", adaptation_set->GetXml().get()));
}
// Verify that content type is set correctly if text info is present in
// MediaInfo.
// TODO(rkuroiwa): Enable this once text support is implemented.
// This fails because it fails to get the codec, therefore Representation
// creation fails.
TEST_F(CommonMpdBuilderTest, DISABLED_CheckAdaptationSetTextContentType) {
TEST_F(CommonMpdBuilderTest, CheckAdaptationSetTextContentType) {
base::AtomicSequenceNumber sequence_counter;
const char kTextMediaInfo[] =
"text_info {\n"
" format: 'ttml'\n"
" language: 'en'\n"
"}\n"
"container_type: 1\n";
"container_type: CONTAINER_TEXT\n";
auto adaptation_set =
CreateAdaptationSet(kAnyAdaptationSetId, "", MpdOptions(),
MpdBuilder::kStatic, &sequence_counter);
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());
EXPECT_NO_FATAL_FAILURE(
ExpectAttributeEqString("contentType", "text", node_xml.get()));
ExpectAttributeEqString("lang", "en", node_xml.get()));
}
TEST_F(CommonMpdBuilderTest, CheckAdaptationSetId) {
@ -1621,6 +1716,54 @@ TEST_F(StaticMpdBuilderTest, WriteToFile) {
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.
// This test must use ASSERT_EQ for comparison because XmlEqual() cannot
// handle namespaces correctly yet.

View File

@ -13,6 +13,24 @@
#include "packager/mpd/base/xml/scoped_xml_ptr.h"
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) {
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 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())
video_codec = media_info.video_info().codec();
return media_info.video_info().codec();
std::string audio_codec;
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()) {
return video_codec + "," + audio_codec;
} else if (!video_codec.empty()) {
return video_codec;
} else if (!audio_codec.empty()) {
return audio_codec;
}
if (media_info.has_text_info())
return TextCodecString(media_info);
NOTREACHED();
return "";
}

View File

@ -49,6 +49,8 @@ bool SimpleMpdNotifier::NotifyNewContainer(const MediaInfo& media_info,
std::string lang;
if (media_info.has_audio_info()) {
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];
if (*adaptation_set == NULL)