Support removing segments outside of live window in HLS

Issue: #223

Change-Id: Ib91c60268d8df9adbaf5f6cac77eaebd6a3edb6e
This commit is contained in:
KongQun Yang 2018-04-17 15:03:02 -07:00
parent 224b597b48
commit 37d4ff017f
7 changed files with 216 additions and 85 deletions

View File

@ -18,6 +18,7 @@
#include "packager/file/file.h" #include "packager/file/file.h"
#include "packager/hls/base/tag.h" #include "packager/hls/base/tag.h"
#include "packager/media/base/language_utils.h" #include "packager/media/base/language_utils.h"
#include "packager/media/base/muxer_util.h"
#include "packager/version/version.h" #include "packager/version/version.h"
namespace shaka { namespace shaka {
@ -577,7 +578,6 @@ void MediaPlaylist::SlideWindow() {
HlsEntry::EntryType prev_entry_type = HlsEntry::EntryType::kExtInf; HlsEntry::EntryType prev_entry_type = HlsEntry::EntryType::kExtInf;
std::list<std::unique_ptr<HlsEntry>>::iterator last = entries_.begin(); std::list<std::unique_ptr<HlsEntry>>::iterator last = entries_.begin();
size_t num_segments_removed = 0;
for (; last != entries_.end(); ++last) { for (; last != entries_.end(); ++last) {
HlsEntry::EntryType entry_type = last->get()->type(); HlsEntry::EntryType entry_type = last->get()->type();
if (entry_type == HlsEntry::EntryType::kExtKey) { if (entry_type == HlsEntry::EntryType::kExtKey) {
@ -588,13 +588,14 @@ void MediaPlaylist::SlideWindow() {
++discontinuity_sequence_number_; ++discontinuity_sequence_number_;
} else { } else {
DCHECK_EQ(entry_type, HlsEntry::EntryType::kExtInf); DCHECK_EQ(entry_type, HlsEntry::EntryType::kExtInf);
const SegmentInfoEntry* segment_info = const SegmentInfoEntry& segment_info =
reinterpret_cast<SegmentInfoEntry*>(last->get()); *reinterpret_cast<SegmentInfoEntry*>(last->get());
const double last_segment_end_time = const double last_segment_end_time =
segment_info->start_time() + segment_info->duration(); segment_info.start_time() + segment_info.duration();
if (timeshift_limit < last_segment_end_time) if (timeshift_limit < last_segment_end_time)
break; break;
++num_segments_removed; RemoveOldSegment(segment_info.start_time());
media_sequence_number_++;
} }
prev_entry_type = entry_type; prev_entry_type = entry_type;
} }
@ -602,7 +603,23 @@ void MediaPlaylist::SlideWindow() {
// Add key entries back. // Add key entries back.
entries_.insert(entries_.begin(), std::make_move_iterator(ext_x_keys.begin()), entries_.insert(entries_.begin(), std::make_move_iterator(ext_x_keys.begin()),
std::make_move_iterator(ext_x_keys.end())); std::make_move_iterator(ext_x_keys.end()));
media_sequence_number_ += num_segments_removed; }
void MediaPlaylist::RemoveOldSegment(uint64_t start_time) {
if (hls_params_.preserved_segments_outside_live_window == 0)
return;
if (stream_type_ == MediaPlaylistStreamType::kVideoIFramesOnly)
return;
segments_to_be_removed_.push_back(
media::GetSegmentName(media_info_.segment_template(), start_time,
media_sequence_number_, media_info_.bandwidth()));
while (segments_to_be_removed_.size() >
hls_params_.preserved_segments_outside_live_window) {
VLOG(2) << "Deleting " << segments_to_be_removed_.front();
File::Delete(segments_to_be_removed_.front().c_str());
segments_to_be_removed_.pop_front();
}
} }
} // namespace hls } // namespace hls

View File

@ -188,8 +188,12 @@ class MediaPlaylist {
// Remove elements from |entries_| for live profile. Increments // Remove elements from |entries_| for live profile. Increments
// |sequence_number_| by the number of segments removed. // |sequence_number_| by the number of segments removed.
void SlideWindow(); void SlideWindow();
// Remove the segment specified by |start_time|. The actual deletion can
// happen at a later time depending on the value of
// |preserved_segment_outside_live_window| in |hls_params_|.
void RemoveOldSegment(uint64_t start_time);
const HlsParams hls_params_; const HlsParams& hls_params_;
// Mainly for MasterPlaylist to use these values. // Mainly for MasterPlaylist to use these values.
const std::string file_name_; const std::string file_name_;
const std::string name_; const std::string name_;
@ -218,6 +222,9 @@ class MediaPlaylist {
uint32_t target_duration_ = 0; uint32_t target_duration_ = 0;
std::list<std::unique_ptr<HlsEntry>> entries_; std::list<std::unique_ptr<HlsEntry>> entries_;
// A list to hold the file names of the segments to be removed temporarily.
// Once a file is actually removed, it is removed from the list.
std::list<std::string> segments_to_be_removed_;
// Used by kVideoIFrameOnly playlists to track the i-frames (key frames). // Used by kVideoIFrameOnly playlists to track the i-frames (key frames).
struct KeyFrameInfo { struct KeyFrameInfo {

View File

@ -7,6 +7,9 @@
#include <gmock/gmock.h> #include <gmock/gmock.h>
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include "packager/base/strings/stringprintf.h"
#include "packager/file/file.h"
#include "packager/file/file_closer.h"
#include "packager/file/file_test_util.h" #include "packager/file/file_test_util.h"
#include "packager/hls/base/media_playlist.h" #include "packager/hls/base/media_playlist.h"
#include "packager/version/version.h" #include "packager/version/version.h"
@ -42,10 +45,9 @@ class MediaPlaylistTest : public ::testing::Test {
: default_file_name_(kDefaultPlaylistFileName), : default_file_name_(kDefaultPlaylistFileName),
default_name_("default_name"), default_name_("default_name"),
default_group_id_("default_group_id") { default_group_id_("default_group_id") {
HlsParams hls_params; hls_params_.playlist_type = type;
hls_params.playlist_type = type; hls_params_.time_shift_buffer_depth = kTimeShiftBufferDepth;
hls_params.time_shift_buffer_depth = kTimeShiftBufferDepth; media_playlist_.reset(new MediaPlaylist(hls_params_, default_file_name_,
media_playlist_.reset(new MediaPlaylist(hls_params, default_file_name_,
default_name_, default_group_id_)); default_name_, default_group_id_));
} }
@ -65,9 +67,12 @@ class MediaPlaylistTest : public ::testing::Test {
valid_video_media_info_.set_reference_time_scale(kTimeScale); valid_video_media_info_.set_reference_time_scale(kTimeScale);
} }
HlsParams* mutable_hls_params() { return &hls_params_; }
const std::string default_file_name_; const std::string default_file_name_;
const std::string default_name_; const std::string default_name_;
const std::string default_group_id_; const std::string default_group_id_;
HlsParams hls_params_;
std::unique_ptr<MediaPlaylist> media_playlist_; std::unique_ptr<MediaPlaylist> media_playlist_;
MediaInfo valid_video_media_info_; MediaInfo valid_video_media_info_;
@ -870,5 +875,84 @@ TEST_F(IFrameMediaPlaylistTest, MultiSegment) {
ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput); ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput);
} }
namespace {
const int kNumPreservedSegmentsOutsideLiveWindow = 3;
const int kMaxNumSegmentsAvailable =
kTimeShiftBufferDepth + 1 + kNumPreservedSegmentsOutsideLiveWindow;
const char kSegmentTemplate[] = "memory://$Number$.mp4";
const char kSegmentTemplateUrl[] = "video/$Number$.mp4";
const char kStringPrintTemplate[] = "memory://%d.mp4";
const char kIgnoredSegmentName[] = "ignored_segment_name";
const uint64_t kInitialStartTime = 0;
const uint64_t kDuration = kTimeScale;
} // namespace
class MediaPlaylistDeleteSegmentsTest : public LiveMediaPlaylistTest {
public:
void SetUp() override {
LiveMediaPlaylistTest::SetUp();
// Create 100 files with the template.
for (int i = 1; i <= 100; ++i) {
File::WriteStringToFile(
base::StringPrintf(kStringPrintTemplate, i).c_str(), "dummy content");
}
valid_video_media_info_.set_segment_template(kSegmentTemplate);
valid_video_media_info_.set_segment_template_url(kSegmentTemplateUrl);
ASSERT_TRUE(media_playlist_->SetMediaInfo(valid_video_media_info_));
mutable_hls_params()->preserved_segments_outside_live_window =
kNumPreservedSegmentsOutsideLiveWindow;
}
bool SegmentDeleted(const std::string& segment_name) {
std::unique_ptr<File, FileCloser> file_closer(
File::Open(segment_name.c_str(), "r"));
return file_closer.get() == nullptr;
}
};
// Verify that no segments are deleted initially until there are more than
// |kMaxNumSegmentsAvailable| segments.
TEST_F(MediaPlaylistDeleteSegmentsTest, NoSegmentsDeletedInitially) {
for (int i = 0; i < kMaxNumSegmentsAvailable; ++i) {
media_playlist_->AddSegment(kIgnoredSegmentName,
kInitialStartTime + i * kDuration, kDuration,
kZeroByteOffset, kMBytes);
}
for (int i = 0; i < kMaxNumSegmentsAvailable; ++i) {
EXPECT_FALSE(
SegmentDeleted(base::StringPrintf(kStringPrintTemplate, i + 1)));
}
}
TEST_F(MediaPlaylistDeleteSegmentsTest, OneSegmentDeleted) {
for (int i = 0; i <= kMaxNumSegmentsAvailable; ++i) {
media_playlist_->AddSegment(kIgnoredSegmentName,
kInitialStartTime + i * kDuration, kDuration,
kZeroByteOffset, kMBytes);
}
EXPECT_FALSE(SegmentDeleted(base::StringPrintf(kStringPrintTemplate, 2)));
EXPECT_TRUE(SegmentDeleted(base::StringPrintf(kStringPrintTemplate, 1)));
}
TEST_F(MediaPlaylistDeleteSegmentsTest, ManySegments) {
int many_segments = 50;
for (int i = 0; i < many_segments; ++i) {
media_playlist_->AddSegment(kIgnoredSegmentName,
kInitialStartTime + i * kDuration, kDuration,
kZeroByteOffset, kMBytes);
}
const int last_available_segment_index =
many_segments - kMaxNumSegmentsAvailable + 1;
EXPECT_FALSE(SegmentDeleted(
base::StringPrintf(kStringPrintTemplate, last_available_segment_index)));
EXPECT_TRUE(SegmentDeleted(base::StringPrintf(
kStringPrintTemplate, last_available_segment_index - 1)));
}
} // namespace hls } // namespace hls
} // namespace shaka } // namespace shaka

View File

@ -31,6 +31,13 @@ struct HlsParams {
/// Defines the live window, or the guaranteed duration of the time shifting /// Defines the live window, or the guaranteed duration of the time shifting
/// buffer for 'live' playlists. /// buffer for 'live' playlists.
double time_shift_buffer_depth = 0; double time_shift_buffer_depth = 0;
/// Segments outside live window (defined by 'time_shift_buffer_depth' above)
/// are automatically removed except the latest number of segments defined by
/// this parameter. This is needed to accommodate latencies in various stages
/// of content serving pipeline, so that the segments stay accessible as they
/// may still be accessed by the player.
/// The segments are not removed if the value is zero.
size_t preserved_segments_outside_live_window = 0;
/// Defines the key uri for "identity" and "com.apple.streamingkeydelivery" /// Defines the key uri for "identity" and "com.apple.streamingkeydelivery"
/// key formats. Ignored if the playlist is not encrypted or not using the /// key formats. Ignored if the playlist is not encrypted or not using the
/// above key formats. /// above key formats.

View File

@ -498,6 +498,7 @@ void Representation::RemoveSegments(uint64_t start_time,
} }
while (segments_to_be_removed_.size() > while (segments_to_be_removed_.size() >
mpd_options_.mpd_params.preserved_segments_outside_live_window) { mpd_options_.mpd_params.preserved_segments_outside_live_window) {
VLOG(2) << "Deleting " << segments_to_be_removed_.front();
File::Delete(segments_to_be_removed_.front().c_str()); File::Delete(segments_to_be_removed_.front().c_str());
segments_to_be_removed_.pop_front(); segments_to_be_removed_.pop_front();
} }

View File

@ -212,7 +212,8 @@ class Representation {
std::list<ContentProtectionElement> content_protection_elements_; std::list<ContentProtectionElement> content_protection_elements_;
// TODO(kqyang): Address sliding window issue with multiple periods. // TODO(kqyang): Address sliding window issue with multiple periods.
std::list<SegmentInfo> segment_infos_; std::list<SegmentInfo> segment_infos_;
// A temporary list to hold the file names of segments to be removed. // A list to hold the file names of the segments to be removed temporarily.
// Once a file is actually removed, it is removed from the list.
std::list<std::string> segments_to_be_removed_; std::list<std::string> segments_to_be_removed_;
const uint32_t id_; const uint32_t id_;

View File

@ -27,12 +27,6 @@ namespace {
const uint32_t kAnyRepresentationId = 1; const uint32_t kAnyRepresentationId = 1;
bool SegmentDeleted(const std::string& segment_name) {
std::unique_ptr<File, FileCloser> file_closer(
File::Open(segment_name.c_str(), "r"));
return file_closer.get() == nullptr;
}
class MockRepresentationStateChangeListener class MockRepresentationStateChangeListener
: public RepresentationStateChangeListener { : public RepresentationStateChangeListener {
public: public:
@ -1185,86 +1179,106 @@ TEST_P(TimeShiftBufferDepthTest, ManySegments) {
XmlNodeEqual(ExpectedXml(expected_s_element, kExpectedStartNumber))); XmlNodeEqual(ExpectedXml(expected_s_element, kExpectedStartNumber)));
} }
TEST_P(TimeShiftBufferDepthTest, DeleteSegmentsOutsideOfLiveWindow) { INSTANTIATE_TEST_CASE_P(InitialStartTime,
const char kSegmentTemplate[] = "memory://$Number$.mp4"; TimeShiftBufferDepthTest,
const char kStringPrintTemplate[] = "memory://%d.mp4"; Values(0, 1000));
namespace {
const int kTimeShiftBufferDepth = 2;
const int kNumPreservedSegmentsOutsideLiveWindow = 3;
const int kMaxNumSegmentsAvailable =
kTimeShiftBufferDepth + 1 + kNumPreservedSegmentsOutsideLiveWindow;
const char kSegmentTemplate[] = "memory://$Number$.mp4";
const char kSegmentTemplateUrl[] = "video/$Number$.mp4";
const char kStringPrintTemplate[] = "memory://%d.mp4";
const uint64_t kInitialStartTime = 0;
const uint64_t kDuration = kDefaultTimeScale;
const uint64_t kSize = 10;
const uint64_t kNoRepeat = 0;
} // namespace
class RepresentationDeleteSegmentsTest : public SegmentTimelineTestBase {
public:
void SetUp() override {
SegmentTimelineTestBase::SetUp();
// Create 100 files with the template. // Create 100 files with the template.
for (int i = 1; i <= 100; ++i) { for (int i = 1; i <= 100; ++i) {
File::WriteStringToFile(base::StringPrintf(kStringPrintTemplate, i).c_str(), File::WriteStringToFile(
"dummy content"); base::StringPrintf(kStringPrintTemplate, i).c_str(), "dummy content");
} }
MediaInfo media_info = ConvertToMediaInfo(GetDefaultMediaInfo()); MediaInfo media_info = ConvertToMediaInfo(GetDefaultMediaInfo());
media_info.set_segment_template(kSegmentTemplate); media_info.set_segment_template(kSegmentTemplate);
media_info.set_segment_template_url(kSegmentTemplateUrl);
representation_ = representation_ =
CreateRepresentation(media_info, kAnyRepresentationId, NoListener()); CreateRepresentation(media_info, kAnyRepresentationId, NoListener());
ASSERT_TRUE(representation_->Init()); ASSERT_TRUE(representation_->Init());
const int kTimeShiftBufferDepth = 2; mpd_options_.mpd_params.time_shift_buffer_depth = kTimeShiftBufferDepth;
const int kNumPreservedSegmentsOutsideLiveWindow = 3; mpd_options_.mpd_params.preserved_segments_outside_live_window =
const int kMaxNumSegmentsAvailable =
kTimeShiftBufferDepth + 1 + kNumPreservedSegmentsOutsideLiveWindow;
mutable_mpd_options()->mpd_params.time_shift_buffer_depth =
kTimeShiftBufferDepth;
mutable_mpd_options()->mpd_params.preserved_segments_outside_live_window =
kNumPreservedSegmentsOutsideLiveWindow; kNumPreservedSegmentsOutsideLiveWindow;
const uint64_t kInitialStartTime = 0;
const uint64_t kDuration = kDefaultTimeScale;
const uint64_t kSize = 10;
const uint64_t kNoRepeat = 0;
// Verify that no segments are deleted initially until there are more than
// |kMaxNumSegmentsAvailable| segments.
uint64_t next_start_time = kInitialStartTime;
int num_total_segments = 0;
int last_available_segment_index = 1; // No segments are deleted.
for (int i = 0; i < kMaxNumSegmentsAvailable; ++i) {
AddSegments(next_start_time, kDuration, kSize, kNoRepeat);
next_start_time += kDuration;
++num_total_segments;
EXPECT_FALSE(SegmentDeleted(base::StringPrintf(
kStringPrintTemplate, last_available_segment_index)));
} }
AddSegments(next_start_time, kDuration, kSize, kNoRepeat); bool SegmentDeleted(const std::string& segment_name) {
next_start_time += kDuration; std::unique_ptr<File, FileCloser> file_closer(
++num_total_segments; File::Open(segment_name.c_str(), "r"));
last_available_segment_index = 2; return file_closer.get() == nullptr;
}
};
EXPECT_FALSE(SegmentDeleted( // Verify that no segments are deleted initially until there are more than
base::StringPrintf(kStringPrintTemplate, last_available_segment_index))); // |kMaxNumSegmentsAvailable| segments.
EXPECT_TRUE(SegmentDeleted(base::StringPrintf( TEST_F(RepresentationDeleteSegmentsTest, NoSegmentsDeletedInitially) {
kStringPrintTemplate, last_available_segment_index - 1))); for (int i = 0; i < kMaxNumSegmentsAvailable; ++i) {
AddSegments(kInitialStartTime + i * kDuration, kDuration, kSize, kNoRepeat);
}
for (int i = 0; i < kMaxNumSegmentsAvailable; ++i) {
EXPECT_FALSE(
SegmentDeleted(base::StringPrintf(kStringPrintTemplate, i + 1)));
}
}
// Verify that there are exactly |kMaxNumSegmentsAvailable| segments TEST_F(RepresentationDeleteSegmentsTest, OneSegmentDeleted) {
// remaining. for (int i = 0; i <= kMaxNumSegmentsAvailable; ++i) {
const uint64_t kRepeat = 10; AddSegments(kInitialStartTime + i * kDuration, kDuration, kSize, kNoRepeat);
AddSegments(next_start_time, kDuration, kSize, kRepeat); }
next_start_time += (kRepeat + 1) * kDuration; EXPECT_FALSE(SegmentDeleted(base::StringPrintf(kStringPrintTemplate, 2)));
num_total_segments += kRepeat + 1; EXPECT_TRUE(SegmentDeleted(base::StringPrintf(kStringPrintTemplate, 1)));
last_available_segment_index = }
num_total_segments - kMaxNumSegmentsAvailable + 1;
EXPECT_FALSE(SegmentDeleted(
base::StringPrintf(kStringPrintTemplate, last_available_segment_index)));
EXPECT_TRUE(SegmentDeleted(base::StringPrintf(
kStringPrintTemplate, last_available_segment_index - 1)));
AddSegments(next_start_time, kDuration, kSize, kNoRepeat);
++last_available_segment_index;
// Verify that segments are deleted as expected with many non-repeating
// segments.
TEST_F(RepresentationDeleteSegmentsTest, ManyNonRepeatingSegments) {
int many_segments = 50;
for (int i = 0; i < many_segments; ++i) {
AddSegments(kInitialStartTime + i * kDuration, kDuration, kSize, kNoRepeat);
}
const int last_available_segment_index =
many_segments - kMaxNumSegmentsAvailable + 1;
EXPECT_FALSE(SegmentDeleted( EXPECT_FALSE(SegmentDeleted(
base::StringPrintf(kStringPrintTemplate, last_available_segment_index))); base::StringPrintf(kStringPrintTemplate, last_available_segment_index)));
EXPECT_TRUE(SegmentDeleted(base::StringPrintf( EXPECT_TRUE(SegmentDeleted(base::StringPrintf(
kStringPrintTemplate, last_available_segment_index - 1))); kStringPrintTemplate, last_available_segment_index - 1)));
} }
INSTANTIATE_TEST_CASE_P(InitialStartTime, // Verify that segments are deleted as expected with many repeating segments.
TimeShiftBufferDepthTest, TEST_F(RepresentationDeleteSegmentsTest, ManyRepeatingSegments) {
Values(0, 1000)); const int kLoops = 4;
const uint64_t kRepeat = 10;
for (int i = 0; i < kLoops; ++i) {
AddSegments(kInitialStartTime + i * kDuration * (kRepeat + 1), kDuration,
kSize, kRepeat);
}
const int kNumSegments = kLoops * (kRepeat + 1);
const int last_available_segment_index =
kNumSegments - kMaxNumSegmentsAvailable + 1;
EXPECT_FALSE(SegmentDeleted(
base::StringPrintf(kStringPrintTemplate, last_available_segment_index)));
EXPECT_TRUE(SegmentDeleted(base::StringPrintf(
kStringPrintTemplate, last_available_segment_index - 1)));
}
} // namespace shaka } // namespace shaka