Implement Representation::GetDurationSeconds

Also added Period::GetAdaptationSets and
AdaptationSet::GetRepresentations.

Instead of implementing GetDurationSeconds in all MpdBuilder classes
(MpdBuilder/Period/Adaptation/Representation) like what we used to do
with GetEarliestTimestamp, the two new functions allows MpdBuilder to
iterate through the Representations to get durations.

Also updates GetEarliestTimestamp functions to use the same iteration
method.

Change-Id: I682b70c07c248c0f6511ec3d9019086f986ee10e
This commit is contained in:
KongQun Yang 2018-01-04 18:31:27 -08:00
parent 509963d60f
commit ef2c424876
10 changed files with 181 additions and 119 deletions

View File

@ -387,23 +387,13 @@ void AdaptationSet::AddTrickPlayReferenceId(uint32_t id) {
trick_play_reference_ids_.insert(id);
}
bool AdaptationSet::GetEarliestTimestamp(double* timestamp_seconds) {
DCHECK(timestamp_seconds);
double earliest_timestamp(-1);
const std::list<Representation*> AdaptationSet::GetRepresentations() const {
std::list<Representation*> representations;
for (const std::unique_ptr<Representation>& representation :
representations_) {
double timestamp;
if (representation->GetEarliestTimestamp(&timestamp) &&
((earliest_timestamp < 0) || (timestamp < earliest_timestamp))) {
earliest_timestamp = timestamp;
representations.push_back(representation.get());
}
}
if (earliest_timestamp < 0)
return false;
*timestamp_seconds = earliest_timestamp;
return true;
return representations;
}
// This implementation assumes that each representations' segments' are

View File

@ -147,6 +147,9 @@ class AdaptationSet {
/// @param id the id of the reference (or main) adapation set.
virtual void AddTrickPlayReferenceId(uint32_t id);
// Return the list of Representations in this AdaptationSet.
const std::list<Representation*> GetRepresentations() const;
protected:
/// @param adaptation_set_id is an ID number for this AdaptationSet.
/// @param lang is the language of this AdaptationSet. Mainly relevant for
@ -188,10 +191,6 @@ class AdaptationSet {
// 2 -> [0, 200, 400]
typedef std::map<uint32_t, std::list<uint64_t>> RepresentationTimeline;
// Gets the earliest, normalized segment timestamp. Returns true if
// successful, false otherwise.
bool GetEarliestTimestamp(double* timestamp_seconds);
/// Called from OnNewSegmentForRepresentation(). Checks whether the segments
/// are aligned. Sets segments_aligned_.
/// This is only for Live. For VOD, CheckVodSegmentAlignment() should be used.

View File

@ -16,6 +16,7 @@
#include "packager/mpd/test/xml_compare.h"
using ::testing::Not;
using ::testing::UnorderedElementsAre;
namespace shaka {
@ -563,6 +564,43 @@ TEST_F(AdaptationSetTest, BubbleUpAttributesToAdaptationSet) {
EXPECT_THAT(no_common_attributes.get(), Not(AttributeSet("frameRate")));
}
TEST_F(AdaptationSetTest, GetRepresentations) {
const char k480pMediaInfo[] =
"video_info {\n"
" codec: 'avc1'\n"
" width: 720\n"
" height: 480\n"
" time_scale: 10\n"
" frame_duration: 10\n"
" pixel_width: 8\n"
" pixel_height: 9\n"
"}\n"
"container_type: 1\n";
const char k360pMediaInfo[] =
"video_info {\n"
" codec: 'avc1'\n"
" width: 640\n"
" height: 360\n"
" time_scale: 10\n"
" frame_duration: 10\n"
" pixel_width: 1\n"
" pixel_height: 1\n"
"}\n"
"container_type: 1\n";
auto adaptation_set = CreateAdaptationSet(kAnyAdaptationSetId, kNoLanguage);
Representation* representation_480p =
adaptation_set->AddRepresentation(ConvertToMediaInfo(k480pMediaInfo));
EXPECT_THAT(adaptation_set->GetRepresentations(),
UnorderedElementsAre(representation_480p));
Representation* representation_360p =
adaptation_set->AddRepresentation(ConvertToMediaInfo(k360pMediaInfo));
EXPECT_THAT(adaptation_set->GetRepresentations(),
UnorderedElementsAre(representation_360p, representation_480p));
}
// Verify that subsegmentAlignment is set to true if all the Representations'
// segments are aligned and the DASH profile is OnDemand.
// Also checking that not all Representations have to be added before calling
@ -961,9 +999,7 @@ TEST_F(OnDemandAdaptationSetTest,
const char kExpectedOutput[] =
"<AdaptationSet id=\"1\" contentType=\"audio\">"
" <Representation id=\"0\" bandwidth=\"195857\" codecs=\"mp4a.40.2\""
" mimeType=\"audio/mp4\" audioSamplingRate=\"44100\" "
// Temporary attribute. Will be removed when generating final mpd.
" duration=\"24.0094\">"
" mimeType=\"audio/mp4\" audioSamplingRate=\"44100\">"
" <AudioChannelConfiguration"
" schemeIdUri="
" \"urn:mpeg:dash:23003:3:audio_channel_configuration:2011\""
@ -1011,9 +1047,7 @@ TEST_F(OnDemandAdaptationSetTest, Text) {
" <Role schemeIdUri=\"urn:mpeg:dash:role:2011\""
" value=\"subtitle\"/>\n"
" <Representation id=\"0\" bandwidth=\"1000\""
" mimeType=\"application/ttml+xml\" "
// Temporary attribute. Will be removed when generating final mpd.
" duration=\"35\">"
" mimeType=\"application/ttml+xml\">"
" <BaseURL>subtitle.xml</BaseURL>"
" </Representation>"
"</AdaptationSet>";

View File

@ -16,6 +16,7 @@
#include "packager/mpd/base/adaptation_set.h"
#include "packager/mpd/base/mpd_utils.h"
#include "packager/mpd/base/period.h"
#include "packager/mpd/base/representation.h"
#include "packager/mpd/base/xml/xml_node.h"
#include "packager/version/version.h"
@ -44,28 +45,6 @@ void AddMpdNameSpaceInfo(XmlNode* mpd) {
mpd->SetStringAttribute("xmlns:cenc", kCencNamespace);
}
bool IsPeriodNode(xmlNodePtr node) {
DCHECK(node);
int kEqual = 0;
return xmlStrcmp(node->name, reinterpret_cast<const xmlChar*>("Period")) ==
kEqual;
}
// Find the first <Period> element. This does not recurse down the tree,
// only checks direct children. Returns the pointer to Period element on
// success, otherwise returns false.
// As noted here, we must traverse.
// http://www.xmlsoft.org/tutorial/ar01s04.html
xmlNodePtr FindPeriodNode(XmlNode* xml_node) {
for (xmlNodePtr node = xml_node->GetRawPtr()->xmlChildrenNode; node != NULL;
node = node->next) {
if (IsPeriodNode(node))
return node;
}
return NULL;
}
bool Positive(double d) {
return d > 0.0;
}
@ -306,44 +285,46 @@ float MpdBuilder::GetStaticMpdDuration(XmlNode* mpd_node) {
DCHECK(mpd_node);
DCHECK_EQ(MpdType::kStatic, mpd_options_.mpd_type);
if (periods_.empty()) {
LOG(WARNING) << "No Period found. Set MPD duration to 0.";
return 0.0f;
}
// Attribute mediaPresentationDuration must be present for 'static' MPD. So
// setting "PT0S" is required even if none of the representaions have duration
// attribute.
float max_duration = 0.0f;
xmlNodePtr period_node = FindPeriodNode(mpd_node);
if (!period_node) {
LOG(WARNING) << "No Period node found. Set MPD duration to 0.";
return 0.0f;
}
DCHECK(IsPeriodNode(period_node));
// TODO(kqyang): Why don't we iterate the C++ classes instead of iterating XML
// elements?
// TODO(kqyang): Verify if this works for static + live profile.
for (xmlNodePtr adaptation_set = xmlFirstElementChild(period_node);
adaptation_set; adaptation_set = xmlNextElementSibling(adaptation_set)) {
for (xmlNodePtr representation = xmlFirstElementChild(adaptation_set);
representation;
representation = xmlNextElementSibling(representation)) {
float duration = 0.0f;
if (GetDurationAttribute(representation, &duration)) {
max_duration = max_duration > duration ? max_duration : duration;
// 'duration' attribute is there only to help generate MPD, not
// necessary for MPD, remove the attribute.
xmlUnsetProp(representation, BAD_CAST "duration");
// TODO(kqyang): Right now all periods contain the duration for the whole MPD.
// Simply get the duration from the first period. Ideally the period duration
// should only count the (sub)segments in that period.
for (const auto* adaptation_set : periods_.front()->GetAdaptationSets()) {
for (const auto* representation : adaptation_set->GetRepresentations()) {
max_duration =
std::max(representation->GetDurationSeconds(), max_duration);
}
}
}
return max_duration;
}
bool MpdBuilder::GetEarliestTimestamp(double* timestamp_seconds) {
DCHECK(timestamp_seconds);
DCHECK(!periods_.empty());
return periods_.front()->GetEarliestTimestamp(timestamp_seconds);
double timestamp = 0;
double earliest_timestamp = -1;
// The first period should have the earliest timestamp.
for (const auto* adaptation_set : periods_.front()->GetAdaptationSets()) {
for (const auto* representation : adaptation_set->GetRepresentations()) {
if (representation->GetEarliestTimestamp(&timestamp) &&
(earliest_timestamp < 0 || timestamp < earliest_timestamp)) {
earliest_timestamp = timestamp;
}
}
}
if (earliest_timestamp < 0)
return false;
*timestamp_seconds = earliest_timestamp;
return true;
}
void MpdBuilder::MakePathsRelativeToMpd(const std::string& mpd_path,

View File

@ -110,24 +110,13 @@ xml::scoped_xml_ptr<xmlNode> Period::GetXml() {
return period.PassScopedPtr();
}
bool Period::GetEarliestTimestamp(double* timestamp_seconds) {
DCHECK(timestamp_seconds);
double earliest_timestamp(-1);
const std::list<AdaptationSet*> Period::GetAdaptationSets() const {
std::list<AdaptationSet*> adaptation_sets;
for (const std::unique_ptr<AdaptationSet>& adaptation_set :
adaptation_sets_) {
double timestamp;
if (adaptation_set->GetEarliestTimestamp(&timestamp) &&
(earliest_timestamp < 0 || timestamp < earliest_timestamp)) {
DCHECK_GE(timestamp, 0);
earliest_timestamp = timestamp;
adaptation_sets.push_back(adaptation_set.get());
}
}
if (earliest_timestamp < 0)
return false;
*timestamp_seconds = earliest_timestamp;
return true;
return adaptation_sets;
}
std::unique_ptr<AdaptationSet> Period::NewAdaptationSet(

View File

@ -50,6 +50,9 @@ class Period {
/// NULL scoped_xml_ptr.
xml::scoped_xml_ptr<xmlNode> GetXml();
/// @return The list of AdaptationSets in this Period.
const std::list<AdaptationSet*> GetAdaptationSets() const;
protected:
/// @param mpd_options is the options for this MPD.
/// @param adaptation_set_counter is a counter for assigning ID numbers to
@ -67,10 +70,6 @@ class Period {
friend class MpdBuilder;
friend class PeriodTest;
// Gets the earliest, normalized segment timestamp. Returns true on success,
// false otherwise.
bool GetEarliestTimestamp(double* timestamp_seconds);
// Calls AdaptationSet constructor. For mock injection.
virtual std::unique_ptr<AdaptationSet> NewAdaptationSet(
uint32_t adaptation_set_id,

View File

@ -712,7 +712,6 @@ TEST_P(PeriodTest, SplitAdaptationSetsByLanguageAndCodec) {
"reference_time_scale: 50\n"
"container_type: CONTAINER_MP4\n"
"media_duration_seconds: 10.5\n";
const char kAacGermanAudioContent[] =
"audio_info {\n"
" codec: 'mp4a.40.2'\n"
@ -724,7 +723,6 @@ TEST_P(PeriodTest, SplitAdaptationSetsByLanguageAndCodec) {
"reference_time_scale: 50\n"
"container_type: CONTAINER_MP4\n"
"media_duration_seconds: 10.5\n";
const char kVorbisGermanAudioContent1[] =
"audio_info {\n"
" codec: 'vorbis'\n"
@ -736,7 +734,6 @@ TEST_P(PeriodTest, SplitAdaptationSetsByLanguageAndCodec) {
"reference_time_scale: 50\n"
"container_type: CONTAINER_WEBM\n"
"media_duration_seconds: 10.5\n";
const char kVorbisGermanAudioContent2[] =
"audio_info {\n"
" codec: 'vorbis'\n"
@ -784,6 +781,57 @@ TEST_P(PeriodTest, SplitAdaptationSetsByLanguageAndCodec) {
content_protection_in_adaptation_set_));
}
TEST_P(PeriodTest, GetAdaptationSets) {
const char kAacEnglishAudioContent[] =
"audio_info {\n"
" codec: 'mp4a.40.2'\n"
" sampling_frequency: 44100\n"
" time_scale: 1200\n"
" num_channels: 2\n"
" language: 'eng'\n"
"}\n"
"reference_time_scale: 50\n"
"container_type: CONTAINER_MP4\n"
"media_duration_seconds: 10.5\n";
const char kAacGermanAudioContent[] =
"audio_info {\n"
" codec: 'mp4a.40.2'\n"
" sampling_frequency: 44100\n"
" time_scale: 1200\n"
" num_channels: 2\n"
" language: 'ger'\n"
"}\n"
"reference_time_scale: 50\n"
"container_type: CONTAINER_MP4\n"
"media_duration_seconds: 10.5\n";
std::unique_ptr<StrictMock<MockAdaptationSet>> aac_eng_adaptation_set(
new StrictMock<MockAdaptationSet>(1));
auto* aac_eng_adaptation_set_ptr = aac_eng_adaptation_set.get();
std::unique_ptr<StrictMock<MockAdaptationSet>> aac_ger_adaptation_set(
new StrictMock<MockAdaptationSet>(2));
auto* aac_ger_adaptation_set_ptr = aac_ger_adaptation_set.get();
EXPECT_CALL(testable_period_, NewAdaptationSet(_, _, _, _))
.WillOnce(Return(ByMove(std::move(aac_eng_adaptation_set))))
.WillOnce(Return(ByMove(std::move(aac_ger_adaptation_set))));
ASSERT_EQ(aac_eng_adaptation_set_ptr,
testable_period_.GetOrCreateAdaptationSet(
ConvertToMediaInfo(kAacEnglishAudioContent),
content_protection_in_adaptation_set_));
EXPECT_THAT(testable_period_.GetAdaptationSets(),
UnorderedElementsAre(aac_eng_adaptation_set_ptr));
ASSERT_EQ(aac_ger_adaptation_set_ptr,
testable_period_.GetOrCreateAdaptationSet(
ConvertToMediaInfo(kAacGermanAudioContent),
content_protection_in_adaptation_set_));
EXPECT_THAT(testable_period_.GetAdaptationSets(),
UnorderedElementsAre(aac_eng_adaptation_set_ptr,
aac_ger_adaptation_set_ptr));
}
INSTANTIATE_TEST_CASE_P(ContentProtectionInAdaptationSet,
PeriodTest,
::testing::Bool());

View File

@ -255,15 +255,6 @@ xml::scoped_xml_ptr<xmlNode> Representation::GetXml() {
return xml::scoped_xml_ptr<xmlNode>();
}
// Set media duration for static mpd.
if (mpd_options_.mpd_type == MpdType::kStatic &&
media_info_.has_media_duration_seconds()) {
// Adding 'duration' attribute, so that this information can be used when
// generating one MPD file. This should be removed from the final MPD.
representation.SetFloatingPointAttribute(
"duration", media_info_.media_duration_seconds());
}
if (HasVODOnlyFields(media_info_) &&
!representation.AddVODOnlyInfo(media_info_)) {
LOG(ERROR) << "Failed to add VOD segment info.";
@ -287,6 +278,21 @@ void Representation::SuppressOnce(SuppressFlag flag) {
output_suppression_flags_ |= flag;
}
bool Representation::GetEarliestTimestamp(double* timestamp_seconds) const {
DCHECK(timestamp_seconds);
if (segment_infos_.empty())
return false;
*timestamp_seconds = static_cast<double>(segment_infos_.begin()->start_time) /
GetTimeScale(media_info_);
return true;
}
float Representation::GetDurationSeconds() const {
return media_info_.media_duration_seconds();
}
bool Representation::HasRequiredMediaInfoFields() {
if (HasVODOnlyFields(media_info_) && HasLiveOnlyFields(media_info_)) {
LOG(ERROR) << "MediaInfo cannot have both VOD and Live fields.";
@ -453,15 +459,4 @@ std::string Representation::GetTextMimeType() const {
return "";
}
bool Representation::GetEarliestTimestamp(double* timestamp_seconds) {
DCHECK(timestamp_seconds);
if (segment_infos_.empty())
return false;
*timestamp_seconds = static_cast<double>(segment_infos_.begin()->start_time) /
GetTimeScale(media_info_);
return true;
}
} // namespace shaka

View File

@ -125,6 +125,13 @@ class Representation {
/// This may be called multiple times to set different (or the same) flags.
void SuppressOnce(SuppressFlag flag);
/// Gets the earliest, normalized segment timestamp.
/// @return true if successful, false otherwise.
bool GetEarliestTimestamp(double* timestamp_seconds) const;
/// @return The duration of the Representation in seconds.
float GetDurationSeconds() const;
/// @return ID number for <Representation>.
uint32_t id() const { return id_; }
@ -150,8 +157,6 @@ class Representation {
friend class AdaptationSet;
friend class RepresentationTest;
bool AddLiveInfo(xml::RepresentationXmlNode* representation);
// Returns true if |media_info_| has required fields to generate a valid
// Representation. Otherwise returns false.
bool HasRequiredMediaInfoFields();
@ -172,10 +177,6 @@ class Representation {
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_;

View File

@ -484,6 +484,32 @@ TEST_F(SegmentTemplateTest, OneSegmentNormal) {
EXPECT_THAT(representation_->GetXml().get(), XmlNodeEqual(kExpectedXml));
}
TEST_F(SegmentTemplateTest, GetEarliestTimestamp) {
double earliest_timestamp;
// No segments.
EXPECT_FALSE(representation_->GetEarliestTimestamp(&earliest_timestamp));
const uint64_t kStartTime = 88;
const uint64_t kDuration = 10;
const uint64_t kSize = 128;
AddSegments(kStartTime, kDuration, kSize, 0);
AddSegments(kStartTime + kDuration, kDuration, kSize, 0);
ASSERT_TRUE(representation_->GetEarliestTimestamp(&earliest_timestamp));
EXPECT_EQ(static_cast<double>(kStartTime) / kDefaultTimeScale,
earliest_timestamp);
}
TEST_F(SegmentTemplateTest, GetDuration) {
const float kMediaDurationSeconds = 88.8f;
MediaInfo media_info = ConvertToMediaInfo(GetDefaultMediaInfo());
media_info.set_media_duration_seconds(kMediaDurationSeconds);
representation_ =
CreateRepresentation(media_info, kAnyRepresentationId, NoListener());
ASSERT_TRUE(representation_->Init());
EXPECT_EQ(kMediaDurationSeconds, representation_->GetDurationSeconds());
}
TEST_F(SegmentTemplateTest, NormalRepeatedSegmentDuration) {
const uint64_t kSize = 256;
uint64_t start_time = 0;