[DASH] Support multiple period

Change-Id: Ifd17bf0eabbd61ec7a1d35f0b864b5aa6666aa87
This commit is contained in:
KongQun Yang 2018-01-02 16:10:54 -08:00
parent 2ba23e1075
commit d76ccea46f
20 changed files with 409 additions and 89 deletions

View File

@ -188,43 +188,30 @@ Representation* AdaptationSet::AddRepresentation(const MediaInfo& media_info) {
// will die before AdaptationSet. // will die before AdaptationSet.
std::unique_ptr<RepresentationStateChangeListener> listener( std::unique_ptr<RepresentationStateChangeListener> listener(
new RepresentationStateChangeListenerImpl(representation_id, this)); new RepresentationStateChangeListenerImpl(representation_id, this));
std::unique_ptr<Representation> representation(new Representation( std::unique_ptr<Representation> new_representation(new Representation(
media_info, mpd_options_, representation_id, std::move(listener))); media_info, mpd_options_, representation_id, std::move(listener)));
if (!representation->Init()) { if (!new_representation->Init()) {
LOG(ERROR) << "Failed to initialize Representation."; LOG(ERROR) << "Failed to initialize Representation.";
return NULL; return NULL;
} }
UpdateFromMediaInfo(media_info);
representations_.push_back(std::move(new_representation));
return representations_.back().get();
}
// For videos, record the width, height, and the frame rate to calculate the Representation* AdaptationSet::CopyRepresentationWithTimeOffset(
// max {width,height,framerate} required for DASH IOP. const Representation& representation,
if (media_info.has_video_info()) { uint64_t presentation_time_offset) {
const MediaInfo::VideoInfo& video_info = media_info.video_info(); // Note that AdaptationSet outlive Representation, so this object
DCHECK(video_info.has_width()); // will die before AdaptationSet.
DCHECK(video_info.has_height()); std::unique_ptr<RepresentationStateChangeListener> listener(
video_widths_.insert(video_info.width()); new RepresentationStateChangeListenerImpl(representation.id(), this));
video_heights_.insert(video_info.height()); std::unique_ptr<Representation> new_representation(new Representation(
representation, presentation_time_offset, std::move(listener)));
if (video_info.has_time_scale() && video_info.has_frame_duration()) UpdateFromMediaInfo(new_representation->GetMediaInfo());
RecordFrameRate(video_info.frame_duration(), video_info.time_scale()); representations_.push_back(std::move(new_representation));
AddPictureAspectRatio(video_info, &picture_aspect_ratio_);
}
if (media_info.has_video_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(std::move(representation));
return representations_.back().get(); return representations_.back().get();
} }
@ -396,6 +383,36 @@ const std::list<Representation*> AdaptationSet::GetRepresentations() const {
return representations; return representations;
} }
void AdaptationSet::UpdateFromMediaInfo(const MediaInfo& media_info) {
// For videos, record the width, height, and the frame rate to calculate the
// max {width,height,framerate} required for DASH IOP.
if (media_info.has_video_info()) {
const MediaInfo::VideoInfo& video_info = media_info.video_info();
DCHECK(video_info.has_width());
DCHECK(video_info.has_height());
video_widths_.insert(video_info.width());
video_heights_.insert(video_info.height());
if (video_info.has_time_scale() && video_info.has_frame_duration())
RecordFrameRate(video_info.frame_duration(), video_info.time_scale());
AddPictureAspectRatio(video_info, &picture_aspect_ratio_);
}
if (media_info.has_video_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()));
}
}
}
// This implementation assumes that each representations' segments' are // This implementation assumes that each representations' segments' are
// contiguous. // contiguous.
// Also assumes that all Representations are added before this is called. // Also assumes that all Representations are added before this is called.

View File

@ -59,6 +59,18 @@ class AdaptationSet {
/// 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);
/// Copy a Representation instance from @a representation in another
/// AdaptationSet. One use case is to duplicate Representation in different
/// periods.
/// @param representation is an existing Representation to be cloned from.
/// @param presentation_time_offset is the presentation time offset for the
/// new Representation instance.
/// @return On success, returns a pointer to Representation. Otherwise returns
/// NULL. The returned pointer is owned by the AdaptationSet instance.
virtual Representation* CopyRepresentationWithTimeOffset(
const Representation& representation,
uint64_t presentation_time_offset);
/// Add a ContenProtection element to the adaptation set. /// Add a ContenProtection element to the adaptation set.
/// AdaptationSet does not add <ContentProtection> elements /// AdaptationSet does not add <ContentProtection> elements
/// automatically to itself even if @a media_info.protected_content is /// automatically to itself even if @a media_info.protected_content is
@ -111,6 +123,10 @@ class AdaptationSet {
// Must be unique in the Period. // Must be unique in the Period.
uint32_t id() const { return id_; } uint32_t id() const { return id_; }
/// Set AdaptationSet@id.
/// @param id is the new ID to be set.
void set_id(uint32_t id) { id_ = id; }
/// Notifies the AdaptationSet instance that a new (sub)segment was added to /// Notifies the AdaptationSet instance that a new (sub)segment was added to
/// the Representation with @a representation_id. /// the Representation with @a representation_id.
/// This must be called every time a (sub)segment is added to a /// This must be called every time a (sub)segment is added to a
@ -191,6 +207,9 @@ class AdaptationSet {
// 2 -> [0, 200, 400] // 2 -> [0, 200, 400]
typedef std::map<uint32_t, std::list<uint64_t>> RepresentationTimeline; typedef std::map<uint32_t, std::list<uint64_t>> RepresentationTimeline;
// Update AdaptationSet attributes for new MediaInfo.
void UpdateFromMediaInfo(const MediaInfo& media_info);
/// Called from OnNewSegmentForRepresentation(). Checks whether the segments /// Called from OnNewSegmentForRepresentation(). Checks whether the segments
/// are aligned. Sets segments_aligned_. /// are aligned. Sets segments_aligned_.
/// This is only for Live. For VOD, CheckVodSegmentAlignment() should be used. /// This is only for Live. For VOD, CheckVodSegmentAlignment() should be used.
@ -214,7 +233,7 @@ class AdaptationSet {
base::AtomicSequenceNumber* const representation_counter_; base::AtomicSequenceNumber* const representation_counter_;
const uint32_t id_; uint32_t id_;
const std::string lang_; const std::string lang_;
const MpdOptions& mpd_options_; const MpdOptions& mpd_options_;

View File

@ -120,6 +120,31 @@ TEST_F(AdaptationSetTest, CheckAdaptationSetTextContentType) {
AttributeEqual("contentType", "text")); AttributeEqual("contentType", "text"));
} }
TEST_F(AdaptationSetTest, CopyRepresentationWithTimeOffset) {
const char kVideoMediaInfo[] =
"video_info {\n"
" codec: 'avc1'\n"
" width: 1280\n"
" height: 720\n"
" time_scale: 10\n"
" frame_duration: 10\n"
" pixel_width: 1\n"
" pixel_height: 1\n"
"}\n"
"container_type: CONTAINER_MP4\n";
auto adaptation_set = CreateAdaptationSet(kAnyAdaptationSetId, kNoLanguage);
Representation* representation =
adaptation_set->AddRepresentation(ConvertToMediaInfo(kVideoMediaInfo));
const uint64_t kPresentationTimeOffset = 80;
Representation* new_representation =
adaptation_set->CopyRepresentationWithTimeOffset(*representation,
kPresentationTimeOffset);
EXPECT_EQ(kPresentationTimeOffset,
new_representation->GetMediaInfo().presentation_time_offset());
}
// Verify that language passed to the constructor sets the @lang field is set. // Verify that language passed to the constructor sets the @lang field is set.
TEST_F(AdaptationSetTest, CheckLanguageAttributeSet) { TEST_F(AdaptationSetTest, CheckLanguageAttributeSet) {
auto adaptation_set = CreateAdaptationSet(kAnyAdaptationSetId, "en"); auto adaptation_set = CreateAdaptationSet(kAnyAdaptationSetId, "en");

View File

@ -133,6 +133,7 @@ message MediaInfo {
// This is the reference time scale if there are multiple VideoInfo and/or // This is the reference time scale if there are multiple VideoInfo and/or
// AudioInfo. // AudioInfo.
optional uint32 reference_time_scale = 13; optional uint32 reference_time_scale = 13;
optional uint64 presentation_time_offset = 16;
optional ContainerType container_type = 14 [default = CONTAINER_UNKNOWN]; optional ContainerType container_type = 14 [default = CONTAINER_UNKNOWN];
// VOD only. // VOD only.

View File

@ -13,8 +13,12 @@ const MpdOptions kDefaultMpdOptions;
MockMpdBuilder::MockMpdBuilder() : MpdBuilder(kDefaultMpdOptions) {} MockMpdBuilder::MockMpdBuilder() : MpdBuilder(kDefaultMpdOptions) {}
MockMpdBuilder::~MockMpdBuilder() {} MockMpdBuilder::~MockMpdBuilder() {}
MockPeriod::MockPeriod() MockPeriod::MockPeriod(uint32_t period_id, double start_time_in_seconds)
: Period(kDefaultMpdOptions, &sequence_counter_, &sequence_counter_) {} : Period(period_id,
start_time_in_seconds,
kDefaultMpdOptions,
&sequence_counter_,
&sequence_counter_) {}
MockAdaptationSet::MockAdaptationSet(uint32_t adaptation_set_id) MockAdaptationSet::MockAdaptationSet(uint32_t adaptation_set_id)
: AdaptationSet(adaptation_set_id, : AdaptationSet(adaptation_set_id,

View File

@ -24,13 +24,13 @@ class MockMpdBuilder : public MpdBuilder {
MockMpdBuilder(); MockMpdBuilder();
~MockMpdBuilder() override; ~MockMpdBuilder() override;
MOCK_METHOD0(AddPeriod, Period*()); MOCK_METHOD1(GetOrCreatePeriod, Period*(double start_time_in_seconds));
MOCK_METHOD1(ToString, bool(std::string* output)); MOCK_METHOD1(ToString, bool(std::string* output));
}; };
class MockPeriod : public Period { class MockPeriod : public Period {
public: public:
MockPeriod(); MockPeriod(uint32_t period_id, double start_time_in_seconds);
MOCK_METHOD2(GetOrCreateAdaptationSet, MOCK_METHOD2(GetOrCreateAdaptationSet,
AdaptationSet*(const MediaInfo& media_info, AdaptationSet*(const MediaInfo& media_info,
@ -48,6 +48,9 @@ class MockAdaptationSet : public AdaptationSet {
~MockAdaptationSet() override; ~MockAdaptationSet() override;
MOCK_METHOD1(AddRepresentation, Representation*(const MediaInfo& media_info)); MOCK_METHOD1(AddRepresentation, Representation*(const MediaInfo& media_info));
MOCK_METHOD2(CopyRepresentationWithTimeOffset,
Representation*(const Representation& representation,
uint64_t presentation_time_offset));
MOCK_METHOD1(AddContentProtectionElement, MOCK_METHOD1(AddContentProtectionElement,
void(const ContentProtectionElement& element)); void(const ContentProtectionElement& element));
MOCK_METHOD2(UpdateContentProtectionPssh, MOCK_METHOD2(UpdateContentProtectionPssh,
@ -75,6 +78,7 @@ class MockRepresentation : public Representation {
MOCK_METHOD3(AddNewSegment, MOCK_METHOD3(AddNewSegment,
void(uint64_t start_time, uint64_t duration, uint64_t size)); void(uint64_t start_time, uint64_t duration, uint64_t size));
MOCK_METHOD1(SetSampleDuration, void(uint32_t sample_duration)); MOCK_METHOD1(SetSampleDuration, void(uint32_t sample_duration));
MOCK_CONST_METHOD0(GetMediaInfo, const MediaInfo&());
}; };
} // namespace shaka } // namespace shaka

View File

@ -119,9 +119,18 @@ void MpdBuilder::AddBaseUrl(const std::string& base_url) {
base_urls_.push_back(base_url); base_urls_.push_back(base_url);
} }
Period* MpdBuilder::AddPeriod() { Period* MpdBuilder::GetOrCreatePeriod(double start_time_in_seconds) {
periods_.emplace_back(new Period(mpd_options_, &adaptation_set_counter_, for (auto& period : periods_) {
&representation_counter_)); const double kPeriodTimeDriftThresholdInSeconds = 1.0;
const bool match =
std::fabs(period->start_time_in_seconds() - start_time_in_seconds) <
kPeriodTimeDriftThresholdInSeconds;
if (match)
return period.get();
}
periods_.emplace_back(
new Period(period_counter_.GetNext(), start_time_in_seconds, mpd_options_,
&adaptation_set_counter_, &representation_counter_));
return periods_.back().get(); return periods_.back().get();
} }

View File

@ -46,9 +46,13 @@ class MpdBuilder {
/// @param base_url URL for <BaseURL> entry. /// @param base_url URL for <BaseURL> entry.
void AddBaseUrl(const std::string& base_url); void AddBaseUrl(const std::string& base_url);
/// Adds <Period> to the MPD. /// Check the existing Periods, if there is one matching the provided
/// @return The new period, which is owned by this instance. /// @a start_time_in_seconds, return it; otherwise a new Period is created and
virtual Period* AddPeriod(); /// returned.
/// @param start_time_in_seconds is the period start time.
/// @return the Period matching @a start_time_in_seconds if found; otherwise
/// return a new Period.
virtual Period* GetOrCreatePeriod(double start_time_in_seconds);
/// Writes the MPD to the given string. /// Writes the MPD to the given string.
/// @param[out] output is an output string where the MPD gets written. /// @param[out] output is an output string where the MPD gets written.
@ -112,6 +116,7 @@ class MpdBuilder {
std::list<std::string> base_urls_; std::list<std::string> base_urls_;
std::string availability_start_time_; std::string availability_start_time_;
base::AtomicSequenceNumber period_counter_;
base::AtomicSequenceNumber adaptation_set_counter_; base::AtomicSequenceNumber adaptation_set_counter_;
base::AtomicSequenceNumber representation_counter_; base::AtomicSequenceNumber representation_counter_;

View File

@ -37,7 +37,8 @@ class MpdBuilderTest : public ::testing::Test {
~MpdBuilderTest() override {} ~MpdBuilderTest() override {}
void SetUp() override { void SetUp() override {
period_ = mpd_.AddPeriod(); const double kPeriodStartTimeSeconds = 0.0;
period_ = mpd_.GetOrCreatePeriod(kPeriodStartTimeSeconds);
ASSERT_TRUE(period_); ASSERT_TRUE(period_);
} }
@ -149,6 +150,26 @@ TEST_F(OnDemandMpdBuilderTest, MediaInfoMissingBandwidth) {
ASSERT_FALSE(mpd_.ToString(&mpd_doc)); ASSERT_FALSE(mpd_.ToString(&mpd_doc));
} }
TEST_F(LiveMpdBuilderTest, MultiplePeriodTest) {
const double kPeriodStartTimeSeconds = 1.0;
Period* period = mpd_.GetOrCreatePeriod(kPeriodStartTimeSeconds);
ASSERT_TRUE(period);
ASSERT_EQ(kPeriodStartTimeSeconds, period->start_time_in_seconds());
const double kPeriodStartTimeSeconds2 = 1.1;
Period* period2 = mpd_.GetOrCreatePeriod(kPeriodStartTimeSeconds2);
ASSERT_TRUE(period2);
// The old Period is re-used if they are closed to each other.
ASSERT_EQ(period, period2);
ASSERT_EQ(kPeriodStartTimeSeconds, period2->start_time_in_seconds());
const double kPeriodStartTimeSeconds3 = 5.0;
Period* period3 = mpd_.GetOrCreatePeriod(kPeriodStartTimeSeconds3);
ASSERT_TRUE(period3);
ASSERT_NE(period, period3);
ASSERT_EQ(kPeriodStartTimeSeconds3, period3->start_time_in_seconds());
}
// 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.

View File

@ -34,10 +34,14 @@ std::set<std::string> GetUUIDs(
} // namespace } // namespace
Period::Period(const MpdOptions& mpd_options, Period::Period(uint32_t period_id,
double start_time_in_seconds,
const MpdOptions& mpd_options,
base::AtomicSequenceNumber* adaptation_set_counter, base::AtomicSequenceNumber* adaptation_set_counter,
base::AtomicSequenceNumber* representation_counter) base::AtomicSequenceNumber* representation_counter)
: mpd_options_(mpd_options), : id_(period_id),
start_time_in_seconds_(start_time_in_seconds),
mpd_options_(mpd_options),
adaptation_set_counter_(adaptation_set_counter), adaptation_set_counter_(adaptation_set_counter),
representation_counter_(representation_counter) {} representation_counter_(representation_counter) {}
@ -92,9 +96,8 @@ AdaptationSet* Period::GetOrCreateAdaptationSet(
xml::scoped_xml_ptr<xmlNode> Period::GetXml() { xml::scoped_xml_ptr<xmlNode> Period::GetXml() {
xml::XmlNode period("Period"); xml::XmlNode period("Period");
// Always set id=0 for now.
// Required for 'dynamic' MPDs. // Required for 'dynamic' MPDs.
period.SetId(0); period.SetId(id_);
// Iterate thru AdaptationSets and add them to one big Period element. // Iterate thru AdaptationSets and add them to one big Period element.
for (const auto& adaptation_set : adaptation_sets_) { for (const auto& adaptation_set : adaptation_sets_) {
xml::scoped_xml_ptr<xmlNode> child(adaptation_set->GetXml()); xml::scoped_xml_ptr<xmlNode> child(adaptation_set->GetXml());
@ -102,10 +105,10 @@ xml::scoped_xml_ptr<xmlNode> Period::GetXml() {
return nullptr; return nullptr;
} }
// TODO(kqyang): Should we set @start unconditionally to 0? if (mpd_options_.mpd_type == MpdType::kDynamic ||
if (mpd_options_.mpd_type == MpdType::kDynamic) { start_time_in_seconds_ != 0) {
// This is the only Period and it is a regular period. period.SetStringAttribute("start",
period.SetStringAttribute("start", "PT0S"); SecondsToXmlDuration(start_time_in_seconds_));
} }
return period.PassScopedPtr(); return period.PassScopedPtr();
} }

View File

@ -53,13 +53,20 @@ class Period {
/// @return The list of AdaptationSets in this Period. /// @return The list of AdaptationSets in this Period.
const std::list<AdaptationSet*> GetAdaptationSets() const; const std::list<AdaptationSet*> GetAdaptationSets() const;
/// @return The start time of this Period.
double start_time_in_seconds() const { return start_time_in_seconds_; }
protected: protected:
/// @param period_id is an ID number for this Period.
/// @param start_time_in_seconds is the start time for this Period.
/// @param mpd_options is the options for this MPD. /// @param mpd_options is the options for this MPD.
/// @param adaptation_set_counter is a counter for assigning ID numbers to /// @param adaptation_set_counter is a counter for assigning ID numbers to
/// AdaptationSet. It can not be NULL. /// AdaptationSet. It can not be NULL.
/// @param representation_counter is a counter for assigning ID numbers to /// @param representation_counter is a counter for assigning ID numbers to
/// Representation. It can not be NULL. /// Representation. It can not be NULL.
Period(const MpdOptions& mpd_options, Period(uint32_t period_id,
double start_time_in_seconds,
const MpdOptions& mpd_options,
base::AtomicSequenceNumber* adaptation_set_counter, base::AtomicSequenceNumber* adaptation_set_counter,
base::AtomicSequenceNumber* representation_counter); base::AtomicSequenceNumber* representation_counter);
@ -93,6 +100,8 @@ class Period {
const MediaInfo& media_info, const MediaInfo& media_info,
uint32_t* original_adaptation_set_id); uint32_t* original_adaptation_set_id);
const uint32_t id_;
const double start_time_in_seconds_;
const MpdOptions& mpd_options_; const MpdOptions& mpd_options_;
base::AtomicSequenceNumber* const adaptation_set_counter_; base::AtomicSequenceNumber* const adaptation_set_counter_;
base::AtomicSequenceNumber* const representation_counter_; base::AtomicSequenceNumber* const representation_counter_;

View File

@ -25,6 +25,8 @@ using ::testing::UnorderedElementsAre;
namespace shaka { namespace shaka {
namespace { namespace {
const uint32_t kDefaultPeriodId = 9u;
const double kDefaultPeriodStartTime = 5.6;
const uint32_t kDefaultAdaptationSetId = 0u; const uint32_t kDefaultAdaptationSetId = 0u;
const uint32_t kTrickPlayAdaptationSetId = 1u; const uint32_t kTrickPlayAdaptationSetId = 1u;
@ -75,7 +77,11 @@ MATCHER_P(ContentProtectionElementEq, expected, "") {
class TestablePeriod : public Period { class TestablePeriod : public Period {
public: public:
TestablePeriod(const MpdOptions& mpd_options) TestablePeriod(const MpdOptions& mpd_options)
: Period(mpd_options, &sequence_number_, &sequence_number_) {} : Period(kDefaultPeriodId,
kDefaultPeriodStartTime,
mpd_options,
&sequence_number_,
&sequence_number_) {}
MOCK_METHOD4(NewAdaptationSet, MOCK_METHOD4(NewAdaptationSet,
std::unique_ptr<AdaptationSet>( std::unique_ptr<AdaptationSet>(
@ -133,7 +139,7 @@ TEST_P(PeriodTest, GetXml) {
content_protection_in_adaptation_set_)); content_protection_in_adaptation_set_));
const char kExpectedXml[] = const char kExpectedXml[] =
"<Period id=\"0\">" "<Period id=\"9\" start=\"PT5.6S\">"
// ContentType and Representation elements are populated after // ContentType and Representation elements are populated after
// Representation::Init() is called. // Representation::Init() is called.
" <AdaptationSet id=\"0\" contentType=\"\"/>" " <AdaptationSet id=\"0\" contentType=\"\"/>"
@ -164,7 +170,7 @@ TEST_P(PeriodTest, DynamicMpdGetXml) {
content_protection_in_adaptation_set_)); content_protection_in_adaptation_set_));
const char kExpectedXml[] = const char kExpectedXml[] =
"<Period id=\"0\" start=\"PT0S\">" "<Period id=\"9\" start=\"PT5.6S\">"
// ContentType and Representation elements are populated after // ContentType and Representation elements are populated after
// Representation::Init() is called. // Representation::Init() is called.
" <AdaptationSet id=\"0\" contentType=\"\"/>" " <AdaptationSet id=\"0\" contentType=\"\"/>"

View File

@ -115,6 +115,24 @@ Representation::Representation(
state_change_listener_(std::move(state_change_listener)), state_change_listener_(std::move(state_change_listener)),
output_suppression_flags_(0) {} output_suppression_flags_(0) {}
Representation::Representation(
const Representation& representation,
uint64_t presentation_time_offset,
std::unique_ptr<RepresentationStateChangeListener> state_change_listener)
: Representation(representation.media_info_,
representation.mpd_options_,
representation.id_,
std::move(state_change_listener)) {
mime_type_ = representation.mime_type_;
codecs_ = representation.codecs_;
start_number_ = representation.start_number_;
for (const SegmentInfo& segment_info : representation.segment_infos_)
start_number_ += segment_info.repeat + 1;
media_info_.set_presentation_time_offset(presentation_time_offset);
}
Representation::~Representation() {} Representation::~Representation() {}
bool Representation::Init() { bool Representation::Init() {
@ -205,6 +223,10 @@ void Representation::SetSampleDuration(uint32_t sample_duration) {
} }
} }
const MediaInfo& Representation::GetMediaInfo() const {
return media_info_;
}
// Uses info in |media_info_| and |content_protection_elements_| to create a // Uses info in |media_info_| and |content_protection_elements_| to create a
// "Representation" node. // "Representation" node.
// MPD schema has strict ordering. The following must be done in order. // MPD schema has strict ordering. The following must be done in order.

View File

@ -112,6 +112,9 @@ class Representation {
/// @param sample_duration is the duration of a sample. /// @param sample_duration is the duration of a sample.
virtual void SetSampleDuration(uint32_t sample_duration); virtual void SetSampleDuration(uint32_t sample_duration);
/// @return MediaInfo for the Representation.
virtual const MediaInfo& GetMediaInfo() const;
/// @return Copy of <Representation>. /// @return Copy of <Representation>.
xml::scoped_xml_ptr<xmlNode> GetXml(); xml::scoped_xml_ptr<xmlNode> GetXml();
@ -150,6 +153,16 @@ class Representation {
uint32_t representation_id, uint32_t representation_id,
std::unique_ptr<RepresentationStateChangeListener> state_change_listener); std::unique_ptr<RepresentationStateChangeListener> state_change_listener);
/// @param representation points to the original Representation to be cloned.
/// @param presentation_time_offset is the presentation time offset for the
/// new Representation.
/// @param state_change_listener is an event handler for state changes to
/// the representation. If null, no event handler registered.
Representation(
const Representation& representation,
uint64_t presentation_time_offset,
std::unique_ptr<RepresentationStateChangeListener> state_change_listener);
private: private:
Representation(const Representation&) = delete; Representation(const Representation&) = delete;
Representation& operator=(const Representation&) = delete; Representation& operator=(const Representation&) = delete;
@ -181,6 +194,7 @@ class Representation {
// any logic using this can assume only one set. // 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_;
// TODO(kqyang): Address sliding window issue with multiple periods.
std::list<SegmentInfo> segment_infos_; std::list<SegmentInfo> segment_infos_;
const uint32_t id_; const uint32_t id_;

View File

@ -52,6 +52,16 @@ class RepresentationTest : public ::testing::Test {
std::move(state_change_listener))); std::move(state_change_listener)));
} }
std::unique_ptr<Representation> CopyRepresentation(
const Representation& representation,
uint64_t presentation_time_offset,
std::unique_ptr<RepresentationStateChangeListener>
state_change_listener) {
return std::unique_ptr<Representation>(
new Representation(representation, presentation_time_offset,
std::move(state_change_listener)));
}
std::unique_ptr<RepresentationStateChangeListener> NoListener() { std::unique_ptr<RepresentationStateChangeListener> NoListener() {
return std::unique_ptr<RepresentationStateChangeListener>(); return std::unique_ptr<RepresentationStateChangeListener>();
} }
@ -484,6 +494,35 @@ TEST_F(SegmentTemplateTest, OneSegmentNormal) {
EXPECT_THAT(representation_->GetXml().get(), XmlNodeEqual(kExpectedXml)); EXPECT_THAT(representation_->GetXml().get(), XmlNodeEqual(kExpectedXml));
} }
TEST_F(SegmentTemplateTest, RepresentationClone) {
MediaInfo media_info = ConvertToMediaInfo(GetDefaultMediaInfo());
media_info.set_segment_template("$Number$.mp4");
representation_ =
CreateRepresentation(media_info, kAnyRepresentationId, NoListener());
ASSERT_TRUE(representation_->Init());
const uint64_t kStartTime = 0;
const uint64_t kDuration = 10;
const uint64_t kSize = 128;
AddSegments(kStartTime, kDuration, kSize, 0);
const uint64_t kPresentationTimeOffset = 100;
auto cloned_representation = CopyRepresentation(
*representation_, kPresentationTimeOffset, NoListener());
const char kExpectedXml[] =
"<Representation id=\"1\" bandwidth=\"0\" "
" codecs=\"avc1.010101\" mimeType=\"video/mp4\" sar=\"1:1\" "
" width=\"720\" height=\"480\" frameRate=\"10/5\">\n"
" <SegmentTemplate presentationTimeOffset=\"100\" timescale=\"1000\" "
" initialization=\"init.mp4\" media=\"$Number$.mp4\" "
" startNumber=\"2\">\n"
" <SegmentTimeline/>\n"
" </SegmentTemplate>\n"
"</Representation>\n";
EXPECT_THAT(cloned_representation->GetXml().get(),
XmlNodeEqual(kExpectedXml));
}
TEST_F(SegmentTemplateTest, GetEarliestTimestamp) { TEST_F(SegmentTemplateTest, GetEarliestTimestamp) {
double earliest_timestamp; double earliest_timestamp;
// No segments. // No segments.

View File

@ -41,32 +41,17 @@ bool SimpleMpdNotifier::NotifyNewContainer(const MediaInfo& media_info,
if (content_type == kContentTypeUnknown) if (content_type == kContentTypeUnknown)
return false; return false;
base::AutoLock auto_lock(lock_);
if (!period_)
period_ = mpd_builder_->AddPeriod();
AdaptationSet* adaptation_set = period_->GetOrCreateAdaptationSet(
media_info, content_protection_in_adaptation_set_);
DCHECK(adaptation_set);
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);
Representation* representation = const Representation* kNoOriginalRepresentation = nullptr;
adaptation_set->AddRepresentation(adjusted_media_info); const double kPeriodStartTimeSeconds = 0.0;
base::AutoLock auto_lock(lock_);
const Representation* representation = AddRepresentationToPeriod(
adjusted_media_info, kNoOriginalRepresentation, kPeriodStartTimeSeconds);
if (!representation) if (!representation)
return false; return false;
if (content_protection_in_adaptation_set_) {
// ContentProtection elements are already added to AdaptationSet above.
// Use RepresentationId to AdaptationSet map to update ContentProtection
// in AdaptationSet in NotifyEncryptionUpdate.
representation_id_to_adaptation_set_[representation->id()] = adaptation_set;
} else {
AddContentProtectionElements(media_info, representation);
}
*container_id = representation->id(); *container_id = representation->id();
DCHECK(!ContainsKey(representation_map_, representation->id()));
representation_map_[representation->id()] = representation;
return true; return true;
} }
@ -98,8 +83,30 @@ bool SimpleMpdNotifier::NotifyNewSegment(uint32_t container_id,
bool SimpleMpdNotifier::NotifyCueEvent(uint32_t container_id, bool SimpleMpdNotifier::NotifyCueEvent(uint32_t container_id,
uint64_t timestamp) { uint64_t timestamp) {
NOTIMPLEMENTED(); base::AutoLock auto_lock(lock_);
return false; auto it = representation_map_.find(container_id);
if (it == representation_map_.end()) {
LOG(ERROR) << "Unexpected container_id: " << container_id;
return false;
}
Representation* original_representation = it->second;
AdaptationSet* original_adaptation_set =
representation_id_to_adaptation_set_[container_id];
const MediaInfo& media_info = original_representation->GetMediaInfo();
const double period_start_time_seconds =
static_cast<double>(timestamp) / media_info.reference_time_scale();
const Representation* new_representation = AddRepresentationToPeriod(
media_info, original_representation, period_start_time_seconds);
if (!new_representation)
return false;
// TODO(kqyang): Pass the ID to GetOrCreateAdaptationSet instead?
AdaptationSet* new_adaptation_set =
representation_id_to_adaptation_set_[container_id];
DCHECK(new_adaptation_set);
new_adaptation_set->set_id(original_adaptation_set->id());
return true;
} }
bool SimpleMpdNotifier::NotifyEncryptionUpdate( bool SimpleMpdNotifier::NotifyEncryptionUpdate(
@ -131,4 +138,38 @@ bool SimpleMpdNotifier::Flush() {
return WriteMpdToFile(output_path_, mpd_builder_.get()); return WriteMpdToFile(output_path_, mpd_builder_.get());
} }
Representation* SimpleMpdNotifier::AddRepresentationToPeriod(
const MediaInfo& media_info,
const Representation* original_representation,
double period_start_time_seconds) {
Period* period = mpd_builder_->GetOrCreatePeriod(period_start_time_seconds);
DCHECK(period);
AdaptationSet* adaptation_set = period->GetOrCreateAdaptationSet(
media_info, content_protection_in_adaptation_set_);
DCHECK(adaptation_set);
Representation* representation = nullptr;
if (original_representation) {
representation = adaptation_set->CopyRepresentationWithTimeOffset(
*original_representation,
period->start_time_in_seconds() * media_info.reference_time_scale());
} else {
representation = adaptation_set->AddRepresentation(media_info);
}
if (!representation)
return nullptr;
if (content_protection_in_adaptation_set_) {
// ContentProtection elements are already added to AdaptationSet above.
// Use RepresentationId to AdaptationSet map to update ContentProtection
// in AdaptationSet in NotifyEncryptionUpdate.
representation_id_to_adaptation_set_[representation->id()] = adaptation_set;
} else {
AddContentProtectionElements(media_info, representation);
}
representation_map_[representation->id()] = representation;
return representation;
}
} // namespace shaka } // namespace shaka

View File

@ -20,7 +20,6 @@ namespace shaka {
class AdaptationSet; class AdaptationSet;
class MpdBuilder; class MpdBuilder;
class Period;
class Representation; class Representation;
struct MpdOptions; struct MpdOptions;
@ -39,7 +38,7 @@ class SimpleMpdNotifier : public MpdNotifier {
bool NotifyNewContainer(const MediaInfo& media_info, uint32_t* id) override; bool NotifyNewContainer(const MediaInfo& media_info, uint32_t* id) override;
bool NotifySampleDuration(uint32_t container_id, bool NotifySampleDuration(uint32_t container_id,
uint32_t sample_duration) override; uint32_t sample_duration) override;
bool NotifyNewSegment(uint32_t id, bool NotifyNewSegment(uint32_t container_id,
uint64_t start_time, uint64_t start_time,
uint64_t duration, uint64_t duration,
uint64_t size) override; uint64_t size) override;
@ -57,6 +56,17 @@ class SimpleMpdNotifier : public MpdNotifier {
friend class SimpleMpdNotifierTest; friend class SimpleMpdNotifierTest;
// Add a new representation. If |original_representation| is not nullptr, the
// new Representation will clone from it; otherwise the new Representation is
// created from |media_info|.
// The new Representation will be added to Period with the specified start
// time.
// Returns the new Representation on success; otherwise a nullptr is returned.
Representation* AddRepresentationToPeriod(
const MediaInfo& media_info,
const Representation* original_representation,
double period_start_time_seconds);
// Testing only method. Returns a pointer to MpdBuilder. // Testing only method. Returns a pointer to MpdBuilder.
MpdBuilder* MpdBuilderForTesting() const { return mpd_builder_.get(); } MpdBuilder* MpdBuilderForTesting() const { return mpd_builder_.get(); }
@ -68,7 +78,6 @@ class SimpleMpdNotifier : public MpdNotifier {
// MPD output path. // MPD output path.
std::string output_path_; std::string output_path_;
std::unique_ptr<MpdBuilder> mpd_builder_; std::unique_ptr<MpdBuilder> mpd_builder_;
Period* period_ = nullptr; // owned by |mpd_builder_|.
bool content_protection_in_adaptation_set_ = true; bool content_protection_in_adaptation_set_ = true;
base::Lock lock_; base::Lock lock_;

View File

@ -20,11 +20,16 @@ namespace shaka {
using ::testing::_; using ::testing::_;
using ::testing::Eq; using ::testing::Eq;
using ::testing::Ref;
using ::testing::Return; using ::testing::Return;
using ::testing::ReturnRef;
using ::testing::StrEq; using ::testing::StrEq;
namespace { namespace {
const uint32_t kDefaultPeriodId = 0u;
const double kDefaultPeriodStartTime = 0.0;
const uint32_t kDefaultAdaptationSetId = 0u; const uint32_t kDefaultAdaptationSetId = 0u;
const uint32_t kDefaultTimeScale = 10;
const bool kContentProtectionInAdaptationSet = true; const bool kContentProtectionInAdaptationSet = true;
MATCHER_P(EqualsProto, message, "") { MATCHER_P(EqualsProto, message, "") {
@ -36,7 +41,8 @@ MATCHER_P(EqualsProto, message, "") {
class SimpleMpdNotifierTest : public ::testing::Test { class SimpleMpdNotifierTest : public ::testing::Test {
protected: protected:
SimpleMpdNotifierTest() SimpleMpdNotifierTest()
: default_mock_period_(new MockPeriod), : default_mock_period_(
new MockPeriod(kDefaultPeriodId, kDefaultPeriodStartTime)),
default_mock_adaptation_set_( default_mock_adaptation_set_(
new MockAdaptationSet(kDefaultAdaptationSetId)) {} new MockAdaptationSet(kDefaultAdaptationSetId)) {}
@ -57,6 +63,7 @@ class SimpleMpdNotifierTest : public ::testing::Test {
"}\n" "}\n"
"container_type: 1\n"; "container_type: 1\n";
valid_media_info1_ = ConvertToMediaInfo(kValidMediaInfo); valid_media_info1_ = ConvertToMediaInfo(kValidMediaInfo);
valid_media_info1_.set_reference_time_scale(kDefaultTimeScale);
valid_media_info2_ = valid_media_info1_; valid_media_info2_ = valid_media_info1_;
valid_media_info2_.mutable_video_info()->set_width(960); valid_media_info2_.mutable_video_info()->set_width(960);
valid_media_info3_ = valid_media_info1_; valid_media_info3_ = valid_media_info1_;
@ -100,7 +107,7 @@ TEST_F(SimpleMpdNotifierTest, NotifyNewContainer) {
std::unique_ptr<MockRepresentation> mock_representation( std::unique_ptr<MockRepresentation> mock_representation(
new MockRepresentation(kRepresentationId)); new MockRepresentation(kRepresentationId));
EXPECT_CALL(*mock_mpd_builder, AddPeriod()) EXPECT_CALL(*mock_mpd_builder, GetOrCreatePeriod(_))
.WillOnce(Return(default_mock_period_.get())); .WillOnce(Return(default_mock_period_.get()));
EXPECT_CALL(*default_mock_period_, EXPECT_CALL(*default_mock_period_,
GetOrCreateAdaptationSet(EqualsProto(valid_media_info1_), _)) GetOrCreateAdaptationSet(EqualsProto(valid_media_info1_), _))
@ -128,7 +135,7 @@ TEST_F(SimpleMpdNotifierTest, NotifySampleDuration) {
std::unique_ptr<MockRepresentation> mock_representation( std::unique_ptr<MockRepresentation> mock_representation(
new MockRepresentation(kRepresentationId)); new MockRepresentation(kRepresentationId));
EXPECT_CALL(*mock_mpd_builder, AddPeriod()) EXPECT_CALL(*mock_mpd_builder, GetOrCreatePeriod(_))
.WillOnce(Return(default_mock_period_.get())); .WillOnce(Return(default_mock_period_.get()));
EXPECT_CALL(*default_mock_period_, GetOrCreateAdaptationSet(_, _)) EXPECT_CALL(*default_mock_period_, GetOrCreateAdaptationSet(_, _))
.WillOnce(Return(default_mock_adaptation_set_.get())); .WillOnce(Return(default_mock_adaptation_set_.get()));
@ -168,7 +175,7 @@ TEST_F(SimpleMpdNotifierTest, NotifyNewSegment) {
std::unique_ptr<MockRepresentation> mock_representation( std::unique_ptr<MockRepresentation> mock_representation(
new MockRepresentation(kRepresentationId)); new MockRepresentation(kRepresentationId));
EXPECT_CALL(*mock_mpd_builder, AddPeriod()) EXPECT_CALL(*mock_mpd_builder, GetOrCreatePeriod(_))
.WillOnce(Return(default_mock_period_.get())); .WillOnce(Return(default_mock_period_.get()));
EXPECT_CALL(*default_mock_period_, GetOrCreateAdaptationSet(_, _)) EXPECT_CALL(*default_mock_period_, GetOrCreateAdaptationSet(_, _))
.WillOnce(Return(default_mock_adaptation_set_.get())); .WillOnce(Return(default_mock_adaptation_set_.get()));
@ -190,6 +197,58 @@ TEST_F(SimpleMpdNotifierTest, NotifyNewSegment) {
kSegmentDuration, kSegmentSize)); kSegmentDuration, kSegmentSize));
} }
TEST_F(SimpleMpdNotifierTest, NotifyCueEvent) {
SimpleMpdNotifier notifier(empty_mpd_option_);
const uint32_t kRepresentationId = 123u;
std::unique_ptr<MockMpdBuilder> mock_mpd_builder(new MockMpdBuilder());
MockMpdBuilder* mock_mpd_builder_ptr = mock_mpd_builder.get();
std::unique_ptr<MockPeriod> mock_period(
new MockPeriod(kDefaultPeriodId, kDefaultPeriodStartTime));
std::unique_ptr<MockAdaptationSet> mock_adaptation_set(
new MockAdaptationSet(kDefaultAdaptationSetId));
std::unique_ptr<MockRepresentation> mock_representation(
new MockRepresentation(kRepresentationId));
EXPECT_CALL(*mock_mpd_builder, GetOrCreatePeriod(Eq(0.0)))
.WillOnce(Return(mock_period.get()));
EXPECT_CALL(*mock_period,
GetOrCreateAdaptationSet(EqualsProto(valid_media_info1_), _))
.WillOnce(Return(mock_adaptation_set.get()));
EXPECT_CALL(*mock_adaptation_set, AddRepresentation(_))
.WillOnce(Return(mock_representation.get()));
uint32_t container_id;
SetMpdBuilder(&notifier, std::move(mock_mpd_builder));
EXPECT_TRUE(notifier.NotifyNewContainer(valid_media_info1_, &container_id));
EXPECT_EQ(kRepresentationId, container_id);
const uint32_t kAnotherPeriodId = 2u;
const double kArbitraryPeriodStartTime = 100; // Value does not matter.
std::unique_ptr<MockPeriod> mock_period2(
new MockPeriod(kAnotherPeriodId, kArbitraryPeriodStartTime));
std::unique_ptr<MockAdaptationSet> mock_adaptation_set2(
new MockAdaptationSet(kDefaultAdaptationSetId));
std::unique_ptr<MockRepresentation> mock_representation2(
new MockRepresentation(kRepresentationId));
const uint64_t kCueEventTimestamp = 1000;
EXPECT_CALL(*mock_representation, GetMediaInfo())
.WillOnce(ReturnRef(valid_media_info1_));
EXPECT_CALL(*mock_mpd_builder_ptr,
GetOrCreatePeriod(Eq(kCueEventTimestamp / kDefaultTimeScale)))
.WillOnce(Return(mock_period2.get()));
EXPECT_CALL(*mock_period2,
GetOrCreateAdaptationSet(EqualsProto(valid_media_info1_), _))
.WillOnce(Return(mock_adaptation_set2.get()));
EXPECT_CALL(*mock_adaptation_set2,
CopyRepresentationWithTimeOffset(Ref(*mock_representation),
kCueEventTimestamp))
.WillOnce(Return(mock_representation2.get()));
EXPECT_TRUE(notifier.NotifyCueEvent(container_id, kCueEventTimestamp));
}
TEST_F(SimpleMpdNotifierTest, TEST_F(SimpleMpdNotifierTest,
ContentProtectionInAdaptationSetUpdateEncryption) { ContentProtectionInAdaptationSetUpdateEncryption) {
MpdOptions mpd_options = empty_mpd_option_; MpdOptions mpd_options = empty_mpd_option_;
@ -202,7 +261,7 @@ TEST_F(SimpleMpdNotifierTest,
std::unique_ptr<MockRepresentation> mock_representation( std::unique_ptr<MockRepresentation> mock_representation(
new MockRepresentation(kRepresentationId)); new MockRepresentation(kRepresentationId));
EXPECT_CALL(*mock_mpd_builder, AddPeriod()) EXPECT_CALL(*mock_mpd_builder, GetOrCreatePeriod(_))
.WillOnce(Return(default_mock_period_.get())); .WillOnce(Return(default_mock_period_.get()));
EXPECT_CALL( EXPECT_CALL(
*default_mock_period_, *default_mock_period_,
@ -245,7 +304,7 @@ TEST_F(SimpleMpdNotifierTest,
std::unique_ptr<MockRepresentation> mock_representation( std::unique_ptr<MockRepresentation> mock_representation(
new MockRepresentation(kRepresentationId)); new MockRepresentation(kRepresentationId));
EXPECT_CALL(*mock_mpd_builder, AddPeriod()) EXPECT_CALL(*mock_mpd_builder, GetOrCreatePeriod(_))
.WillOnce(Return(default_mock_period_.get())); .WillOnce(Return(default_mock_period_.get()));
EXPECT_CALL( EXPECT_CALL(
*default_mock_period_, *default_mock_period_,
@ -291,8 +350,9 @@ TEST_F(SimpleMpdNotifierTest, MultipleMediaInfo) {
std::unique_ptr<MockRepresentation> representation3( std::unique_ptr<MockRepresentation> representation3(
new MockRepresentation(3)); new MockRepresentation(3));
EXPECT_CALL(*mock_mpd_builder, AddPeriod()) EXPECT_CALL(*mock_mpd_builder, GetOrCreatePeriod(_))
.WillOnce(Return(default_mock_period_.get())); .Times(3)
.WillRepeatedly(Return(default_mock_period_.get()));
EXPECT_CALL(*default_mock_period_, EXPECT_CALL(*default_mock_period_,
GetOrCreateAdaptationSet(EqualsProto(valid_media_info1_), _)) GetOrCreateAdaptationSet(EqualsProto(valid_media_info1_), _))

View File

@ -300,6 +300,11 @@ bool RepresentationXmlNode::AddVODOnlyInfo(const MediaInfo& media_info) {
media_info.reference_time_scale()); media_info.reference_time_scale());
} }
if (media_info.has_presentation_time_offset()) {
segment_base.SetIntegerAttribute("presentationTimeOffset",
media_info.presentation_time_offset());
}
if (media_info.has_init_range()) { if (media_info.has_init_range()) {
XmlNode initialization("Initialization"); XmlNode initialization("Initialization");
initialization.SetStringAttribute("range", initialization.SetStringAttribute("range",
@ -326,6 +331,11 @@ bool RepresentationXmlNode::AddLiveOnlyInfo(
media_info.reference_time_scale()); media_info.reference_time_scale());
} }
if (media_info.has_presentation_time_offset()) {
segment_template.SetIntegerAttribute("presentationTimeOffset",
media_info.presentation_time_offset());
}
if (media_info.has_init_segment_name()) { if (media_info.has_init_segment_name()) {
// The spec does not allow '$Number$' and '$Time$' in initialization // The spec does not allow '$Number$' and '$Time$' in initialization
// attribute. // attribute.

View File

@ -28,7 +28,9 @@ bool XmlEqual(const std::string& xml1, xmlNodePtr xml2);
std::string XmlNodeToString(xmlNodePtr xml_node); std::string XmlNodeToString(xmlNodePtr xml_node);
/// Match an xmlNodePtr with an xml in string representation. /// Match an xmlNodePtr with an xml in string representation.
MATCHER_P(XmlNodeEqual, xml, "") { MATCHER_P(XmlNodeEqual,
xml,
std::string("xml node equal (ignore extra white spaces)\n") + xml) {
*result_listener << "\n" << XmlNodeToString(arg); *result_listener << "\n" << XmlNodeToString(arg);
return XmlEqual(xml, arg); return XmlEqual(xml, arg);
} }