Support removing segments outside of live window in HLS
Issue: #223 Change-Id: Ib91c60268d8df9adbaf5f6cac77eaebd6a3edb6e
This commit is contained in:
parent
224b597b48
commit
37d4ff017f
|
@ -18,6 +18,7 @@
|
|||
#include "packager/file/file.h"
|
||||
#include "packager/hls/base/tag.h"
|
||||
#include "packager/media/base/language_utils.h"
|
||||
#include "packager/media/base/muxer_util.h"
|
||||
#include "packager/version/version.h"
|
||||
|
||||
namespace shaka {
|
||||
|
@ -577,7 +578,6 @@ void MediaPlaylist::SlideWindow() {
|
|||
HlsEntry::EntryType prev_entry_type = HlsEntry::EntryType::kExtInf;
|
||||
|
||||
std::list<std::unique_ptr<HlsEntry>>::iterator last = entries_.begin();
|
||||
size_t num_segments_removed = 0;
|
||||
for (; last != entries_.end(); ++last) {
|
||||
HlsEntry::EntryType entry_type = last->get()->type();
|
||||
if (entry_type == HlsEntry::EntryType::kExtKey) {
|
||||
|
@ -588,13 +588,14 @@ void MediaPlaylist::SlideWindow() {
|
|||
++discontinuity_sequence_number_;
|
||||
} else {
|
||||
DCHECK_EQ(entry_type, HlsEntry::EntryType::kExtInf);
|
||||
const SegmentInfoEntry* segment_info =
|
||||
reinterpret_cast<SegmentInfoEntry*>(last->get());
|
||||
const SegmentInfoEntry& segment_info =
|
||||
*reinterpret_cast<SegmentInfoEntry*>(last->get());
|
||||
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)
|
||||
break;
|
||||
++num_segments_removed;
|
||||
RemoveOldSegment(segment_info.start_time());
|
||||
media_sequence_number_++;
|
||||
}
|
||||
prev_entry_type = entry_type;
|
||||
}
|
||||
|
@ -602,7 +603,23 @@ void MediaPlaylist::SlideWindow() {
|
|||
// Add key entries back.
|
||||
entries_.insert(entries_.begin(), std::make_move_iterator(ext_x_keys.begin()),
|
||||
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
|
||||
|
|
|
@ -188,8 +188,12 @@ class MediaPlaylist {
|
|||
// Remove elements from |entries_| for live profile. Increments
|
||||
// |sequence_number_| by the number of segments removed.
|
||||
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.
|
||||
const std::string file_name_;
|
||||
const std::string name_;
|
||||
|
@ -218,6 +222,9 @@ class MediaPlaylist {
|
|||
uint32_t target_duration_ = 0;
|
||||
|
||||
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).
|
||||
struct KeyFrameInfo {
|
||||
|
|
|
@ -7,6 +7,9 @@
|
|||
#include <gmock/gmock.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/hls/base/media_playlist.h"
|
||||
#include "packager/version/version.h"
|
||||
|
@ -42,10 +45,9 @@ class MediaPlaylistTest : public ::testing::Test {
|
|||
: default_file_name_(kDefaultPlaylistFileName),
|
||||
default_name_("default_name"),
|
||||
default_group_id_("default_group_id") {
|
||||
HlsParams hls_params;
|
||||
hls_params.playlist_type = type;
|
||||
hls_params.time_shift_buffer_depth = kTimeShiftBufferDepth;
|
||||
media_playlist_.reset(new MediaPlaylist(hls_params, default_file_name_,
|
||||
hls_params_.playlist_type = type;
|
||||
hls_params_.time_shift_buffer_depth = kTimeShiftBufferDepth;
|
||||
media_playlist_.reset(new MediaPlaylist(hls_params_, default_file_name_,
|
||||
default_name_, default_group_id_));
|
||||
}
|
||||
|
||||
|
@ -65,9 +67,12 @@ class MediaPlaylistTest : public ::testing::Test {
|
|||
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_name_;
|
||||
const std::string default_group_id_;
|
||||
HlsParams hls_params_;
|
||||
std::unique_ptr<MediaPlaylist> media_playlist_;
|
||||
|
||||
MediaInfo valid_video_media_info_;
|
||||
|
@ -870,5 +875,84 @@ TEST_F(IFrameMediaPlaylistTest, MultiSegment) {
|
|||
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 shaka
|
||||
|
|
|
@ -31,6 +31,13 @@ struct HlsParams {
|
|||
/// Defines the live window, or the guaranteed duration of the time shifting
|
||||
/// buffer for 'live' playlists.
|
||||
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"
|
||||
/// key formats. Ignored if the playlist is not encrypted or not using the
|
||||
/// above key formats.
|
||||
|
|
|
@ -498,6 +498,7 @@ void Representation::RemoveSegments(uint64_t start_time,
|
|||
}
|
||||
while (segments_to_be_removed_.size() >
|
||||
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());
|
||||
segments_to_be_removed_.pop_front();
|
||||
}
|
||||
|
|
|
@ -212,7 +212,8 @@ class Representation {
|
|||
std::list<ContentProtectionElement> content_protection_elements_;
|
||||
// TODO(kqyang): Address sliding window issue with multiple periods.
|
||||
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_;
|
||||
|
||||
const uint32_t id_;
|
||||
|
|
|
@ -27,12 +27,6 @@ namespace {
|
|||
|
||||
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
|
||||
: public RepresentationStateChangeListener {
|
||||
public:
|
||||
|
@ -1185,86 +1179,106 @@ TEST_P(TimeShiftBufferDepthTest, ManySegments) {
|
|||
XmlNodeEqual(ExpectedXml(expected_s_element, kExpectedStartNumber)));
|
||||
}
|
||||
|
||||
TEST_P(TimeShiftBufferDepthTest, DeleteSegmentsOutsideOfLiveWindow) {
|
||||
const char kSegmentTemplate[] = "memory://$Number$.mp4";
|
||||
const char kStringPrintTemplate[] = "memory://%d.mp4";
|
||||
INSTANTIATE_TEST_CASE_P(InitialStartTime,
|
||||
TimeShiftBufferDepthTest,
|
||||
Values(0, 1000));
|
||||
|
||||
// Create 100 files with the template.
|
||||
for (int i = 1; i <= 100; ++i) {
|
||||
File::WriteStringToFile(base::StringPrintf(kStringPrintTemplate, i).c_str(),
|
||||
"dummy content");
|
||||
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.
|
||||
for (int i = 1; i <= 100; ++i) {
|
||||
File::WriteStringToFile(
|
||||
base::StringPrintf(kStringPrintTemplate, i).c_str(), "dummy content");
|
||||
}
|
||||
|
||||
MediaInfo media_info = ConvertToMediaInfo(GetDefaultMediaInfo());
|
||||
media_info.set_segment_template(kSegmentTemplate);
|
||||
media_info.set_segment_template_url(kSegmentTemplateUrl);
|
||||
representation_ =
|
||||
CreateRepresentation(media_info, kAnyRepresentationId, NoListener());
|
||||
ASSERT_TRUE(representation_->Init());
|
||||
|
||||
mpd_options_.mpd_params.time_shift_buffer_depth = kTimeShiftBufferDepth;
|
||||
mpd_options_.mpd_params.preserved_segments_outside_live_window =
|
||||
kNumPreservedSegmentsOutsideLiveWindow;
|
||||
}
|
||||
|
||||
MediaInfo media_info = ConvertToMediaInfo(GetDefaultMediaInfo());
|
||||
media_info.set_segment_template(kSegmentTemplate);
|
||||
representation_ =
|
||||
CreateRepresentation(media_info, kAnyRepresentationId, NoListener());
|
||||
ASSERT_TRUE(representation_->Init());
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
const int kTimeShiftBufferDepth = 2;
|
||||
const int kNumPreservedSegmentsOutsideLiveWindow = 3;
|
||||
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;
|
||||
|
||||
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.
|
||||
// Verify that no segments are deleted initially until there are more than
|
||||
// |kMaxNumSegmentsAvailable| segments.
|
||||
TEST_F(RepresentationDeleteSegmentsTest, NoSegmentsDeletedInitially) {
|
||||
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(kInitialStartTime + i * kDuration, kDuration, kSize, kNoRepeat);
|
||||
}
|
||||
for (int i = 0; i < kMaxNumSegmentsAvailable; ++i) {
|
||||
EXPECT_FALSE(
|
||||
SegmentDeleted(base::StringPrintf(kStringPrintTemplate, i + 1)));
|
||||
}
|
||||
}
|
||||
|
||||
AddSegments(next_start_time, kDuration, kSize, kNoRepeat);
|
||||
next_start_time += kDuration;
|
||||
++num_total_segments;
|
||||
last_available_segment_index = 2;
|
||||
|
||||
EXPECT_FALSE(SegmentDeleted(
|
||||
base::StringPrintf(kStringPrintTemplate, last_available_segment_index)));
|
||||
EXPECT_TRUE(SegmentDeleted(base::StringPrintf(
|
||||
kStringPrintTemplate, last_available_segment_index - 1)));
|
||||
|
||||
// Verify that there are exactly |kMaxNumSegmentsAvailable| segments
|
||||
// remaining.
|
||||
const uint64_t kRepeat = 10;
|
||||
AddSegments(next_start_time, kDuration, kSize, kRepeat);
|
||||
next_start_time += (kRepeat + 1) * kDuration;
|
||||
num_total_segments += kRepeat + 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;
|
||||
TEST_F(RepresentationDeleteSegmentsTest, OneSegmentDeleted) {
|
||||
for (int i = 0; i <= kMaxNumSegmentsAvailable; ++i) {
|
||||
AddSegments(kInitialStartTime + i * kDuration, kDuration, kSize, kNoRepeat);
|
||||
}
|
||||
EXPECT_FALSE(SegmentDeleted(base::StringPrintf(kStringPrintTemplate, 2)));
|
||||
EXPECT_TRUE(SegmentDeleted(base::StringPrintf(kStringPrintTemplate, 1)));
|
||||
}
|
||||
|
||||
// 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(
|
||||
base::StringPrintf(kStringPrintTemplate, last_available_segment_index)));
|
||||
EXPECT_TRUE(SegmentDeleted(base::StringPrintf(
|
||||
kStringPrintTemplate, last_available_segment_index - 1)));
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(InitialStartTime,
|
||||
TimeShiftBufferDepthTest,
|
||||
Values(0, 1000));
|
||||
// Verify that segments are deleted as expected with many repeating segments.
|
||||
TEST_F(RepresentationDeleteSegmentsTest, ManyRepeatingSegments) {
|
||||
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
|
||||
|
|
Loading…
Reference in New Issue