[DASH] Support multiple period
Change-Id: Ifd17bf0eabbd61ec7a1d35f0b864b5aa6666aa87
This commit is contained in:
parent
2ba23e1075
commit
d76ccea46f
|
@ -188,43 +188,30 @@ Representation* AdaptationSet::AddRepresentation(const MediaInfo& media_info) {
|
|||
// will die before AdaptationSet.
|
||||
std::unique_ptr<RepresentationStateChangeListener> listener(
|
||||
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)));
|
||||
|
||||
if (!representation->Init()) {
|
||||
if (!new_representation->Init()) {
|
||||
LOG(ERROR) << "Failed to initialize Representation.";
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// 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_);
|
||||
UpdateFromMediaInfo(media_info);
|
||||
representations_.push_back(std::move(new_representation));
|
||||
return representations_.back().get();
|
||||
}
|
||||
|
||||
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";
|
||||
Representation* AdaptationSet::CopyRepresentationWithTimeOffset(
|
||||
const Representation& representation,
|
||||
uint64_t presentation_time_offset) {
|
||||
// Note that AdaptationSet outlive Representation, so this object
|
||||
// will die before AdaptationSet.
|
||||
std::unique_ptr<RepresentationStateChangeListener> listener(
|
||||
new RepresentationStateChangeListenerImpl(representation.id(), this));
|
||||
std::unique_ptr<Representation> new_representation(new Representation(
|
||||
representation, presentation_time_offset, std::move(listener)));
|
||||
|
||||
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));
|
||||
UpdateFromMediaInfo(new_representation->GetMediaInfo());
|
||||
representations_.push_back(std::move(new_representation));
|
||||
return representations_.back().get();
|
||||
}
|
||||
|
||||
|
@ -396,6 +383,36 @@ const std::list<Representation*> AdaptationSet::GetRepresentations() const {
|
|||
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
|
||||
// contiguous.
|
||||
// Also assumes that all Representations are added before this is called.
|
||||
|
|
|
@ -59,6 +59,18 @@ class AdaptationSet {
|
|||
/// NULL. The returned pointer is owned by the AdaptationSet instance.
|
||||
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.
|
||||
/// AdaptationSet does not add <ContentProtection> elements
|
||||
/// automatically to itself even if @a media_info.protected_content is
|
||||
|
@ -111,6 +123,10 @@ class AdaptationSet {
|
|||
// Must be unique in the Period.
|
||||
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
|
||||
/// the Representation with @a representation_id.
|
||||
/// This must be called every time a (sub)segment is added to a
|
||||
|
@ -191,6 +207,9 @@ class AdaptationSet {
|
|||
// 2 -> [0, 200, 400]
|
||||
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
|
||||
/// are aligned. Sets segments_aligned_.
|
||||
/// This is only for Live. For VOD, CheckVodSegmentAlignment() should be used.
|
||||
|
@ -214,7 +233,7 @@ class AdaptationSet {
|
|||
|
||||
base::AtomicSequenceNumber* const representation_counter_;
|
||||
|
||||
const uint32_t id_;
|
||||
uint32_t id_;
|
||||
const std::string lang_;
|
||||
const MpdOptions& mpd_options_;
|
||||
|
||||
|
|
|
@ -120,6 +120,31 @@ TEST_F(AdaptationSetTest, CheckAdaptationSetTextContentType) {
|
|||
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.
|
||||
TEST_F(AdaptationSetTest, CheckLanguageAttributeSet) {
|
||||
auto adaptation_set = CreateAdaptationSet(kAnyAdaptationSetId, "en");
|
||||
|
|
|
@ -133,6 +133,7 @@ message MediaInfo {
|
|||
// This is the reference time scale if there are multiple VideoInfo and/or
|
||||
// AudioInfo.
|
||||
optional uint32 reference_time_scale = 13;
|
||||
optional uint64 presentation_time_offset = 16;
|
||||
optional ContainerType container_type = 14 [default = CONTAINER_UNKNOWN];
|
||||
|
||||
// VOD only.
|
||||
|
|
|
@ -13,8 +13,12 @@ const MpdOptions kDefaultMpdOptions;
|
|||
MockMpdBuilder::MockMpdBuilder() : MpdBuilder(kDefaultMpdOptions) {}
|
||||
MockMpdBuilder::~MockMpdBuilder() {}
|
||||
|
||||
MockPeriod::MockPeriod()
|
||||
: Period(kDefaultMpdOptions, &sequence_counter_, &sequence_counter_) {}
|
||||
MockPeriod::MockPeriod(uint32_t period_id, double start_time_in_seconds)
|
||||
: Period(period_id,
|
||||
start_time_in_seconds,
|
||||
kDefaultMpdOptions,
|
||||
&sequence_counter_,
|
||||
&sequence_counter_) {}
|
||||
|
||||
MockAdaptationSet::MockAdaptationSet(uint32_t adaptation_set_id)
|
||||
: AdaptationSet(adaptation_set_id,
|
||||
|
|
|
@ -24,13 +24,13 @@ class MockMpdBuilder : public MpdBuilder {
|
|||
MockMpdBuilder();
|
||||
~MockMpdBuilder() override;
|
||||
|
||||
MOCK_METHOD0(AddPeriod, Period*());
|
||||
MOCK_METHOD1(GetOrCreatePeriod, Period*(double start_time_in_seconds));
|
||||
MOCK_METHOD1(ToString, bool(std::string* output));
|
||||
};
|
||||
|
||||
class MockPeriod : public Period {
|
||||
public:
|
||||
MockPeriod();
|
||||
MockPeriod(uint32_t period_id, double start_time_in_seconds);
|
||||
|
||||
MOCK_METHOD2(GetOrCreateAdaptationSet,
|
||||
AdaptationSet*(const MediaInfo& media_info,
|
||||
|
@ -48,6 +48,9 @@ class MockAdaptationSet : public AdaptationSet {
|
|||
~MockAdaptationSet() override;
|
||||
|
||||
MOCK_METHOD1(AddRepresentation, Representation*(const MediaInfo& media_info));
|
||||
MOCK_METHOD2(CopyRepresentationWithTimeOffset,
|
||||
Representation*(const Representation& representation,
|
||||
uint64_t presentation_time_offset));
|
||||
MOCK_METHOD1(AddContentProtectionElement,
|
||||
void(const ContentProtectionElement& element));
|
||||
MOCK_METHOD2(UpdateContentProtectionPssh,
|
||||
|
@ -75,6 +78,7 @@ class MockRepresentation : public Representation {
|
|||
MOCK_METHOD3(AddNewSegment,
|
||||
void(uint64_t start_time, uint64_t duration, uint64_t size));
|
||||
MOCK_METHOD1(SetSampleDuration, void(uint32_t sample_duration));
|
||||
MOCK_CONST_METHOD0(GetMediaInfo, const MediaInfo&());
|
||||
};
|
||||
|
||||
} // namespace shaka
|
||||
|
|
|
@ -119,9 +119,18 @@ void MpdBuilder::AddBaseUrl(const std::string& base_url) {
|
|||
base_urls_.push_back(base_url);
|
||||
}
|
||||
|
||||
Period* MpdBuilder::AddPeriod() {
|
||||
periods_.emplace_back(new Period(mpd_options_, &adaptation_set_counter_,
|
||||
&representation_counter_));
|
||||
Period* MpdBuilder::GetOrCreatePeriod(double start_time_in_seconds) {
|
||||
for (auto& period : periods_) {
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
|
@ -46,9 +46,13 @@ class MpdBuilder {
|
|||
/// @param base_url URL for <BaseURL> entry.
|
||||
void AddBaseUrl(const std::string& base_url);
|
||||
|
||||
/// Adds <Period> to the MPD.
|
||||
/// @return The new period, which is owned by this instance.
|
||||
virtual Period* AddPeriod();
|
||||
/// Check the existing Periods, if there is one matching the provided
|
||||
/// @a start_time_in_seconds, return it; otherwise a new Period is created and
|
||||
/// 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.
|
||||
/// @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::string availability_start_time_;
|
||||
|
||||
base::AtomicSequenceNumber period_counter_;
|
||||
base::AtomicSequenceNumber adaptation_set_counter_;
|
||||
base::AtomicSequenceNumber representation_counter_;
|
||||
|
||||
|
|
|
@ -37,7 +37,8 @@ class MpdBuilderTest : public ::testing::Test {
|
|||
~MpdBuilderTest() override {}
|
||||
|
||||
void SetUp() override {
|
||||
period_ = mpd_.AddPeriod();
|
||||
const double kPeriodStartTimeSeconds = 0.0;
|
||||
period_ = mpd_.GetOrCreatePeriod(kPeriodStartTimeSeconds);
|
||||
ASSERT_TRUE(period_);
|
||||
}
|
||||
|
||||
|
@ -149,6 +150,26 @@ TEST_F(OnDemandMpdBuilderTest, MediaInfoMissingBandwidth) {
|
|||
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.
|
||||
// This test must use ASSERT_EQ for comparison because XmlEqual() cannot
|
||||
// handle namespaces correctly yet.
|
||||
|
|
|
@ -34,10 +34,14 @@ std::set<std::string> GetUUIDs(
|
|||
|
||||
} // 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* 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),
|
||||
representation_counter_(representation_counter) {}
|
||||
|
||||
|
@ -92,9 +96,8 @@ AdaptationSet* Period::GetOrCreateAdaptationSet(
|
|||
xml::scoped_xml_ptr<xmlNode> Period::GetXml() {
|
||||
xml::XmlNode period("Period");
|
||||
|
||||
// Always set id=0 for now.
|
||||
// Required for 'dynamic' MPDs.
|
||||
period.SetId(0);
|
||||
period.SetId(id_);
|
||||
// Iterate thru AdaptationSets and add them to one big Period element.
|
||||
for (const auto& adaptation_set : adaptation_sets_) {
|
||||
xml::scoped_xml_ptr<xmlNode> child(adaptation_set->GetXml());
|
||||
|
@ -102,10 +105,10 @@ xml::scoped_xml_ptr<xmlNode> Period::GetXml() {
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
// TODO(kqyang): Should we set @start unconditionally to 0?
|
||||
if (mpd_options_.mpd_type == MpdType::kDynamic) {
|
||||
// This is the only Period and it is a regular period.
|
||||
period.SetStringAttribute("start", "PT0S");
|
||||
if (mpd_options_.mpd_type == MpdType::kDynamic ||
|
||||
start_time_in_seconds_ != 0) {
|
||||
period.SetStringAttribute("start",
|
||||
SecondsToXmlDuration(start_time_in_seconds_));
|
||||
}
|
||||
return period.PassScopedPtr();
|
||||
}
|
||||
|
|
|
@ -53,13 +53,20 @@ class Period {
|
|||
/// @return The list of AdaptationSets in this Period.
|
||||
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:
|
||||
/// @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 adaptation_set_counter is a counter for assigning ID numbers to
|
||||
/// AdaptationSet. It can not be NULL.
|
||||
/// @param representation_counter is a counter for assigning ID numbers to
|
||||
/// 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* representation_counter);
|
||||
|
||||
|
@ -93,6 +100,8 @@ class Period {
|
|||
const MediaInfo& media_info,
|
||||
uint32_t* original_adaptation_set_id);
|
||||
|
||||
const uint32_t id_;
|
||||
const double start_time_in_seconds_;
|
||||
const MpdOptions& mpd_options_;
|
||||
base::AtomicSequenceNumber* const adaptation_set_counter_;
|
||||
base::AtomicSequenceNumber* const representation_counter_;
|
||||
|
|
|
@ -25,6 +25,8 @@ using ::testing::UnorderedElementsAre;
|
|||
|
||||
namespace shaka {
|
||||
namespace {
|
||||
const uint32_t kDefaultPeriodId = 9u;
|
||||
const double kDefaultPeriodStartTime = 5.6;
|
||||
const uint32_t kDefaultAdaptationSetId = 0u;
|
||||
const uint32_t kTrickPlayAdaptationSetId = 1u;
|
||||
|
||||
|
@ -75,7 +77,11 @@ MATCHER_P(ContentProtectionElementEq, expected, "") {
|
|||
class TestablePeriod : public Period {
|
||||
public:
|
||||
TestablePeriod(const MpdOptions& mpd_options)
|
||||
: Period(mpd_options, &sequence_number_, &sequence_number_) {}
|
||||
: Period(kDefaultPeriodId,
|
||||
kDefaultPeriodStartTime,
|
||||
mpd_options,
|
||||
&sequence_number_,
|
||||
&sequence_number_) {}
|
||||
|
||||
MOCK_METHOD4(NewAdaptationSet,
|
||||
std::unique_ptr<AdaptationSet>(
|
||||
|
@ -133,7 +139,7 @@ TEST_P(PeriodTest, GetXml) {
|
|||
content_protection_in_adaptation_set_));
|
||||
|
||||
const char kExpectedXml[] =
|
||||
"<Period id=\"0\">"
|
||||
"<Period id=\"9\" start=\"PT5.6S\">"
|
||||
// ContentType and Representation elements are populated after
|
||||
// Representation::Init() is called.
|
||||
" <AdaptationSet id=\"0\" contentType=\"\"/>"
|
||||
|
@ -164,7 +170,7 @@ TEST_P(PeriodTest, DynamicMpdGetXml) {
|
|||
content_protection_in_adaptation_set_));
|
||||
|
||||
const char kExpectedXml[] =
|
||||
"<Period id=\"0\" start=\"PT0S\">"
|
||||
"<Period id=\"9\" start=\"PT5.6S\">"
|
||||
// ContentType and Representation elements are populated after
|
||||
// Representation::Init() is called.
|
||||
" <AdaptationSet id=\"0\" contentType=\"\"/>"
|
||||
|
|
|
@ -115,6 +115,24 @@ Representation::Representation(
|
|||
state_change_listener_(std::move(state_change_listener)),
|
||||
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() {}
|
||||
|
||||
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
|
||||
// "Representation" node.
|
||||
// MPD schema has strict ordering. The following must be done in order.
|
||||
|
|
|
@ -112,6 +112,9 @@ class Representation {
|
|||
/// @param sample_duration is the duration of a sample.
|
||||
virtual void SetSampleDuration(uint32_t sample_duration);
|
||||
|
||||
/// @return MediaInfo for the Representation.
|
||||
virtual const MediaInfo& GetMediaInfo() const;
|
||||
|
||||
/// @return Copy of <Representation>.
|
||||
xml::scoped_xml_ptr<xmlNode> GetXml();
|
||||
|
||||
|
@ -150,6 +153,16 @@ class Representation {
|
|||
uint32_t representation_id,
|
||||
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:
|
||||
Representation(const Representation&) = delete;
|
||||
Representation& operator=(const Representation&) = delete;
|
||||
|
@ -181,6 +194,7 @@ class Representation {
|
|||
// any logic using this can assume only one set.
|
||||
MediaInfo media_info_;
|
||||
std::list<ContentProtectionElement> content_protection_elements_;
|
||||
// TODO(kqyang): Address sliding window issue with multiple periods.
|
||||
std::list<SegmentInfo> segment_infos_;
|
||||
|
||||
const uint32_t id_;
|
||||
|
|
|
@ -52,6 +52,16 @@ class RepresentationTest : public ::testing::Test {
|
|||
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() {
|
||||
return std::unique_ptr<RepresentationStateChangeListener>();
|
||||
}
|
||||
|
@ -484,6 +494,35 @@ TEST_F(SegmentTemplateTest, OneSegmentNormal) {
|
|||
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) {
|
||||
double earliest_timestamp;
|
||||
// No segments.
|
||||
|
|
|
@ -41,32 +41,17 @@ bool SimpleMpdNotifier::NotifyNewContainer(const MediaInfo& media_info,
|
|||
if (content_type == kContentTypeUnknown)
|
||||
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);
|
||||
MpdBuilder::MakePathsRelativeToMpd(output_path_, &adjusted_media_info);
|
||||
Representation* representation =
|
||||
adaptation_set->AddRepresentation(adjusted_media_info);
|
||||
const Representation* kNoOriginalRepresentation = nullptr;
|
||||
const double kPeriodStartTimeSeconds = 0.0;
|
||||
|
||||
base::AutoLock auto_lock(lock_);
|
||||
const Representation* representation = AddRepresentationToPeriod(
|
||||
adjusted_media_info, kNoOriginalRepresentation, kPeriodStartTimeSeconds);
|
||||
if (!representation)
|
||||
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();
|
||||
DCHECK(!ContainsKey(representation_map_, representation->id()));
|
||||
representation_map_[representation->id()] = representation;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -98,9 +83,31 @@ bool SimpleMpdNotifier::NotifyNewSegment(uint32_t container_id,
|
|||
|
||||
bool SimpleMpdNotifier::NotifyCueEvent(uint32_t container_id,
|
||||
uint64_t timestamp) {
|
||||
NOTIMPLEMENTED();
|
||||
base::AutoLock auto_lock(lock_);
|
||||
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(
|
||||
uint32_t container_id,
|
||||
|
@ -131,4 +138,38 @@ bool SimpleMpdNotifier::Flush() {
|
|||
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
|
||||
|
|
|
@ -20,7 +20,6 @@ namespace shaka {
|
|||
|
||||
class AdaptationSet;
|
||||
class MpdBuilder;
|
||||
class Period;
|
||||
class Representation;
|
||||
|
||||
struct MpdOptions;
|
||||
|
@ -39,7 +38,7 @@ class SimpleMpdNotifier : public MpdNotifier {
|
|||
bool NotifyNewContainer(const MediaInfo& media_info, uint32_t* id) override;
|
||||
bool NotifySampleDuration(uint32_t container_id,
|
||||
uint32_t sample_duration) override;
|
||||
bool NotifyNewSegment(uint32_t id,
|
||||
bool NotifyNewSegment(uint32_t container_id,
|
||||
uint64_t start_time,
|
||||
uint64_t duration,
|
||||
uint64_t size) override;
|
||||
|
@ -57,6 +56,17 @@ class SimpleMpdNotifier : public MpdNotifier {
|
|||
|
||||
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.
|
||||
MpdBuilder* MpdBuilderForTesting() const { return mpd_builder_.get(); }
|
||||
|
||||
|
@ -68,7 +78,6 @@ class SimpleMpdNotifier : public MpdNotifier {
|
|||
// MPD output path.
|
||||
std::string output_path_;
|
||||
std::unique_ptr<MpdBuilder> mpd_builder_;
|
||||
Period* period_ = nullptr; // owned by |mpd_builder_|.
|
||||
bool content_protection_in_adaptation_set_ = true;
|
||||
base::Lock lock_;
|
||||
|
||||
|
|
|
@ -20,11 +20,16 @@ namespace shaka {
|
|||
|
||||
using ::testing::_;
|
||||
using ::testing::Eq;
|
||||
using ::testing::Ref;
|
||||
using ::testing::Return;
|
||||
using ::testing::ReturnRef;
|
||||
using ::testing::StrEq;
|
||||
|
||||
namespace {
|
||||
const uint32_t kDefaultPeriodId = 0u;
|
||||
const double kDefaultPeriodStartTime = 0.0;
|
||||
const uint32_t kDefaultAdaptationSetId = 0u;
|
||||
const uint32_t kDefaultTimeScale = 10;
|
||||
const bool kContentProtectionInAdaptationSet = true;
|
||||
|
||||
MATCHER_P(EqualsProto, message, "") {
|
||||
|
@ -36,7 +41,8 @@ MATCHER_P(EqualsProto, message, "") {
|
|||
class SimpleMpdNotifierTest : public ::testing::Test {
|
||||
protected:
|
||||
SimpleMpdNotifierTest()
|
||||
: default_mock_period_(new MockPeriod),
|
||||
: default_mock_period_(
|
||||
new MockPeriod(kDefaultPeriodId, kDefaultPeriodStartTime)),
|
||||
default_mock_adaptation_set_(
|
||||
new MockAdaptationSet(kDefaultAdaptationSetId)) {}
|
||||
|
||||
|
@ -57,6 +63,7 @@ class SimpleMpdNotifierTest : public ::testing::Test {
|
|||
"}\n"
|
||||
"container_type: 1\n";
|
||||
valid_media_info1_ = ConvertToMediaInfo(kValidMediaInfo);
|
||||
valid_media_info1_.set_reference_time_scale(kDefaultTimeScale);
|
||||
valid_media_info2_ = valid_media_info1_;
|
||||
valid_media_info2_.mutable_video_info()->set_width(960);
|
||||
valid_media_info3_ = valid_media_info1_;
|
||||
|
@ -100,7 +107,7 @@ TEST_F(SimpleMpdNotifierTest, NotifyNewContainer) {
|
|||
std::unique_ptr<MockRepresentation> mock_representation(
|
||||
new MockRepresentation(kRepresentationId));
|
||||
|
||||
EXPECT_CALL(*mock_mpd_builder, AddPeriod())
|
||||
EXPECT_CALL(*mock_mpd_builder, GetOrCreatePeriod(_))
|
||||
.WillOnce(Return(default_mock_period_.get()));
|
||||
EXPECT_CALL(*default_mock_period_,
|
||||
GetOrCreateAdaptationSet(EqualsProto(valid_media_info1_), _))
|
||||
|
@ -128,7 +135,7 @@ TEST_F(SimpleMpdNotifierTest, NotifySampleDuration) {
|
|||
std::unique_ptr<MockRepresentation> mock_representation(
|
||||
new MockRepresentation(kRepresentationId));
|
||||
|
||||
EXPECT_CALL(*mock_mpd_builder, AddPeriod())
|
||||
EXPECT_CALL(*mock_mpd_builder, GetOrCreatePeriod(_))
|
||||
.WillOnce(Return(default_mock_period_.get()));
|
||||
EXPECT_CALL(*default_mock_period_, GetOrCreateAdaptationSet(_, _))
|
||||
.WillOnce(Return(default_mock_adaptation_set_.get()));
|
||||
|
@ -168,7 +175,7 @@ TEST_F(SimpleMpdNotifierTest, NotifyNewSegment) {
|
|||
std::unique_ptr<MockRepresentation> mock_representation(
|
||||
new MockRepresentation(kRepresentationId));
|
||||
|
||||
EXPECT_CALL(*mock_mpd_builder, AddPeriod())
|
||||
EXPECT_CALL(*mock_mpd_builder, GetOrCreatePeriod(_))
|
||||
.WillOnce(Return(default_mock_period_.get()));
|
||||
EXPECT_CALL(*default_mock_period_, GetOrCreateAdaptationSet(_, _))
|
||||
.WillOnce(Return(default_mock_adaptation_set_.get()));
|
||||
|
@ -190,6 +197,58 @@ TEST_F(SimpleMpdNotifierTest, NotifyNewSegment) {
|
|||
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(¬ifier, 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,
|
||||
ContentProtectionInAdaptationSetUpdateEncryption) {
|
||||
MpdOptions mpd_options = empty_mpd_option_;
|
||||
|
@ -202,7 +261,7 @@ TEST_F(SimpleMpdNotifierTest,
|
|||
std::unique_ptr<MockRepresentation> mock_representation(
|
||||
new MockRepresentation(kRepresentationId));
|
||||
|
||||
EXPECT_CALL(*mock_mpd_builder, AddPeriod())
|
||||
EXPECT_CALL(*mock_mpd_builder, GetOrCreatePeriod(_))
|
||||
.WillOnce(Return(default_mock_period_.get()));
|
||||
EXPECT_CALL(
|
||||
*default_mock_period_,
|
||||
|
@ -245,7 +304,7 @@ TEST_F(SimpleMpdNotifierTest,
|
|||
std::unique_ptr<MockRepresentation> mock_representation(
|
||||
new MockRepresentation(kRepresentationId));
|
||||
|
||||
EXPECT_CALL(*mock_mpd_builder, AddPeriod())
|
||||
EXPECT_CALL(*mock_mpd_builder, GetOrCreatePeriod(_))
|
||||
.WillOnce(Return(default_mock_period_.get()));
|
||||
EXPECT_CALL(
|
||||
*default_mock_period_,
|
||||
|
@ -291,8 +350,9 @@ TEST_F(SimpleMpdNotifierTest, MultipleMediaInfo) {
|
|||
std::unique_ptr<MockRepresentation> representation3(
|
||||
new MockRepresentation(3));
|
||||
|
||||
EXPECT_CALL(*mock_mpd_builder, AddPeriod())
|
||||
.WillOnce(Return(default_mock_period_.get()));
|
||||
EXPECT_CALL(*mock_mpd_builder, GetOrCreatePeriod(_))
|
||||
.Times(3)
|
||||
.WillRepeatedly(Return(default_mock_period_.get()));
|
||||
|
||||
EXPECT_CALL(*default_mock_period_,
|
||||
GetOrCreateAdaptationSet(EqualsProto(valid_media_info1_), _))
|
||||
|
|
|
@ -300,6 +300,11 @@ bool RepresentationXmlNode::AddVODOnlyInfo(const MediaInfo& media_info) {
|
|||
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()) {
|
||||
XmlNode initialization("Initialization");
|
||||
initialization.SetStringAttribute("range",
|
||||
|
@ -326,6 +331,11 @@ bool RepresentationXmlNode::AddLiveOnlyInfo(
|
|||
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()) {
|
||||
// The spec does not allow '$Number$' and '$Time$' in initialization
|
||||
// attribute.
|
||||
|
|
|
@ -28,7 +28,9 @@ bool XmlEqual(const std::string& xml1, xmlNodePtr xml2);
|
|||
std::string XmlNodeToString(xmlNodePtr xml_node);
|
||||
|
||||
/// 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);
|
||||
return XmlEqual(xml, arg);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue