Support live and event HLS playlists

Closes #114

Change-Id: I6440383c3dc022fba5a9926839f79b9ed8a51f55
This commit is contained in:
KongQun Yang 2017-06-02 17:05:47 -07:00
parent e2401f02ec
commit b4aa34b803
26 changed files with 939 additions and 369 deletions

View File

@ -10,8 +10,12 @@ DEFINE_string(hls_master_playlist_output,
"",
"Output path for the master playlist for HLS. This flag must be"
"used to output HLS.");
DEFINE_string(hls_base_url,
"",
"The base URL for the Media Playlists and media files listed in "
"the playlists. This is the prefix for the files.");
DEFINE_string(hls_playlist_type,
"VOD",
"VOD, EVENT, or LIVE. This defines the EXT-X-PLAYLIST-TYPE in "
"the HLS specification. For hls_playlist_type of LIVE, "
"EXT-X-PLAYLIST-TYPE tag is omitted.");

View File

@ -11,5 +11,6 @@
DECLARE_string(hls_master_playlist_output);
DECLARE_string(hls_base_url);
DECLARE_string(hls_playlist_type);
#endif // PACKAGER_APP_HLS_FLAGS_H_

View File

@ -259,8 +259,19 @@ base::Optional<PackagingParams> GetPackagingParams() {
mpd_params.default_language = FLAGS_default_language;
HlsParams& hls_params = packaging_params.hls_params;
if (FLAGS_hls_playlist_type == "VOD") {
hls_params.playlist_type = HlsPlaylistType::kVod;
} else if (FLAGS_hls_playlist_type == "LIVE") {
hls_params.playlist_type = HlsPlaylistType::kLive;
} else if (FLAGS_hls_playlist_type == "EVENT") {
hls_params.playlist_type = HlsPlaylistType::kEvent;
} else {
LOG(ERROR) << "Unrecognized playlist type " << FLAGS_hls_playlist_type;
return base::nullopt;
}
hls_params.master_playlist_output = FLAGS_hls_master_playlist_output;
hls_params.base_url = FLAGS_hls_base_url;
hls_params.time_shift_buffer_depth = FLAGS_time_shift_buffer_depth;
TestParams& test_params = packaging_params.test_params;
test_params.dump_stream_info = FLAGS_dump_stream_info;

View File

@ -132,6 +132,8 @@ class PackagerAppTest(unittest.TestCase):
dash_if_iop=True,
output_media_info=False,
output_hls=False,
hls_playlist_type=None,
time_shift_buffer_depth=0.0,
generate_static_mpd=False,
use_fake_clock=True):
flags = []
@ -177,6 +179,12 @@ class PackagerAppTest(unittest.TestCase):
flags.append('--output_media_info')
elif output_hls:
flags += ['--hls_master_playlist_output', self.hls_master_playlist_output]
if hls_playlist_type:
flags += ['--hls_playlist_type', hls_playlist_type]
if time_shift_buffer_depth != 0.0:
flags += [
'--time_shift_buffer_depth={0}'.format(time_shift_buffer_depth)
]
else:
flags += ['--mpd_output', self.mpd_output]
@ -427,6 +435,58 @@ class PackagerFunctionalTest(PackagerAppTest):
self._DiffGold(
os.path.join(self.tmp_dir, 'video.m3u8'), 'bear-640x360-v-golden.m3u8')
def testPackageAvcTsLivePlaylist(self):
self.assertPackageSuccess(
self._GetStreams(
['audio', 'video'],
output_format='ts',
live=True,
test_files=['bear-640x360.ts']),
self._GetFlags(
output_hls=True,
hls_playlist_type='LIVE',
time_shift_buffer_depth=0.5))
self._DiffLiveGold(self.output[0],
'bear-640x360-a-golden',
output_format='ts')
self._DiffLiveGold(self.output[1],
'bear-640x360-v-golden',
output_format='ts')
self._DiffGold(self.hls_master_playlist_output,
'bear-640x360-av-master-golden.m3u8')
self._DiffGold(
os.path.join(self.tmp_dir, 'audio.m3u8'),
'bear-640x360-a-live-golden.m3u8')
self._DiffGold(
os.path.join(self.tmp_dir, 'video.m3u8'),
'bear-640x360-v-live-golden.m3u8')
def testPackageAvcTsEventPlaylist(self):
self.assertPackageSuccess(
self._GetStreams(
['audio', 'video'],
output_format='ts',
live=True,
test_files=['bear-640x360.ts']),
self._GetFlags(
output_hls=True,
hls_playlist_type='EVENT',
time_shift_buffer_depth=0.5))
self._DiffLiveGold(self.output[0],
'bear-640x360-a-golden',
output_format='ts')
self._DiffLiveGold(self.output[1],
'bear-640x360-v-golden',
output_format='ts')
self._DiffGold(self.hls_master_playlist_output,
'bear-640x360-av-master-golden.m3u8')
self._DiffGold(
os.path.join(self.tmp_dir, 'audio.m3u8'),
'bear-640x360-a-event-golden.m3u8')
self._DiffGold(
os.path.join(self.tmp_dir, 'video.m3u8'),
'bear-640x360-v-event-golden.m3u8')
def testPackageVp8Webm(self):
self.assertPackageSuccess(
self._GetStreams(['video'],

View File

@ -0,0 +1,11 @@
#EXTM3U
#EXT-X-VERSION:6
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
#EXT-X-TARGETDURATION:2
#EXT-X-PLAYLIST-TYPE:EVENT
#EXTINF:0.975,
output_audio-1.ts
#EXTINF:0.998,
output_audio-2.ts
#EXTINF:0.789,
output_audio-3.ts

View File

@ -0,0 +1,9 @@
#EXTM3U
#EXT-X-VERSION:6
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
#EXT-X-TARGETDURATION:2
#EXT-X-MEDIA-SEQUENCE:1
#EXTINF:0.998,
output_audio-2.ts
#EXTINF:0.789,
output_audio-3.ts

View File

@ -0,0 +1,11 @@
#EXTM3U
#EXT-X-VERSION:6
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
#EXT-X-TARGETDURATION:2
#EXT-X-PLAYLIST-TYPE:EVENT
#EXTINF:1.001,
output_video-1.ts
#EXTINF:1.001,
output_video-2.ts
#EXTINF:0.734,
output_video-3.ts

View File

@ -0,0 +1,9 @@
#EXTM3U
#EXT-X-VERSION:6
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
#EXT-X-TARGETDURATION:2
#EXT-X-MEDIA-SEQUENCE:1
#EXTINF:1.001,
output_video-2.ts
#EXTINF:0.734,
output_video-3.ts

View File

@ -15,14 +15,16 @@
namespace shaka {
namespace hls {
// TODO(rkuroiwa): Consider merging this with MpdNotifier.
class HlsNotifier {
public:
// TODO(kqyang): Combine with MediaPlaylistType in media_playlist.h.
enum class HlsProfile {
kOnDemandProfile,
kEventProfile,
kLiveProfile,
};
// TODO(rkuroiwa): Consider merging this with MpdNotifier.
class HlsNotifier {
public:
explicit HlsNotifier(HlsProfile profile) : profile_(profile) {}
virtual ~HlsNotifier() {}
@ -76,7 +78,7 @@ class HlsNotifier {
/// @return true on success, false otherwise.
virtual bool Flush() = 0;
protected:
/// @return the profile.
HlsProfile profile() const { return profile_; }
private:

View File

@ -8,9 +8,6 @@
#include <inttypes.h>
#include <cmath>
#include <set>
#include "packager/base/files/file_path.h"
#include "packager/base/strings/string_number_conversions.h"
#include "packager/base/strings/stringprintf.h"
@ -73,65 +70,8 @@ void MasterPlaylist::AddMediaPlaylist(MediaPlaylist* media_playlist) {
all_playlists_.push_back(media_playlist);
}
bool MasterPlaylist::WriteAllPlaylists(const std::string& base_url,
const std::string& output_dir) {
if (!WriteMasterPlaylist(base_url, output_dir)) {
LOG(ERROR) << "Failed to write master playlist.";
return false;
}
double longest_segment_duration = 0.0;
if (!has_set_playlist_target_duration_) {
for (const MediaPlaylist* playlist : all_playlists_) {
const double playlist_longest_segment =
playlist->GetLongestSegmentDuration();
if (longest_segment_duration < playlist_longest_segment)
longest_segment_duration = playlist_longest_segment;
}
}
base::FilePath output_path = base::FilePath::FromUTF8Unsafe(output_dir);
for (MediaPlaylist* playlist : all_playlists_) {
std::string file_path =
output_path
.Append(base::FilePath::FromUTF8Unsafe(playlist->file_name()))
.AsUTF8Unsafe();
if (!has_set_playlist_target_duration_) {
const bool set_target_duration = playlist->SetTargetDuration(
static_cast<uint32_t>(ceil(longest_segment_duration)));
LOG_IF(WARNING, !set_target_duration)
<< "Target duration was already set for " << file_path;
}
std::unique_ptr<media::File, media::FileCloser> file(
media::File::Open(file_path.c_str(), "w"));
if (!file) {
LOG(ERROR) << "Failed to open file " << file_path;
return false;
}
if (!playlist->WriteToFile(file.get())) {
LOG(ERROR) << "Failed to write playlist " << file_path;
return false;
}
}
has_set_playlist_target_duration_ = true;
return true;
}
bool MasterPlaylist::WriteMasterPlaylist(const std::string& base_url,
const std::string& output_dir) {
std::string file_path =
base::FilePath::FromUTF8Unsafe(output_dir)
.Append(base::FilePath::FromUTF8Unsafe(file_name_))
.AsUTF8Unsafe();
std::unique_ptr<media::File, media::FileCloser> file(
media::File::Open(file_path.c_str(), "w"));
if (!file) {
LOG(ERROR) << "Failed to open file " << file_path;
return false;
}
// TODO(rkuroiwa): Handle audio only.
std::string audio_output;
std::string video_output;
@ -213,6 +153,22 @@ bool MasterPlaylist::WriteMasterPlaylist(const std::string& base_url,
std::string content =
"#EXTM3U\n" + version_line + audio_output + video_output;
// Skip if the playlist is already written.
if (content == written_playlist_)
return true;
std::string file_path =
base::FilePath::FromUTF8Unsafe(output_dir)
.Append(base::FilePath::FromUTF8Unsafe(file_name_))
.AsUTF8Unsafe();
std::unique_ptr<media::File, media::FileCloser> file(
media::File::Open(file_path.c_str(), "w"));
if (!file) {
LOG(ERROR) << "Failed to open file " << file_path;
return false;
}
int64_t bytes_written = file->Write(content.data(), content.size());
if (bytes_written < 0) {
LOG(ERROR) << "Error while writing master playlist " << file_path;
@ -223,6 +179,7 @@ bool MasterPlaylist::WriteMasterPlaylist(const std::string& base_url,
<< content.size() << " " << file_path;
return false;
}
written_playlist_ = content;
return true;
}

View File

@ -31,18 +31,6 @@ class MasterPlaylist {
/// @return true on success, false otherwise.
virtual void AddMediaPlaylist(MediaPlaylist* media_playlist);
/// Write out Master Playlist and all the added MediaPlaylists to
/// base_url + <name of playlist>.
/// This assumes that @a base_url is used as the prefix for Media Playlists.
/// @param base_url is the prefix for the playlist files. This should be in
/// URI form such that prefix_+file_name is a valid HLS URI.
/// @param output_dir is where the playlist files are written. This is not
/// necessarily the same as base_url. It must be in a form that File
/// interface can open.
/// @return true on success, false otherwise.
virtual bool WriteAllPlaylists(const std::string& base_url,
const std::string& output_dir);
/// Writes Master Playlist to output_dir + <name of playlist>.
/// This assumes that @a base_url is used as the prefix for Media Playlists.
/// @param base_url is the prefix for the Media Playlist files. This should be
@ -50,19 +38,19 @@ class MasterPlaylist {
/// @param output_dir is where the playlist files are written. This is not
/// necessarily the same as base_url. It must be in a form that File
/// interface can open.
/// @return true on success, false otherwise.
/// @return true if the playlist is updated successfully or there is no
/// difference since the last write, false otherwise.
virtual bool WriteMasterPlaylist(const std::string& base_url,
const std::string& output_dir);
private:
std::string written_playlist_;
const std::string file_name_;
std::list<MediaPlaylist*> all_playlists_;
std::list<const MediaPlaylist*> video_playlists_;
// The key is the audio group name.
std::map<std::string, std::list<const MediaPlaylist*>> audio_playlist_groups_;
bool has_set_playlist_target_duration_ = false;
DISALLOW_COPY_AND_ASSIGN(MasterPlaylist);
};

View File

@ -23,6 +23,7 @@ using ::testing::NotNull;
using ::testing::Return;
using ::testing::ReturnRef;
using ::testing::SetArgPointee;
using ::testing::StrEq;
using ::testing::_;
using base::FilePath;
@ -58,8 +59,7 @@ class MasterPlaylistTest : public ::testing::Test {
ASSERT_TRUE(temp_dir_.IsValid());
*temp_dir_path = temp_dir_.path();
// TODO(rkuroiwa): Use memory file sys once prefix is exposed.
*output_dir = media::kLocalFilePrefix + temp_dir_.path().AsUTF8Unsafe()
+ "/";
*output_dir = temp_dir_.path().AsUTF8Unsafe() + "/";
}
base::ScopedTempDir temp_dir_;
@ -276,45 +276,5 @@ TEST_F(MasterPlaylistTest, WriteMasterPlaylistMultipleAudioGroups) {
ASSERT_EQ(expected, actual);
}
MATCHER_P(FileNameMatches, expected_file_name, "") {
const std::string& actual_filename = arg->file_name();
*result_listener << "which is " << actual_filename;
return expected_file_name == actual_filename;
}
// This test basically is WriteMasterPlaylist() and also make sure that
// the target duration is set for MediaPlaylist and
// MediaPlaylist::WriteToFile() is called.
TEST_F(MasterPlaylistTest, WriteAllPlaylists) {
std::string codec = "avc1";
MockMediaPlaylist mock_playlist(kVodPlaylist, "media1.m3u8", "somename",
"somegroupid");
mock_playlist.SetStreamTypeForTesting(
MediaPlaylist::MediaPlaylistStreamType::kPlayListVideo);
mock_playlist.SetCodecForTesting(codec);
ON_CALL(mock_playlist, Bitrate()).WillByDefault(Return(435889));
ON_CALL(mock_playlist, GetResolution(NotNull(), NotNull())).WillByDefault(
DoAll(SetArgPointee<0>(kWidth),
SetArgPointee<1>(kHeight),
Return(true)));
EXPECT_CALL(mock_playlist, GetLongestSegmentDuration()).WillOnce(Return(10));
EXPECT_CALL(mock_playlist, SetTargetDuration(10)).WillOnce(Return(true));
master_playlist_.AddMediaPlaylist(&mock_playlist);
EXPECT_CALL(
mock_playlist,
WriteToFile(FileNameMatches(
test_output_dir_path_.Append(FilePath::FromUTF8Unsafe("media1.m3u8"))
.AsUTF8Unsafe())))
.WillOnce(Return(true));
const char kBaseUrl[] = "http://domain.com/";
EXPECT_TRUE(master_playlist_.WriteAllPlaylists(kBaseUrl, test_output_dir_));
FilePath master_playlist_path = test_output_dir_path_.Append(
FilePath::FromUTF8Unsafe(kDefaultMasterPlaylistName));
ASSERT_TRUE(base::PathExists(master_playlist_path))
<< "Cannot find master playlist at " << master_playlist_path.value();
}
} // namespace hls
} // namespace shaka

View File

@ -14,6 +14,7 @@
#include "packager/base/strings/stringprintf.h"
#include "packager/media/base/language_utils.h"
#include "packager/media/file/file.h"
#include "packager/media/file/file_closer.h"
#include "packager/version/version.h"
namespace shaka {
@ -32,10 +33,11 @@ uint32_t GetTimeScale(const MediaInfo& media_info) {
return 0u;
}
std::string CreatePlaylistHeader(
const std::string& init_segment_name,
std::string CreatePlaylistHeader(const std::string& init_segment_name,
uint32_t target_duration,
MediaPlaylist::MediaPlaylistType type) {
MediaPlaylist::MediaPlaylistType type,
int sequence_number,
int discontinuity_sequence_number) {
const std::string version = GetPackagerVersion();
std::string version_line;
if (!version.empty()) {
@ -52,8 +54,25 @@ std::string CreatePlaylistHeader(
"#EXT-X-TARGETDURATION:%d\n",
version_line.c_str(), target_duration);
if (type == MediaPlaylist::MediaPlaylistType::kVod) {
switch (type) {
case MediaPlaylist::MediaPlaylistType::kVod:
header += "#EXT-X-PLAYLIST-TYPE:VOD\n";
break;
case MediaPlaylist::MediaPlaylistType::kEvent:
header += "#EXT-X-PLAYLIST-TYPE:EVENT\n";
break;
case MediaPlaylist::MediaPlaylistType::kLive:
if (sequence_number > 0) {
base::StringAppendF(&header, "#EXT-X-MEDIA-SEQUENCE:%d\n",
sequence_number);
}
if (discontinuity_sequence_number > 0) {
base::StringAppendF(&header, "#EXT-X-DISCONTINUITY-SEQUENCE:%d\n",
discontinuity_sequence_number);
}
break;
default:
NOTREACHED() << "Unexpected MediaPlaylistType " << static_cast<int>(type);
}
// Put EXT-X-MAP at the end since the rest of the playlist is about the
@ -67,22 +86,29 @@ std::string CreatePlaylistHeader(
class SegmentInfoEntry : public HlsEntry {
public:
SegmentInfoEntry(const std::string& file_name, double duration);
SegmentInfoEntry(const std::string& file_name,
double start_time,
double duration);
~SegmentInfoEntry() override;
std::string ToString() override;
double start_time() const { return start_time_; }
double duration() const { return duration_; }
private:
const std::string file_name_;
const double start_time_;
const double duration_;
DISALLOW_COPY_AND_ASSIGN(SegmentInfoEntry);
};
SegmentInfoEntry::SegmentInfoEntry(const std::string& file_name,
double start_time,
double duration)
: HlsEntry(HlsEntry::EntryType::kExtInf),
file_name_(file_name),
start_time_(start_time),
duration_(duration) {}
SegmentInfoEntry::~SegmentInfoEntry() {}
@ -160,19 +186,34 @@ std::string EncryptionInfoEntry::ToString() {
return ext_key + ",KEYFORMAT=\"" + key_format_ + "\"\n";
}
double LatestSegmentStartTime(
const std::list<std::unique_ptr<HlsEntry>>& entries) {
DCHECK(!entries.empty());
for (auto iter = entries.rbegin(); iter != entries.rend(); ++iter) {
if (iter->get()->type() == HlsEntry::EntryType::kExtInf) {
const SegmentInfoEntry* segment_info =
reinterpret_cast<SegmentInfoEntry*>(iter->get());
return segment_info->start_time();
}
}
return 0.0;
}
} // namespace
HlsEntry::HlsEntry(HlsEntry::EntryType type) : type_(type) {}
HlsEntry::~HlsEntry() {}
MediaPlaylist::MediaPlaylist(MediaPlaylistType type,
double time_shift_buffer_depth,
const std::string& file_name,
const std::string& name,
const std::string& group_id)
: file_name_(file_name), name_(name), group_id_(group_id), type_(type) {
LOG_IF(WARNING, type != MediaPlaylistType::kVod)
<< "Non VOD Media Playlist is not supported.";
}
: type_(type),
time_shift_buffer_depth_(time_shift_buffer_depth),
file_name_(file_name),
name_(name),
group_id_(group_id) {}
MediaPlaylist::~MediaPlaylist() {}
@ -209,16 +250,19 @@ bool MediaPlaylist::SetMediaInfo(const MediaInfo& media_info) {
}
void MediaPlaylist::AddSegment(const std::string& file_name,
uint64_t start_time,
uint64_t duration,
uint64_t size) {
if (time_scale_ == 0) {
LOG(WARNING) << "Timescale is not set and the duration for " << duration
<< " cannot be calculated. The output will be wrong.";
entries_.emplace_back(new SegmentInfoEntry(file_name, 0.0));
entries_.emplace_back(new SegmentInfoEntry(file_name, 0.0, 0.0));
return;
}
const double start_time_seconds =
static_cast<double>(start_time) / time_scale_;
const double segment_duration_seconds =
static_cast<double>(duration) / time_scale_;
if (segment_duration_seconds > longest_segment_duration_)
@ -227,8 +271,9 @@ void MediaPlaylist::AddSegment(const std::string& file_name,
const int kBitsInByte = 8;
const uint64_t bitrate = kBitsInByte * size / segment_duration_seconds;
max_bitrate_ = std::max(max_bitrate_, bitrate);
entries_.emplace_back(
new SegmentInfoEntry(file_name, segment_duration_seconds));
entries_.emplace_back(new SegmentInfoEntry(file_name, start_time_seconds,
segment_duration_seconds));
SlideWindow();
}
// TODO(rkuroiwa): This works for single key format but won't work for multiple
@ -298,13 +343,14 @@ void MediaPlaylist::AddEncryptionInfo(MediaPlaylist::EncryptionMethod method,
method, url, key_id, iv, key_format, key_format_versions));
}
bool MediaPlaylist::WriteToFile(media::File* file) {
bool MediaPlaylist::WriteToFile(const std::string& file_path) {
if (!target_duration_set_) {
SetTargetDuration(ceil(GetLongestSegmentDuration()));
}
std::string header = CreatePlaylistHeader(media_info_.init_segment_name(),
target_duration_, type_);
std::string header = CreatePlaylistHeader(
media_info_.init_segment_name(), target_duration_, type_,
sequence_number_, discontinuity_sequence_number_);
std::string body;
if (!entries_.empty()) {
@ -327,6 +373,12 @@ bool MediaPlaylist::WriteToFile(media::File* file) {
content += "#EXT-X-ENDLIST\n";
}
std::unique_ptr<media::File, media::FileCloser> file(
media::File::Open(file_path.c_str(), "w"));
if (!file) {
LOG(ERROR) << "Failed to open file " << file_path;
return false;
}
int64_t bytes_written = file->Write(content.data(), content.size());
if (bytes_written < 0) {
LOG(ERROR) << "Error while writing playlist to file.";
@ -355,15 +407,15 @@ double MediaPlaylist::GetLongestSegmentDuration() const {
return longest_segment_duration_;
}
bool MediaPlaylist::SetTargetDuration(uint32_t target_duration) {
void MediaPlaylist::SetTargetDuration(uint32_t target_duration) {
if (target_duration_set_) {
LOG(WARNING) << "Cannot set target duration to " << target_duration
<< ". Target duration already set to " << target_duration_;
return false;
if (target_duration_ == target_duration)
return;
VLOG(1) << "Updating target duration from " << target_duration << " to "
<< target_duration_;
}
target_duration_ = target_duration;
target_duration_set_ = true;
return true;
}
// Duplicated from MpdUtils because:
@ -392,5 +444,67 @@ bool MediaPlaylist::GetResolution(uint32_t* width, uint32_t* height) const {
return false;
}
void MediaPlaylist::SlideWindow() {
DCHECK(!entries_.empty());
if (time_shift_buffer_depth_ <= 0.0 || type_ != MediaPlaylistType::kLive)
return;
DCHECK_GT(time_scale_, 0u);
// The start time of the latest segment is considered the current_play_time,
// and this should guarantee that the latest segment will stay in the list.
const double current_play_time = LatestSegmentStartTime(entries_);
if (current_play_time <= time_shift_buffer_depth_)
return;
const double timeshift_limit = current_play_time - time_shift_buffer_depth_;
// Temporary list to hold the EXT-X-KEYs. For example, this allows us to
// remove <3> without removing <1> and <2> below (<1> and <2> are moved to the
// temporary list and added back later).
// #EXT-X-KEY <1>
// #EXT-X-KEY <2>
// #EXTINF <3>
// #EXTINF <4>
std::list<std::unique_ptr<HlsEntry>> ext_x_keys;
// Consecutive key entries are either fully removed or not removed at all.
// Keep track of entry types so we know if it is consecutive key entries.
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) {
if (prev_entry_type != HlsEntry::EntryType::kExtKey) {
if (!ext_x_keys.empty()) {
// Increase discontinuity sequence every time key changes. Note that
// it is inconsistent to how we insert EXT-X-DISCONTINUITY tag
// currently as we only insert the tag for the first EXT-X-KEY.
// TODO(kqyang): Find out if it is necessary to insert the
// EXT-X-DISCONTINUITY tag when key changes.
++discontinuity_sequence_number_;
ext_x_keys.clear();
}
}
ext_x_keys.push_back(std::move(*last));
} else {
DCHECK_EQ(entry_type, HlsEntry::EntryType::kExtInf);
const SegmentInfoEntry* segment_info =
reinterpret_cast<SegmentInfoEntry*>(last->get());
const double last_segment_end_time =
segment_info->start_time() + segment_info->duration();
if (timeshift_limit < last_segment_end_time)
break;
++num_segments_removed;
}
prev_entry_type = entry_type;
}
entries_.erase(entries_.begin(), last);
// Add key entries back.
entries_.insert(entries_.begin(), std::make_move_iterator(ext_x_keys.begin()),
std::make_move_iterator(ext_x_keys.end()));
sequence_number_ += num_segments_removed;
}
} // namespace hls
} // namespace shaka

View File

@ -62,6 +62,8 @@ class MediaPlaylist {
};
/// @param type is the type of this media playlist.
/// @param time_shift_buffer_depth determines the duration of the time
/// shifting buffer, only for live HLS.
/// @param file_name is the file name of this media playlist.
/// @param name is the name of this playlist. In other words this is the
/// value of the NAME attribute for EXT-X-MEDIA. This is not
@ -69,6 +71,7 @@ class MediaPlaylist {
/// @param group_id is the group ID for this playlist. This is the value of
/// GROUP-ID attribute for EXT-X-MEDIA.
MediaPlaylist(MediaPlaylistType type,
double time_shift_buffer_depth,
const std::string& file_name,
const std::string& name,
const std::string& group_id);
@ -94,9 +97,11 @@ class MediaPlaylist {
/// Segments must be added in order.
/// @param file_name is the file name of the segment.
/// @param start_time is in terms of the timescale of the media.
/// @param duration is in terms of the timescale of the media.
/// @param size is size in bytes.
virtual void AddSegment(const std::string& file_name,
uint64_t start_time,
uint64_t duration,
uint64_t size);
@ -122,16 +127,17 @@ class MediaPlaylist {
const std::string& key_format,
const std::string& key_format_versions);
/// Write the playlist to |file|.
/// Write the playlist to |file_path|.
/// This does not close the file.
/// If target duration is not set expliticly, this will try to find the target
/// duration. Note that target duration cannot be changed. So calling this
/// without explicitly setting the target duration and before adding any
/// segments will end up setting the target duration to 0 and will always
/// generate an invalid playlist.
/// @param file is the output file.
/// @param file_path is the output file path accepted by the File
/// implementation.
/// @return true on success, false otherwise.
virtual bool WriteToFile(media::File* file);
virtual bool WriteToFile(const std::string& file_path);
/// If bitrate is specified in MediaInfo then it will use that value.
/// Otherwise, returns the max bitrate.
@ -146,11 +152,10 @@ class MediaPlaylist {
/// In other words this is the value for EXT-X-TARGETDURATION.
/// If this is not called before calling Write(), it will estimate the best
/// target duration.
/// The spec does not allow changing EXT-X-TARGETDURATION, once Write() is
/// called, this will fail.
/// The spec does not allow changing EXT-X-TARGETDURATION. However, this class
/// has no control over the input source.
/// @param target_duration is the target duration for this playlist.
/// @return true if set, false otherwise.
virtual bool SetTargetDuration(uint32_t target_duration);
virtual void SetTargetDuration(uint32_t target_duration);
/// @return the language of the media, as an ISO language tag in its shortest
/// form. May be an empty string for video.
@ -161,15 +166,22 @@ class MediaPlaylist {
virtual bool GetResolution(uint32_t* width, uint32_t* height) const;
private:
// Remove elements from |entries_| for live profile. Increments
// |sequence_number_| by the number of segments removed.
void SlideWindow();
const MediaPlaylistType type_;
const double time_shift_buffer_depth_;
// Mainly for MasterPlaylist to use these values.
const std::string file_name_;
const std::string name_;
const std::string group_id_;
MediaInfo media_info_;
const MediaPlaylistType type_;
MediaPlaylistStreamType stream_type_ =
MediaPlaylistStreamType::kPlaylistUnknown;
std::string codec_;
int sequence_number_ = 0;
int discontinuity_sequence_number_ = 0;
double longest_segment_duration_ = 0.0;
uint32_t time_scale_ = 0;

View File

@ -7,8 +7,8 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "packager/media/file/file.h"
#include "packager/hls/base/media_playlist.h"
#include "packager/media/file/file_test_util.h"
#include "packager/version/version.h"
namespace shaka {
@ -20,21 +20,9 @@ using ::testing::ReturnArg;
namespace {
const char kDefaultPlaylistFileName[] = "default_playlist.m3u8";
class MockFile : public media::File {
public:
MockFile() : File(kDefaultPlaylistFileName) {}
MOCK_METHOD0(Close, bool());
MOCK_METHOD2(Read, int64_t(void* buffer, uint64_t length));
MOCK_METHOD2(Write,int64_t(const void* buffer, uint64_t length));
MOCK_METHOD0(Size, int64_t());
MOCK_METHOD0(Flush, bool());
MOCK_METHOD1(Seek, bool(uint64_t position));
MOCK_METHOD1(Tell, bool(uint64_t* position));
private:
MOCK_METHOD0(Open, bool());
};
const double kTimeShiftBufferDepth = 20;
const uint64_t kTimeScale = 90000;
const uint64_t kMBytes = 1000000;
MATCHER_P(MatchesString, expected_string, "") {
const std::string arg_string(static_cast<const char*>(arg));
@ -48,10 +36,14 @@ MATCHER_P(MatchesString, expected_string, "") {
class MediaPlaylistTest : public ::testing::Test {
protected:
MediaPlaylistTest()
: MediaPlaylistTest(MediaPlaylist::MediaPlaylistType::kVod) {}
MediaPlaylistTest(MediaPlaylist::MediaPlaylistType type)
: default_file_name_(kDefaultPlaylistFileName),
default_name_("default_name"),
default_group_id_("default_group_id"),
media_playlist_(MediaPlaylist::MediaPlaylistType::kVod,
media_playlist_(type,
kTimeShiftBufferDepth,
default_file_name_,
default_name_,
default_group_id_) {}
@ -62,12 +54,14 @@ class MediaPlaylistTest : public ::testing::Test {
MediaInfo::VideoInfo* video_info =
valid_video_media_info_.mutable_video_info();
video_info->set_codec("avc1");
video_info->set_time_scale(90000);
video_info->set_time_scale(kTimeScale);
video_info->set_frame_duration(3000);
video_info->set_width(1280);
video_info->set_height(720);
video_info->set_pixel_width(1);
video_info->set_pixel_height(1);
valid_video_media_info_.set_reference_time_scale(kTimeScale);
}
const std::string default_file_name_;
@ -87,7 +81,7 @@ TEST_F(MediaPlaylistTest, NoTimeScale) {
// The current implementation only handles video and audio.
TEST_F(MediaPlaylistTest, NoAudioOrVideo) {
MediaInfo media_info;
media_info.set_reference_time_scale(90000);
media_info.set_reference_time_scale(kTimeScale);
MediaInfo::TextInfo* text_info = media_info.mutable_text_info();
text_info->set_format("vtt");
EXPECT_FALSE(media_playlist_.SetMediaInfo(media_info));
@ -95,7 +89,7 @@ TEST_F(MediaPlaylistTest, NoAudioOrVideo) {
TEST_F(MediaPlaylistTest, SetMediaInfo) {
MediaInfo media_info;
media_info.set_reference_time_scale(90000);
media_info.set_reference_time_scale(kTimeScale);
MediaInfo::VideoInfo* video_info = media_info.mutable_video_info();
video_info->set_width(1280);
video_info->set_height(720);
@ -105,7 +99,7 @@ TEST_F(MediaPlaylistTest, SetMediaInfo) {
// Verify that AddSegment works (not crash).
TEST_F(MediaPlaylistTest, AddSegment) {
ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_));
media_playlist_.AddSegment("file1.ts", 900000, 1000000);
media_playlist_.AddSegment("file1.ts", 0, 10 * kTimeScale, kMBytes);
}
// Verify that AddEncryptionInfo works (not crash).
@ -118,7 +112,7 @@ TEST_F(MediaPlaylistTest, AddEncryptionInfo) {
TEST_F(MediaPlaylistTest, WriteToFile) {
ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_));
const std::string kExpectedOutput =
const char kExpectedOutput[] =
"#EXTM3U\n"
"#EXT-X-VERSION:6\n"
"## Generated with https://github.com/google/shaka-packager version "
@ -127,11 +121,9 @@ TEST_F(MediaPlaylistTest, WriteToFile) {
"#EXT-X-PLAYLIST-TYPE:VOD\n"
"#EXT-X-ENDLIST\n";
MockFile file;
EXPECT_CALL(file,
Write(MatchesString(kExpectedOutput), kExpectedOutput.size()))
.WillOnce(ReturnArg<1>());
EXPECT_TRUE(media_playlist_.WriteToFile(&file));
const char kMemoryFilePath[] = "memory://media.m3u8";
EXPECT_TRUE(media_playlist_.WriteToFile(kMemoryFilePath));
ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput);
}
// If bitrate (bandwidth) is not set in the MediaInfo, use it.
@ -145,65 +137,35 @@ TEST_F(MediaPlaylistTest, UseBitrateInMediaInfo) {
// segments.
TEST_F(MediaPlaylistTest, GetBitrateFromSegments) {
valid_video_media_info_.clear_bandwidth();
valid_video_media_info_.set_reference_time_scale(90000);
ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_));
// 10 seconds, 1MB.
media_playlist_.AddSegment("file1.ts", 900000, 1000000);
// 20 seconds, 5MB.
media_playlist_.AddSegment("file2.ts", 1800000, 5000000);
media_playlist_.AddSegment("file1.ts", 0, 10 * kTimeScale, kMBytes);
media_playlist_.AddSegment("file2.ts", 10 * kTimeScale, 20 * kTimeScale,
5 * kMBytes);
// Max bitrate is 2000Kb/s.
EXPECT_EQ(2000000u, media_playlist_.Bitrate());
}
TEST_F(MediaPlaylistTest, GetLongestSegmentDuration) {
valid_video_media_info_.set_reference_time_scale(90000);
ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_));
// 10 seconds.
media_playlist_.AddSegment("file1.ts", 900000, 1000000);
// 30 seconds.
media_playlist_.AddSegment("file2.ts", 2700000, 5000000);
// 14 seconds.
media_playlist_.AddSegment("file3.ts", 1260000, 3000000);
media_playlist_.AddSegment("file1.ts", 0, 10 * kTimeScale, kMBytes);
media_playlist_.AddSegment("file2.ts", 10 * kTimeScale, 30 * kTimeScale,
5 * kMBytes);
media_playlist_.AddSegment("file3.ts", 40 * kTimeScale, 14 * kTimeScale,
3 * kMBytes);
EXPECT_NEAR(30.0, media_playlist_.GetLongestSegmentDuration(), 0.01);
}
TEST_F(MediaPlaylistTest, SetTargetDuration) {
ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_));
EXPECT_TRUE(media_playlist_.SetTargetDuration(20));
const std::string kExpectedOutput =
"#EXTM3U\n"
"#EXT-X-VERSION:6\n"
"## Generated with https://github.com/google/shaka-packager version "
"test\n"
"#EXT-X-TARGETDURATION:20\n"
"#EXT-X-PLAYLIST-TYPE:VOD\n"
"#EXT-X-ENDLIST\n";
MockFile file;
EXPECT_CALL(file,
Write(MatchesString(kExpectedOutput), kExpectedOutput.size()))
.WillOnce(ReturnArg<1>());
EXPECT_TRUE(media_playlist_.WriteToFile(&file));
// Cannot set target duration more than once.
EXPECT_FALSE(media_playlist_.SetTargetDuration(20));
EXPECT_FALSE(media_playlist_.SetTargetDuration(10));
}
TEST_F(MediaPlaylistTest, WriteToFileWithSegments) {
valid_video_media_info_.set_reference_time_scale(90000);
ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_));
// 10 seconds.
media_playlist_.AddSegment("file1.ts", 900000, 1000000);
// 30 seconds.
media_playlist_.AddSegment("file2.ts", 2700000, 5000000);
const std::string kExpectedOutput =
media_playlist_.AddSegment("file1.ts", 0, 10 * kTimeScale, kMBytes);
media_playlist_.AddSegment("file2.ts", 10 * kTimeScale, 30 * kTimeScale,
5 * kMBytes);
const char kExpectedOutput[] =
"#EXTM3U\n"
"#EXT-X-VERSION:6\n"
"## Generated with https://github.com/google/shaka-packager version "
@ -216,25 +178,21 @@ TEST_F(MediaPlaylistTest, WriteToFileWithSegments) {
"file2.ts\n"
"#EXT-X-ENDLIST\n";
MockFile file;
EXPECT_CALL(file,
Write(MatchesString(kExpectedOutput), kExpectedOutput.size()))
.WillOnce(ReturnArg<1>());
EXPECT_TRUE(media_playlist_.WriteToFile(&file));
const char kMemoryFilePath[] = "memory://media.m3u8";
EXPECT_TRUE(media_playlist_.WriteToFile(kMemoryFilePath));
ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput);
}
TEST_F(MediaPlaylistTest, WriteToFileWithEncryptionInfo) {
valid_video_media_info_.set_reference_time_scale(90000);
ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_));
media_playlist_.AddEncryptionInfo(MediaPlaylist::EncryptionMethod::kSampleAes,
"http://example.com", "", "0x12345678",
"com.widevine", "1/2/4");
// 10 seconds.
media_playlist_.AddSegment("file1.ts", 900000, 1000000);
// 30 seconds.
media_playlist_.AddSegment("file2.ts", 2700000, 5000000);
const std::string kExpectedOutput =
media_playlist_.AddSegment("file1.ts", 0, 10 * kTimeScale, kMBytes);
media_playlist_.AddSegment("file2.ts", 10 * kTimeScale, 30 * kTimeScale,
5 * kMBytes);
const char kExpectedOutput[] =
"#EXTM3U\n"
"#EXT-X-VERSION:6\n"
"## Generated with https://github.com/google/shaka-packager version "
@ -250,25 +208,21 @@ TEST_F(MediaPlaylistTest, WriteToFileWithEncryptionInfo) {
"file2.ts\n"
"#EXT-X-ENDLIST\n";
MockFile file;
EXPECT_CALL(file,
Write(MatchesString(kExpectedOutput), kExpectedOutput.size()))
.WillOnce(ReturnArg<1>());
EXPECT_TRUE(media_playlist_.WriteToFile(&file));
const char kMemoryFilePath[] = "memory://media.m3u8";
EXPECT_TRUE(media_playlist_.WriteToFile(kMemoryFilePath));
ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput);
}
TEST_F(MediaPlaylistTest, WriteToFileWithEncryptionInfoEmptyIv) {
valid_video_media_info_.set_reference_time_scale(90000);
ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_));
media_playlist_.AddEncryptionInfo(MediaPlaylist::EncryptionMethod::kSampleAes,
"http://example.com", "", "", "com.widevine",
"");
// 10 seconds.
media_playlist_.AddSegment("file1.ts", 900000, 1000000);
// 30 seconds.
media_playlist_.AddSegment("file2.ts", 2700000, 5000000);
const std::string kExpectedOutput =
media_playlist_.AddSegment("file1.ts", 0, 10 * kTimeScale, kMBytes);
media_playlist_.AddSegment("file2.ts", 10 * kTimeScale, 30 * kTimeScale,
5 * kMBytes);
const char kExpectedOutput[] =
"#EXTM3U\n"
"#EXT-X-VERSION:6\n"
"## Generated with https://github.com/google/shaka-packager version "
@ -283,25 +237,23 @@ TEST_F(MediaPlaylistTest, WriteToFileWithEncryptionInfoEmptyIv) {
"file2.ts\n"
"#EXT-X-ENDLIST\n";
MockFile file;
EXPECT_CALL(file,
Write(MatchesString(kExpectedOutput), kExpectedOutput.size()))
.WillOnce(ReturnArg<1>());
EXPECT_TRUE(media_playlist_.WriteToFile(&file));
const char kMemoryFilePath[] = "memory://media.m3u8";
EXPECT_TRUE(media_playlist_.WriteToFile(kMemoryFilePath));
ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput);
}
// Verify that EXT-X-DISCONTINUITY is inserted before EXT-X-KEY.
TEST_F(MediaPlaylistTest, WriteToFileWithClearLead) {
valid_video_media_info_.set_reference_time_scale(90000);
ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_));
media_playlist_.AddSegment("file1.ts", 900000, 1000000);
media_playlist_.AddSegment("file1.ts", 0, 10 * kTimeScale, kMBytes);
media_playlist_.AddEncryptionInfo(MediaPlaylist::EncryptionMethod::kSampleAes,
"http://example.com", "", "0x12345678",
"com.widevine", "1/2/4");
media_playlist_.AddSegment("file2.ts", 2700000, 5000000);
const std::string kExpectedOutput =
media_playlist_.AddSegment("file2.ts", 10 * kTimeScale, 30 * kTimeScale,
5 * kMBytes);
const char kExpectedOutput[] =
"#EXTM3U\n"
"#EXT-X-VERSION:6\n"
"## Generated with https://github.com/google/shaka-packager version test\n"
@ -317,25 +269,21 @@ TEST_F(MediaPlaylistTest, WriteToFileWithClearLead) {
"file2.ts\n"
"#EXT-X-ENDLIST\n";
MockFile file;
EXPECT_CALL(file,
Write(MatchesString(kExpectedOutput), kExpectedOutput.size()))
.WillOnce(ReturnArg<1>());
EXPECT_TRUE(media_playlist_.WriteToFile(&file));
const char kMemoryFilePath[] = "memory://media.m3u8";
EXPECT_TRUE(media_playlist_.WriteToFile(kMemoryFilePath));
ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput);
}
TEST_F(MediaPlaylistTest, RemoveOldestSegment) {
valid_video_media_info_.set_reference_time_scale(90000);
ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_));
// 10 seconds.
media_playlist_.AddSegment("file1.ts", 900000, 1000000);
// 30 seconds.
media_playlist_.AddSegment("file2.ts", 2700000, 5000000);
media_playlist_.AddSegment("file1.ts", 0, 10 * kTimeScale, kMBytes);
media_playlist_.AddSegment("file2.ts", 10 * kTimeScale, 30 * kTimeScale,
5 * kMBytes);
media_playlist_.RemoveOldestSegment();
const std::string kExpectedOutput =
const char kExpectedOutput[] =
"#EXTM3U\n"
"#EXT-X-VERSION:6\n"
"## Generated with https://github.com/google/shaka-packager version "
@ -346,16 +294,14 @@ TEST_F(MediaPlaylistTest, RemoveOldestSegment) {
"file2.ts\n"
"#EXT-X-ENDLIST\n";
MockFile file;
EXPECT_CALL(file,
Write(MatchesString(kExpectedOutput), kExpectedOutput.size()))
.WillOnce(ReturnArg<1>());
EXPECT_TRUE(media_playlist_.WriteToFile(&file));
const char kMemoryFilePath[] = "memory://media.m3u8";
EXPECT_TRUE(media_playlist_.WriteToFile(kMemoryFilePath));
ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput);
}
TEST_F(MediaPlaylistTest, GetLanguage) {
MediaInfo media_info;
media_info.set_reference_time_scale(90000);
media_info.set_reference_time_scale(kTimeScale);
// Check conversions from long to short form.
media_info.mutable_audio_info()->set_language("eng");
@ -372,16 +318,14 @@ TEST_F(MediaPlaylistTest, GetLanguage) {
}
TEST_F(MediaPlaylistTest, InitSegment) {
valid_video_media_info_.set_reference_time_scale(90000);
valid_video_media_info_.set_init_segment_name("init_segment.mp4");
ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_));
// 10 seconds.
media_playlist_.AddSegment("file1.mp4", 900000, 1000000);
// 30 seconds.
media_playlist_.AddSegment("file2.mp4", 2700000, 5000000);
media_playlist_.AddSegment("file1.mp4", 0, 10 * kTimeScale, kMBytes);
media_playlist_.AddSegment("file2.mp4", 10 * kTimeScale, 30 * kTimeScale,
5 * kMBytes);
const std::string kExpectedOutput =
const char kExpectedOutput[] =
"#EXTM3U\n"
"#EXT-X-VERSION:6\n"
"## Generated with https://github.com/google/shaka-packager version test\n"
@ -394,27 +338,23 @@ TEST_F(MediaPlaylistTest, InitSegment) {
"file2.mp4\n"
"#EXT-X-ENDLIST\n";
MockFile file;
EXPECT_CALL(file,
Write(MatchesString(kExpectedOutput), kExpectedOutput.size()))
.WillOnce(ReturnArg<1>());
EXPECT_TRUE(media_playlist_.WriteToFile(&file));
const char kMemoryFilePath[] = "memory://media.m3u8";
EXPECT_TRUE(media_playlist_.WriteToFile(kMemoryFilePath));
ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput);
}
// Verify that kSampleAesCenc is handled correctly.
TEST_F(MediaPlaylistTest, SampleAesCenc) {
valid_video_media_info_.set_reference_time_scale(90000);
ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_));
media_playlist_.AddEncryptionInfo(
MediaPlaylist::EncryptionMethod::kSampleAesCenc, "http://example.com", "",
"0x12345678", "com.widevine", "1/2/4");
// 10 seconds.
media_playlist_.AddSegment("file1.ts", 900000, 1000000);
// 30 seconds.
media_playlist_.AddSegment("file2.ts", 2700000, 5000000);
const std::string kExpectedOutput =
media_playlist_.AddSegment("file1.ts", 0, 10 * kTimeScale, kMBytes);
media_playlist_.AddSegment("file2.ts", 10 * kTimeScale, 30 * kTimeScale,
5 * kMBytes);
const char kExpectedOutput[] =
"#EXTM3U\n"
"#EXT-X-VERSION:6\n"
"## Generated with https://github.com/google/shaka-packager version "
@ -430,16 +370,13 @@ TEST_F(MediaPlaylistTest, SampleAesCenc) {
"file2.ts\n"
"#EXT-X-ENDLIST\n";
MockFile file;
EXPECT_CALL(file,
Write(MatchesString(kExpectedOutput), kExpectedOutput.size()))
.WillOnce(ReturnArg<1>());
EXPECT_TRUE(media_playlist_.WriteToFile(&file));
const char kMemoryFilePath[] = "memory://media.m3u8";
EXPECT_TRUE(media_playlist_.WriteToFile(kMemoryFilePath));
ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput);
}
// Verify that multiple encryption info can be set.
TEST_F(MediaPlaylistTest, MultipleEncryptionInfo) {
valid_video_media_info_.set_reference_time_scale(90000);
ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_));
media_playlist_.AddEncryptionInfo(MediaPlaylist::EncryptionMethod::kSampleAes,
@ -450,11 +387,10 @@ TEST_F(MediaPlaylistTest, MultipleEncryptionInfo) {
MediaPlaylist::EncryptionMethod::kSampleAes, "http://mydomain.com",
"0xfedc", "0x12345678", "com.widevine.someother", "1");
// 10 seconds.
media_playlist_.AddSegment("file1.ts", 900000, 1000000);
// 30 seconds.
media_playlist_.AddSegment("file2.ts", 2700000, 5000000);
const std::string kExpectedOutput =
media_playlist_.AddSegment("file1.ts", 0, 10 * kTimeScale, kMBytes);
media_playlist_.AddSegment("file2.ts", 10 * kTimeScale, 30 * kTimeScale,
5 * kMBytes);
const char kExpectedOutput[] =
"#EXTM3U\n"
"#EXT-X-VERSION:6\n"
"## Generated with https://github.com/google/shaka-packager version "
@ -474,11 +410,193 @@ TEST_F(MediaPlaylistTest, MultipleEncryptionInfo) {
"file2.ts\n"
"#EXT-X-ENDLIST\n";
MockFile file;
EXPECT_CALL(file,
Write(MatchesString(kExpectedOutput), kExpectedOutput.size()))
.WillOnce(ReturnArg<1>());
EXPECT_TRUE(media_playlist_.WriteToFile(&file));
const char kMemoryFilePath[] = "memory://media.m3u8";
EXPECT_TRUE(media_playlist_.WriteToFile(kMemoryFilePath));
ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput);
}
class LiveMediaPlaylistTest : public MediaPlaylistTest {
protected:
LiveMediaPlaylistTest()
: MediaPlaylistTest(MediaPlaylist::MediaPlaylistType::kLive) {}
};
TEST_F(LiveMediaPlaylistTest, Basic) {
ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_));
media_playlist_.AddSegment("file1.ts", 0, 10 * kTimeScale, kMBytes);
media_playlist_.AddSegment("file2.ts", 10 * kTimeScale, 20 * kTimeScale,
2 * kMBytes);
const char kExpectedOutput[] =
"#EXTM3U\n"
"#EXT-X-VERSION:6\n"
"## Generated with https://github.com/google/shaka-packager version "
"test\n"
"#EXT-X-TARGETDURATION:20\n"
"#EXTINF:10.000,\n"
"file1.ts\n"
"#EXTINF:20.000,\n"
"file2.ts\n";
const char kMemoryFilePath[] = "memory://media.m3u8";
EXPECT_TRUE(media_playlist_.WriteToFile(kMemoryFilePath));
ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput);
}
TEST_F(LiveMediaPlaylistTest, TimeShifted) {
ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_));
media_playlist_.AddSegment("file1.ts", 0, 10 * kTimeScale, kMBytes);
media_playlist_.AddSegment("file2.ts", 10 * kTimeScale, 20 * kTimeScale,
2 * kMBytes);
media_playlist_.AddSegment("file3.ts", 30 * kTimeScale, 20 * kTimeScale,
2 * kMBytes);
const char kExpectedOutput[] =
"#EXTM3U\n"
"#EXT-X-VERSION:6\n"
"## Generated with https://github.com/google/shaka-packager version "
"test\n"
"#EXT-X-TARGETDURATION:20\n"
"#EXT-X-MEDIA-SEQUENCE:1\n"
"#EXTINF:20.000,\n"
"file2.ts\n"
"#EXTINF:20.000,\n"
"file3.ts\n";
const char kMemoryFilePath[] = "memory://media.m3u8";
EXPECT_TRUE(media_playlist_.WriteToFile(kMemoryFilePath));
ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput);
}
TEST_F(LiveMediaPlaylistTest, TimeShiftedWithEncryptionInfo) {
ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_));
media_playlist_.AddEncryptionInfo(MediaPlaylist::EncryptionMethod::kSampleAes,
"http://example.com", "", "0x12345678",
"com.widevine", "1/2/4");
media_playlist_.AddEncryptionInfo(
MediaPlaylist::EncryptionMethod::kSampleAes, "http://mydomain.com",
"0xfedc", "0x12345678", "com.widevine.someother", "1");
media_playlist_.AddSegment("file1.ts", 0, 10 * kTimeScale, kMBytes);
media_playlist_.AddSegment("file2.ts", 10 * kTimeScale, 20 * kTimeScale,
2 * kMBytes);
media_playlist_.AddSegment("file3.ts", 30 * kTimeScale, 20 * kTimeScale,
2 * kMBytes);
const char kExpectedOutput[] =
"#EXTM3U\n"
"#EXT-X-VERSION:6\n"
"## Generated with https://github.com/google/shaka-packager version "
"test\n"
"#EXT-X-TARGETDURATION:20\n"
"#EXT-X-MEDIA-SEQUENCE:1\n"
"#EXT-X-KEY:METHOD=SAMPLE-AES,"
"URI=\"http://example.com\",IV=0x12345678,KEYFORMATVERSIONS=\"1/2/4\","
"KEYFORMAT=\"com.widevine\"\n"
"#EXT-X-KEY:METHOD=SAMPLE-AES,"
"URI=\"http://mydomain.com\",KEYID=0xfedc,IV=0x12345678,"
"KEYFORMATVERSIONS=\"1\","
"KEYFORMAT=\"com.widevine.someother\"\n"
"#EXTINF:20.000,\n"
"file2.ts\n"
"#EXTINF:20.000,\n"
"file3.ts\n";
const char kMemoryFilePath[] = "memory://media.m3u8";
EXPECT_TRUE(media_playlist_.WriteToFile(kMemoryFilePath));
ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput);
}
TEST_F(LiveMediaPlaylistTest, TimeShiftedWithEncryptionInfoShifted) {
ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_));
media_playlist_.AddEncryptionInfo(MediaPlaylist::EncryptionMethod::kSampleAes,
"http://example.com", "", "0x12345678",
"com.widevine", "1/2/4");
media_playlist_.AddEncryptionInfo(
MediaPlaylist::EncryptionMethod::kSampleAes, "http://mydomain.com",
"0xfedc", "0x12345678", "com.widevine.someother", "1");
media_playlist_.AddSegment("file1.ts", 0, 10 * kTimeScale, kMBytes);
media_playlist_.AddEncryptionInfo(MediaPlaylist::EncryptionMethod::kSampleAes,
"http://example.com", "", "0x22345678",
"com.widevine", "1/2/4");
media_playlist_.AddEncryptionInfo(
MediaPlaylist::EncryptionMethod::kSampleAes, "http://mydomain.com",
"0xfedd", "0x22345678", "com.widevine.someother", "1");
media_playlist_.AddSegment("file2.ts", 10 * kTimeScale, 20 * kTimeScale,
2 * kMBytes);
media_playlist_.AddEncryptionInfo(MediaPlaylist::EncryptionMethod::kSampleAes,
"http://example.com", "", "0x32345678",
"com.widevine", "1/2/4");
media_playlist_.AddEncryptionInfo(
MediaPlaylist::EncryptionMethod::kSampleAes, "http://mydomain.com",
"0xfede", "0x32345678", "com.widevine.someother", "1");
media_playlist_.AddSegment("file3.ts", 30 * kTimeScale, 20 * kTimeScale,
2 * kMBytes);
const char kExpectedOutput[] =
"#EXTM3U\n"
"#EXT-X-VERSION:6\n"
"## Generated with https://github.com/google/shaka-packager version "
"test\n"
"#EXT-X-TARGETDURATION:20\n"
"#EXT-X-MEDIA-SEQUENCE:1\n"
"#EXT-X-DISCONTINUITY-SEQUENCE:1\n"
"#EXT-X-KEY:METHOD=SAMPLE-AES,"
"URI=\"http://example.com\",IV=0x22345678,KEYFORMATVERSIONS=\"1/2/4\","
"KEYFORMAT=\"com.widevine\"\n"
"#EXT-X-KEY:METHOD=SAMPLE-AES,"
"URI=\"http://mydomain.com\",KEYID=0xfedd,IV=0x22345678,"
"KEYFORMATVERSIONS=\"1\","
"KEYFORMAT=\"com.widevine.someother\"\n"
"#EXTINF:20.000,\n"
"file2.ts\n"
"#EXT-X-KEY:METHOD=SAMPLE-AES,"
"URI=\"http://example.com\",IV=0x32345678,KEYFORMATVERSIONS=\"1/2/4\","
"KEYFORMAT=\"com.widevine\"\n"
"#EXT-X-KEY:METHOD=SAMPLE-AES,"
"URI=\"http://mydomain.com\",KEYID=0xfede,IV=0x32345678,"
"KEYFORMATVERSIONS=\"1\","
"KEYFORMAT=\"com.widevine.someother\"\n"
"#EXTINF:20.000,\n"
"file3.ts\n";
const char kMemoryFilePath[] = "memory://media.m3u8";
EXPECT_TRUE(media_playlist_.WriteToFile(kMemoryFilePath));
ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput);
}
class EventMediaPlaylistTest : public MediaPlaylistTest {
protected:
EventMediaPlaylistTest()
: MediaPlaylistTest(MediaPlaylist::MediaPlaylistType::kEvent) {}
};
TEST_F(EventMediaPlaylistTest, Basic) {
ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_));
media_playlist_.AddSegment("file1.ts", 0, 10 * kTimeScale, kMBytes);
media_playlist_.AddSegment("file2.ts", 10 * kTimeScale, 20 * kTimeScale,
2 * kMBytes);
const char kExpectedOutput[] =
"#EXTM3U\n"
"#EXT-X-VERSION:6\n"
"## Generated with https://github.com/google/shaka-packager version "
"test\n"
"#EXT-X-TARGETDURATION:20\n"
"#EXT-X-PLAYLIST-TYPE:EVENT\n"
"#EXTINF:10.000,\n"
"file1.ts\n"
"#EXTINF:20.000,\n"
"file2.ts\n";
const char kMemoryFilePath[] = "memory://media.m3u8";
EXPECT_TRUE(media_playlist_.WriteToFile(kMemoryFilePath));
ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput);
}
} // namespace hls

View File

@ -13,7 +13,7 @@ MockMediaPlaylist::MockMediaPlaylist(MediaPlaylistType type,
const std::string& file_name,
const std::string& name,
const std::string& group_id)
: MediaPlaylist(type, file_name, name, group_id) {}
: MediaPlaylist(type, 0, file_name, name, group_id) {}
MockMediaPlaylist::~MockMediaPlaylist() {}
} // namespace hls

View File

@ -25,8 +25,9 @@ class MockMediaPlaylist : public MediaPlaylist {
~MockMediaPlaylist() override;
MOCK_METHOD1(SetMediaInfo, bool(const MediaInfo& media_info));
MOCK_METHOD3(AddSegment,
MOCK_METHOD4(AddSegment,
void(const std::string& file_name,
uint64_t start_time,
uint64_t duration,
uint64_t size));
MOCK_METHOD0(RemoveOldestSegment, void());
@ -37,10 +38,10 @@ class MockMediaPlaylist : public MediaPlaylist {
const std::string& iv,
const std::string& key_format,
const std::string& key_format_versions));
MOCK_METHOD1(WriteToFile, bool(media::File* file));
MOCK_METHOD1(WriteToFile, bool(const std::string& file_path));
MOCK_CONST_METHOD0(Bitrate, uint64_t());
MOCK_CONST_METHOD0(GetLongestSegmentDuration, double());
MOCK_METHOD1(SetTargetDuration, bool(uint32_t target_duration));
MOCK_METHOD1(SetTargetDuration, void(uint32_t target_duration));
MOCK_CONST_METHOD0(GetLanguage, std::string());
MOCK_CONST_METHOD2(GetResolution, bool(uint32_t* width, uint32_t* height));
};

View File

@ -6,6 +6,8 @@
#include "packager/hls/base/simple_hls_notifier.h"
#include <cmath>
#include "packager/base/base64.h"
#include "packager/base/files/file_path.h"
#include "packager/base/json/json_writer.h"
@ -191,24 +193,40 @@ bool HandleWidevineKeyFormats(
return true;
}
bool WriteMediaPlaylist(const std::string& output_dir,
MediaPlaylist* playlist) {
std::string file_path =
base::FilePath::FromUTF8Unsafe(output_dir)
.Append(base::FilePath::FromUTF8Unsafe(playlist->file_name()))
.AsUTF8Unsafe();
if (!playlist->WriteToFile(file_path)) {
LOG(ERROR) << "Failed to write playlist " << file_path;
return false;
}
return true;
}
} // namespace
MediaPlaylistFactory::~MediaPlaylistFactory() {}
std::unique_ptr<MediaPlaylist> MediaPlaylistFactory::Create(
MediaPlaylist::MediaPlaylistType type,
double time_shift_buffer_depth,
const std::string& file_name,
const std::string& name,
const std::string& group_id) {
return std::unique_ptr<MediaPlaylist>(
new MediaPlaylist(type, file_name, name, group_id));
return std::unique_ptr<MediaPlaylist>(new MediaPlaylist(
type, time_shift_buffer_depth, file_name, name, group_id));
}
SimpleHlsNotifier::SimpleHlsNotifier(HlsProfile profile,
double time_shift_buffer_depth,
const std::string& prefix,
const std::string& output_dir,
const std::string& master_playlist_name)
: HlsNotifier(profile),
time_shift_buffer_depth_(time_shift_buffer_depth),
prefix_(prefix),
output_dir_(output_dir),
media_playlist_factory_(new MediaPlaylistFactory()),
@ -235,6 +253,9 @@ bool SimpleHlsNotifier::NotifyNewStream(const MediaInfo& media_info,
case HlsProfile::kOnDemandProfile:
type = MediaPlaylist::MediaPlaylistType::kVod;
break;
case HlsProfile::kEventProfile:
type = MediaPlaylist::MediaPlaylistType::kEvent;
break;
default:
NOTREACHED();
return false;
@ -244,7 +265,8 @@ bool SimpleHlsNotifier::NotifyNewStream(const MediaInfo& media_info,
MakePathsRelativeToOutputDirectory(output_dir_, &adjusted_media_info);
std::unique_ptr<MediaPlaylist> media_playlist =
media_playlist_factory_->Create(type, playlist_name, name, group_id);
media_playlist_factory_->Create(type, time_shift_buffer_depth_,
playlist_name, name, group_id);
if (!media_playlist->SetMediaInfo(adjusted_media_info)) {
LOG(ERROR) << "Failed to set media info for playlist " << playlist_name;
return false;
@ -288,7 +310,37 @@ bool SimpleHlsNotifier::NotifyNewSegment(uint32_t stream_id,
MakePathRelative(segment_name, output_dir_);
auto& media_playlist = stream_iterator->second->media_playlist;
media_playlist->AddSegment(prefix_ + relative_segment_name, duration, size);
media_playlist->AddSegment(prefix_ + relative_segment_name, start_time,
duration, size);
// Update target duration.
uint32_t longest_segment_duration =
static_cast<uint32_t>(ceil(media_playlist->GetLongestSegmentDuration()));
bool target_duration_updated = false;
if (longest_segment_duration > target_duration_) {
target_duration_ = longest_segment_duration;
target_duration_updated = true;
}
// Update the playlists when there is new segments in live mode.
if (profile() == HlsProfile::kLiveProfile ||
profile() == HlsProfile::kEventProfile) {
if (!master_playlist_->WriteMasterPlaylist(prefix_, output_dir_)) {
LOG(ERROR) << "Failed to write master playlist.";
return false;
}
// Update all playlists if target duration is updated.
if (target_duration_updated) {
for (auto& streams : stream_map_) {
MediaPlaylist* playlist = streams.second->media_playlist.get();
playlist->SetTargetDuration(target_duration_);
if (!WriteMediaPlaylist(output_dir_, playlist))
return false;
}
} else {
return WriteMediaPlaylist(output_dir_, media_playlist.get());
}
}
return true;
}
@ -333,7 +385,17 @@ bool SimpleHlsNotifier::NotifyEncryptionUpdate(
bool SimpleHlsNotifier::Flush() {
base::AutoLock auto_lock(lock_);
return master_playlist_->WriteAllPlaylists(prefix_, output_dir_);
if (!master_playlist_->WriteMasterPlaylist(prefix_, output_dir_)) {
LOG(ERROR) << "Failed to write master playlist.";
return false;
}
for (auto& streams : stream_map_) {
MediaPlaylist* playlist = streams.second->media_playlist.get();
playlist->SetTargetDuration(target_duration_);
if (!WriteMediaPlaylist(output_dir_, playlist))
return false;
}
return true;
}
} // namespace hls

View File

@ -29,6 +29,7 @@ class MediaPlaylistFactory {
virtual ~MediaPlaylistFactory();
virtual std::unique_ptr<MediaPlaylist> Create(
MediaPlaylist::MediaPlaylistType type,
double time_shift_buffer_depth,
const std::string& file_name,
const std::string& name,
const std::string& group_id);
@ -40,12 +41,15 @@ class SimpleHlsNotifier : public HlsNotifier {
/// @a prefix is used as hte prefix for all the URIs for Media Playlist. This
/// includes the segment URIs in the Media Playlists.
/// @param profile is the profile of the playlists.
/// @param time_shift_buffer_depth determines the duration of the time
/// shifting buffer, only for live HLS.
/// @param prefix is the used as the prefix for MediaPlaylist URIs. May be
/// empty for relative URI from the playlist.
/// @param output_dir is the output directory of the playlists. May be empty
/// to write to current directory.
/// @param master_playlist_name is the name of the master playlist.
SimpleHlsNotifier(HlsProfile profile,
double time_shift_buffer_depth,
const std::string& prefix,
const std::string& output_dir,
const std::string& master_playlist_name);
@ -81,8 +85,10 @@ class SimpleHlsNotifier : public HlsNotifier {
MediaPlaylist::EncryptionMethod encryption_method;
};
const double time_shift_buffer_depth_ = 0;
const std::string prefix_;
const std::string output_dir_;
uint32_t target_duration_ = 0;
std::unique_ptr<MediaPlaylistFactory> media_playlist_factory_;
std::unique_ptr<MasterPlaylist> master_playlist_;

View File

@ -8,6 +8,7 @@
#include <gtest/gtest.h>
#include "packager/base/base64.h"
#include "packager/base/files/file_path.h"
#include "packager/hls/base/mock_media_playlist.h"
#include "packager/hls/base/simple_hls_notifier.h"
#include "packager/media/base/fixed_key_source.h"
@ -18,6 +19,9 @@
namespace shaka {
namespace hls {
using ::testing::Eq;
using ::testing::InSequence;
using ::testing::Mock;
using ::testing::Return;
using ::testing::StrEq;
using ::testing::_;
@ -32,29 +36,30 @@ class MockMasterPlaylist : public MasterPlaylist {
MockMasterPlaylist() : MasterPlaylist(kMasterPlaylistName) {}
MOCK_METHOD1(AddMediaPlaylist, void(MediaPlaylist* media_playlist));
MOCK_METHOD2(WriteAllPlaylists,
bool(const std::string& prefix, const std::string& output_dir));
MOCK_METHOD2(WriteMasterPlaylist,
bool(const std::string& prefix, const std::string& output_dir));
};
class MockMediaPlaylistFactory : public MediaPlaylistFactory {
public:
MOCK_METHOD4(CreateMock,
MOCK_METHOD5(CreateMock,
MediaPlaylist*(MediaPlaylist::MediaPlaylistType type,
double time_shift_buffer_depth,
const std::string& file_name,
const std::string& name,
const std::string& group_id));
std::unique_ptr<MediaPlaylist> Create(MediaPlaylist::MediaPlaylistType type,
double time_shift_buffer_depth,
const std::string& file_name,
const std::string& name,
const std::string& group_id) override {
return std::unique_ptr<MediaPlaylist>(
CreateMock(type, file_name, name, group_id));
CreateMock(type, time_shift_buffer_depth, file_name, name, group_id));
}
};
const double kTestTimeShiftBufferDepth = 1800.0;
const char kTestPrefix[] = "http://testprefix.com/";
const char kAnyOutputDir[] = "anything/";
@ -75,7 +80,11 @@ const char kSampleAesProtectionScheme[] = "cbca";
class SimpleHlsNotifierTest : public ::testing::Test {
protected:
SimpleHlsNotifierTest()
: notifier_(HlsNotifier::HlsProfile::kOnDemandProfile,
: SimpleHlsNotifierTest(HlsProfile::kOnDemandProfile) {}
SimpleHlsNotifierTest(HlsProfile profile)
: notifier_(profile,
kTestTimeShiftBufferDepth,
kTestPrefix,
kAnyOutputDir,
kMasterPlaylistName),
@ -121,7 +130,7 @@ class SimpleHlsNotifierTest : public ::testing::Test {
*mock_master_playlist,
AddMediaPlaylist(static_cast<MediaPlaylist*>(mock_media_playlist)));
EXPECT_CALL(*mock_media_playlist, SetMediaInfo(_)).WillOnce(Return(true));
EXPECT_CALL(*factory, CreateMock(_, _, _, _))
EXPECT_CALL(*factory, CreateMock(_, _, _, _, _))
.WillOnce(Return(mock_media_playlist));
InjectMasterPlaylist(std::move(mock_master_playlist));
@ -153,7 +162,7 @@ TEST_F(SimpleHlsNotifierTest, RebaseSegmentTemplateRelative) {
// Pointer released by SimpleHlsNotifier.
MockMediaPlaylist* mock_media_playlist =
new MockMediaPlaylist(kVodPlaylist, "", "", "");
new MockMediaPlaylist(kVodPlaylist, "playlist.m3u8", "", "");
EXPECT_CALL(*mock_master_playlist, AddMediaPlaylist(mock_media_playlist));
EXPECT_CALL(
@ -163,9 +172,10 @@ TEST_F(SimpleHlsNotifierTest, RebaseSegmentTemplateRelative) {
// Verify that the common prefix is stripped for AddSegment().
EXPECT_CALL(*mock_media_playlist,
AddSegment("http://testprefix.com/path/to/media1.ts", _, _));
EXPECT_CALL(*factory, CreateMock(kVodPlaylist, StrEq("video_playlist.m3u8"),
StrEq("name"), StrEq("groupid")))
AddSegment("http://testprefix.com/path/to/media1.ts", _, _, _));
EXPECT_CALL(*factory, CreateMock(kVodPlaylist, Eq(kTestTimeShiftBufferDepth),
StrEq("video_playlist.m3u8"), StrEq("name"),
StrEq("groupid")))
.WillOnce(Return(mock_media_playlist));
InjectMasterPlaylist(std::move(mock_master_playlist));
@ -188,9 +198,9 @@ TEST_F(SimpleHlsNotifierTest,
RebaseAbsoluteSegmentTemplatePrefixAndOutputDirMatch) {
const char kAbsoluteOutputDir[] = "/tmp/something/";
// Require a separate instance to set kAbsoluteOutputDir.
SimpleHlsNotifier test_notifier(HlsNotifier::HlsProfile::kOnDemandProfile,
kTestPrefix, kAbsoluteOutputDir,
kMasterPlaylistName);
SimpleHlsNotifier test_notifier(HlsProfile::kOnDemandProfile,
kTestTimeShiftBufferDepth, kTestPrefix,
kAbsoluteOutputDir, kMasterPlaylistName);
std::unique_ptr<MockMasterPlaylist> mock_master_playlist(
new MockMasterPlaylist());
@ -199,7 +209,7 @@ TEST_F(SimpleHlsNotifierTest,
// Pointer released by SimpleHlsNotifier.
MockMediaPlaylist* mock_media_playlist =
new MockMediaPlaylist(kVodPlaylist, "", "", "");
new MockMediaPlaylist(kVodPlaylist, "playlist.m3u8", "", "");
EXPECT_CALL(*mock_master_playlist, AddMediaPlaylist(mock_media_playlist));
EXPECT_CALL(*mock_media_playlist,
@ -208,9 +218,10 @@ TEST_F(SimpleHlsNotifierTest,
// Verify that the output_dir is stripped and then kTestPrefix is prepended.
EXPECT_CALL(*mock_media_playlist,
AddSegment("http://testprefix.com/media1.ts", _, _));
EXPECT_CALL(*factory, CreateMock(kVodPlaylist, StrEq("video_playlist.m3u8"),
StrEq("name"), StrEq("groupid")))
AddSegment("http://testprefix.com/media1.ts", _, _, _));
EXPECT_CALL(*factory, CreateMock(kVodPlaylist, Eq(kTestTimeShiftBufferDepth),
StrEq("video_playlist.m3u8"), StrEq("name"),
StrEq("groupid")))
.WillOnce(Return(mock_media_playlist));
InjectMasterPlaylist(std::move(mock_master_playlist), &test_notifier);
@ -232,9 +243,9 @@ TEST_F(SimpleHlsNotifierTest,
TEST_F(SimpleHlsNotifierTest,
RebaseAbsoluteSegmentTemplateCompletelyDifferentDirectory) {
const char kAbsoluteOutputDir[] = "/tmp/something/";
SimpleHlsNotifier test_notifier(HlsNotifier::HlsProfile::kOnDemandProfile,
kTestPrefix, kAbsoluteOutputDir,
kMasterPlaylistName);
SimpleHlsNotifier test_notifier(HlsProfile::kOnDemandProfile,
kTestTimeShiftBufferDepth, kTestPrefix,
kAbsoluteOutputDir, kMasterPlaylistName);
std::unique_ptr<MockMasterPlaylist> mock_master_playlist(
new MockMasterPlaylist());
@ -243,18 +254,19 @@ TEST_F(SimpleHlsNotifierTest,
// Pointer released by SimpleHlsNotifier.
MockMediaPlaylist* mock_media_playlist =
new MockMediaPlaylist(kVodPlaylist, "", "", "");
new MockMediaPlaylist(kVodPlaylist, "playlist.m3u8", "", "");
EXPECT_CALL(*mock_master_playlist, AddMediaPlaylist(mock_media_playlist));
EXPECT_CALL(
*mock_media_playlist,
SetMediaInfo(SegmentTemplateEq("/var/somewhereelse/media$Number$.ts")))
.WillOnce(Return(true));
EXPECT_CALL(
*mock_media_playlist,
AddSegment("http://testprefix.com//var/somewhereelse/media1.ts", _, _));
EXPECT_CALL(*factory, CreateMock(kVodPlaylist, StrEq("video_playlist.m3u8"),
StrEq("name"), StrEq("groupid")))
EXPECT_CALL(*mock_media_playlist,
AddSegment("http://testprefix.com//var/somewhereelse/media1.ts",
_, _, _));
EXPECT_CALL(*factory, CreateMock(kVodPlaylist, Eq(kTestTimeShiftBufferDepth),
StrEq("video_playlist.m3u8"), StrEq("name"),
StrEq("groupid")))
.WillOnce(Return(mock_media_playlist));
InjectMasterPlaylist(std::move(mock_master_playlist), &test_notifier);
@ -270,6 +282,17 @@ TEST_F(SimpleHlsNotifierTest,
kAnyStartTime, kAnyDuration, kAnySize));
}
TEST_F(SimpleHlsNotifierTest, Flush) {
std::unique_ptr<MockMasterPlaylist> mock_master_playlist(
new MockMasterPlaylist());
EXPECT_CALL(*mock_master_playlist,
WriteMasterPlaylist(StrEq(kTestPrefix), StrEq(kAnyOutputDir)))
.WillOnce(Return(true));
InjectMasterPlaylist(std::move(mock_master_playlist));
EXPECT_TRUE(notifier_.Init());
EXPECT_TRUE(notifier_.Flush());
}
TEST_F(SimpleHlsNotifierTest, NotifyNewStream) {
std::unique_ptr<MockMasterPlaylist> mock_master_playlist(
new MockMasterPlaylist());
@ -278,12 +301,13 @@ TEST_F(SimpleHlsNotifierTest, NotifyNewStream) {
// Pointer released by SimpleHlsNotifier.
MockMediaPlaylist* mock_media_playlist =
new MockMediaPlaylist(kVodPlaylist, "", "", "");
new MockMediaPlaylist(kVodPlaylist, "playlist.m3u8", "", "");
EXPECT_CALL(*mock_master_playlist, AddMediaPlaylist(mock_media_playlist));
EXPECT_CALL(*mock_media_playlist, SetMediaInfo(_)).WillOnce(Return(true));
EXPECT_CALL(*factory, CreateMock(kVodPlaylist, StrEq("video_playlist.m3u8"),
StrEq("name"), StrEq("groupid")))
EXPECT_CALL(*factory, CreateMock(kVodPlaylist, Eq(kTestTimeShiftBufferDepth),
StrEq("video_playlist.m3u8"), StrEq("name"),
StrEq("groupid")))
.WillOnce(Return(mock_media_playlist));
InjectMasterPlaylist(std::move(mock_master_playlist));
@ -304,13 +328,13 @@ TEST_F(SimpleHlsNotifierTest, NotifyNewSegment) {
// Pointer released by SimpleHlsNotifier.
MockMediaPlaylist* mock_media_playlist =
new MockMediaPlaylist(kVodPlaylist, "", "", "");
new MockMediaPlaylist(kVodPlaylist, "playlist.m3u8", "", "");
EXPECT_CALL(
*mock_master_playlist,
AddMediaPlaylist(static_cast<MediaPlaylist*>(mock_media_playlist)));
EXPECT_CALL(*mock_media_playlist, SetMediaInfo(_)).WillOnce(Return(true));
EXPECT_CALL(*factory, CreateMock(_, _, _, _))
EXPECT_CALL(*factory, CreateMock(_, _, _, _, _))
.WillOnce(Return(mock_media_playlist));
const uint64_t kStartTime = 1328;
@ -318,8 +342,15 @@ TEST_F(SimpleHlsNotifierTest, NotifyNewSegment) {
const uint64_t kSize = 6595840;
const std::string segment_name = "segmentname";
EXPECT_CALL(*mock_media_playlist,
AddSegment(StrEq(kTestPrefix + segment_name), kDuration, kSize));
AddSegment(StrEq(kTestPrefix + segment_name), kStartTime,
kDuration, kSize));
const double kLongestSegmentDuration = 11.3;
const uint32_t kTargetDuration = 12; // ceil(kLongestSegmentDuration).
EXPECT_CALL(*mock_media_playlist, GetLongestSegmentDuration())
.WillOnce(Return(kLongestSegmentDuration));
MockMasterPlaylist* mock_master_playlist_ptr = mock_master_playlist.get();
InjectMasterPlaylist(std::move(mock_master_playlist));
InjectMediaPlaylistFactory(std::move(factory));
EXPECT_TRUE(notifier_.Init());
@ -330,6 +361,22 @@ TEST_F(SimpleHlsNotifierTest, NotifyNewSegment) {
EXPECT_TRUE(notifier_.NotifyNewSegment(stream_id, segment_name, kStartTime,
kDuration, kSize));
Mock::VerifyAndClearExpectations(mock_master_playlist_ptr);
Mock::VerifyAndClearExpectations(mock_media_playlist);
EXPECT_CALL(*mock_master_playlist_ptr,
WriteMasterPlaylist(StrEq(kTestPrefix), StrEq(kAnyOutputDir)))
.WillOnce(Return(true));
EXPECT_CALL(*mock_media_playlist, SetTargetDuration(kTargetDuration))
.Times(1);
EXPECT_CALL(*mock_media_playlist,
WriteToFile(StrEq(
base::FilePath::FromUTF8Unsafe(kAnyOutputDir)
.Append(base::FilePath::FromUTF8Unsafe("playlist.m3u8"))
.AsUTF8Unsafe())))
.WillOnce(Return(true));
EXPECT_TRUE(notifier_.Flush());
}
TEST_F(SimpleHlsNotifierTest, NotifyNewSegmentWithoutStreamsRegistered) {
@ -340,7 +387,7 @@ TEST_F(SimpleHlsNotifierTest, NotifyNewSegmentWithoutStreamsRegistered) {
TEST_F(SimpleHlsNotifierTest, NotifyEncryptionUpdateWidevine) {
// Pointer released by SimpleHlsNotifier.
MockMediaPlaylist* mock_media_playlist =
new MockMediaPlaylist(kVodPlaylist, "", "", "");
new MockMediaPlaylist(kVodPlaylist, "playlist.m3u8", "", "");
const uint32_t stream_id =
SetupStream(kSampleAesProtectionScheme, mock_media_playlist);
@ -405,7 +452,7 @@ TEST_F(SimpleHlsNotifierTest, NotifyEncryptionUpdateWidevine) {
TEST_F(SimpleHlsNotifierTest, NotifyEncryptionUpdateWidevineNoKeyidsInPssh) {
// Pointer released by SimpleHlsNotifier.
MockMediaPlaylist* mock_media_playlist =
new MockMediaPlaylist(kVodPlaylist, "", "", "");
new MockMediaPlaylist(kVodPlaylist, "playlist.m3u8", "", "");
const uint32_t stream_id =
SetupStream(kSampleAesProtectionScheme, mock_media_playlist);
@ -467,7 +514,7 @@ TEST_F(SimpleHlsNotifierTest, NotifyEncryptionUpdateWidevineNoKeyidsInPssh) {
TEST_F(SimpleHlsNotifierTest, NotifyEncryptionUpdateFixedKey) {
// Pointer released by SimpleHlsNotifier.
MockMediaPlaylist* mock_media_playlist =
new MockMediaPlaylist(kVodPlaylist, "", "", "");
new MockMediaPlaylist(kVodPlaylist, "playlist.m3u8", "", "");
const uint32_t stream_id =
SetupStream(kSampleAesProtectionScheme, mock_media_playlist);
@ -496,7 +543,7 @@ TEST_F(SimpleHlsNotifierTest, NotifyEncryptionUpdateFixedKey) {
TEST_F(SimpleHlsNotifierTest, WidevineMultipleKeyIdsNoContentIdInPssh) {
// Pointer released by SimpleHlsNotifier.
MockMediaPlaylist* mock_media_playlist =
new MockMediaPlaylist(kVodPlaylist, "", "", "");
new MockMediaPlaylist(kVodPlaylist, "playlist.m3u8", "", "");
uint32_t stream_id =
SetupStream(kSampleAesProtectionScheme, mock_media_playlist);
@ -578,7 +625,7 @@ TEST_F(SimpleHlsNotifierTest, WidevineMultipleKeyIdsNoContentIdInPssh) {
TEST_F(SimpleHlsNotifierTest, EncryptionScheme) {
// Pointer released by SimpleHlsNotifier.
MockMediaPlaylist* mock_media_playlist =
new MockMediaPlaylist(kVodPlaylist, "", "", "");
new MockMediaPlaylist(kVodPlaylist, "playlist.m3u8", "", "");
const uint32_t stream_id =
SetupStream(kCencProtectionScheme, mock_media_playlist);
@ -605,7 +652,7 @@ TEST_F(SimpleHlsNotifierTest, EncryptionScheme) {
TEST_F(SimpleHlsNotifierTest, WidevineCencEncryptionScheme) {
// Pointer released by SimpleHlsNotifier.
MockMediaPlaylist* mock_media_playlist =
new MockMediaPlaylist(kVodPlaylist, "", "", "");
new MockMediaPlaylist(kVodPlaylist, "playlist.m3u8", "", "");
const uint32_t stream_id =
SetupStream(kCencProtectionScheme, mock_media_playlist);
@ -654,7 +701,7 @@ TEST_F(SimpleHlsNotifierTest, WidevineCencEncryptionScheme) {
TEST_F(SimpleHlsNotifierTest, WidevineNotifyEncryptionUpdateEmptyIv) {
// Pointer released by SimpleHlsNotifier.
MockMediaPlaylist* mock_media_playlist =
new MockMediaPlaylist(kVodPlaylist, "", "", "");
new MockMediaPlaylist(kVodPlaylist, "playlist.m3u8", "", "");
const uint32_t stream_id =
SetupStream(kSampleAesProtectionScheme, mock_media_playlist);
@ -727,19 +774,172 @@ TEST_F(SimpleHlsNotifierTest, NotifyEncryptionUpdateWithoutStreamsRegistered) {
pssh_data));
}
TEST_F(SimpleHlsNotifierTest, Flush) {
class LiveOrEventSimpleHlsNotifierTest
: public SimpleHlsNotifierTest,
public ::testing::WithParamInterface<HlsProfile> {
protected:
LiveOrEventSimpleHlsNotifierTest() : SimpleHlsNotifierTest(GetParam()) {
switch (GetParam()) {
case HlsProfile::kLiveProfile:
expected_playlist_type_ = MediaPlaylist::MediaPlaylistType::kLive;
break;
case HlsProfile::kOnDemandProfile:
expected_playlist_type_ = MediaPlaylist::MediaPlaylistType::kVod;
break;
case HlsProfile::kEventProfile:
expected_playlist_type_ = MediaPlaylist::MediaPlaylistType::kEvent;
break;
}
}
MediaPlaylist::MediaPlaylistType expected_playlist_type_;
};
TEST_P(LiveOrEventSimpleHlsNotifierTest, NotifyNewSegment) {
std::unique_ptr<MockMasterPlaylist> mock_master_playlist(
new MockMasterPlaylist());
EXPECT_CALL(*mock_master_playlist,
WriteAllPlaylists(StrEq(kTestPrefix), StrEq(kAnyOutputDir)))
.WillOnce(Return(true));
std::unique_ptr<MockMediaPlaylistFactory> factory(
new MockMediaPlaylistFactory());
// Pointer released by SimpleHlsNotifier.
MockMediaPlaylist* mock_media_playlist =
new MockMediaPlaylist(expected_playlist_type_, "playlist.m3u8", "", "");
EXPECT_CALL(
*mock_master_playlist,
AddMediaPlaylist(static_cast<MediaPlaylist*>(mock_media_playlist)));
EXPECT_CALL(*mock_media_playlist, SetMediaInfo(_)).WillOnce(Return(true));
EXPECT_CALL(*factory, CreateMock(expected_playlist_type_, _, _, _, _))
.WillOnce(Return(mock_media_playlist));
const uint64_t kStartTime = 1328;
const uint64_t kDuration = 398407;
const uint64_t kSize = 6595840;
const std::string segment_name = "segmentname";
EXPECT_CALL(*mock_media_playlist,
AddSegment(StrEq(kTestPrefix + segment_name), kStartTime,
kDuration, kSize));
const double kLongestSegmentDuration = 11.3;
const uint32_t kTargetDuration = 12; // ceil(kLongestSegmentDuration).
EXPECT_CALL(*mock_media_playlist, GetLongestSegmentDuration())
.WillOnce(Return(kLongestSegmentDuration));
EXPECT_CALL(*mock_master_playlist,
WriteMasterPlaylist(StrEq(kTestPrefix), StrEq(kAnyOutputDir)))
.WillOnce(Return(true));
EXPECT_CALL(*mock_media_playlist, SetTargetDuration(kTargetDuration))
.Times(1);
EXPECT_CALL(*mock_media_playlist,
WriteToFile(StrEq(
base::FilePath::FromUTF8Unsafe(kAnyOutputDir)
.Append(base::FilePath::FromUTF8Unsafe("playlist.m3u8"))
.AsUTF8Unsafe())))
.WillOnce(Return(true));
InjectMasterPlaylist(std::move(mock_master_playlist));
InjectMediaPlaylistFactory(std::move(factory));
EXPECT_TRUE(notifier_.Init());
EXPECT_TRUE(notifier_.Flush());
MediaInfo media_info;
uint32_t stream_id;
EXPECT_TRUE(notifier_.NotifyNewStream(media_info, "playlist.m3u8", "name",
"groupid", &stream_id));
EXPECT_TRUE(notifier_.NotifyNewSegment(stream_id, segment_name, kStartTime,
kDuration, kSize));
}
TEST_P(LiveOrEventSimpleHlsNotifierTest, NotifyNewSegmentsWithMultipleStreams) {
const uint64_t kStartTime = 1328;
const uint64_t kDuration = 398407;
const uint64_t kSize = 6595840;
InSequence in_sequence;
std::unique_ptr<MockMasterPlaylist> mock_master_playlist(
new MockMasterPlaylist());
std::unique_ptr<MockMediaPlaylistFactory> factory(
new MockMediaPlaylistFactory());
// Pointer released by SimpleHlsNotifier.
MockMediaPlaylist* mock_media_playlist1 =
new MockMediaPlaylist(expected_playlist_type_, "playlist1.m3u8", "", "");
MockMediaPlaylist* mock_media_playlist2 =
new MockMediaPlaylist(expected_playlist_type_, "playlist2.m3u8", "", "");
EXPECT_CALL(*factory, CreateMock(_, _, StrEq("playlist1.m3u8"), _, _))
.WillOnce(Return(mock_media_playlist1));
EXPECT_CALL(*mock_media_playlist1, SetMediaInfo(_)).WillOnce(Return(true));
EXPECT_CALL(
*mock_master_playlist,
AddMediaPlaylist(static_cast<MediaPlaylist*>(mock_media_playlist1)));
EXPECT_CALL(*factory, CreateMock(_, _, StrEq("playlist2.m3u8"), _, _))
.WillOnce(Return(mock_media_playlist2));
EXPECT_CALL(*mock_media_playlist2, SetMediaInfo(_)).WillOnce(Return(true));
EXPECT_CALL(
*mock_master_playlist,
AddMediaPlaylist(static_cast<MediaPlaylist*>(mock_media_playlist2)));
MockMasterPlaylist* mock_master_playlist_ptr = mock_master_playlist.get();
InjectMasterPlaylist(std::move(mock_master_playlist));
InjectMediaPlaylistFactory(std::move(factory));
EXPECT_TRUE(notifier_.Init());
MediaInfo media_info;
uint32_t stream_id1;
EXPECT_TRUE(notifier_.NotifyNewStream(media_info, "playlist1.m3u8", "name",
"groupid", &stream_id1));
uint32_t stream_id2;
EXPECT_TRUE(notifier_.NotifyNewStream(media_info, "playlist2.m3u8", "name",
"groupid", &stream_id2));
EXPECT_CALL(*mock_media_playlist1, AddSegment(_, _, _, _)).Times(1);
const double kLongestSegmentDuration = 11.3;
const uint32_t kTargetDuration = 12; // ceil(kLongestSegmentDuration).
EXPECT_CALL(*mock_media_playlist1, GetLongestSegmentDuration())
.WillOnce(Return(kLongestSegmentDuration));
EXPECT_CALL(*mock_master_playlist_ptr, WriteMasterPlaylist(_, _))
.WillOnce(Return(true));
// SetTargetDuration and update all playlists as target duration is updated.
EXPECT_CALL(*mock_media_playlist1, SetTargetDuration(kTargetDuration))
.Times(1);
EXPECT_CALL(*mock_media_playlist1,
WriteToFile(StrEq(
base::FilePath::FromUTF8Unsafe(kAnyOutputDir)
.Append(base::FilePath::FromUTF8Unsafe("playlist1.m3u8"))
.AsUTF8Unsafe())))
.WillOnce(Return(true));
EXPECT_CALL(*mock_media_playlist2, SetTargetDuration(kTargetDuration))
.Times(1);
EXPECT_CALL(*mock_media_playlist2,
WriteToFile(StrEq(
base::FilePath::FromUTF8Unsafe(kAnyOutputDir)
.Append(base::FilePath::FromUTF8Unsafe("playlist2.m3u8"))
.AsUTF8Unsafe())))
.WillOnce(Return(true));
EXPECT_TRUE(notifier_.NotifyNewSegment(stream_id1, "segment_name", kStartTime,
kDuration, kSize));
EXPECT_CALL(*mock_media_playlist2, AddSegment(_, _, _, _)).Times(1);
EXPECT_CALL(*mock_media_playlist2, GetLongestSegmentDuration())
.WillOnce(Return(kLongestSegmentDuration));
EXPECT_CALL(*mock_master_playlist_ptr, WriteMasterPlaylist(_, _))
.WillOnce(Return(true));
// Not updating other playlists as target duration does not change.
EXPECT_CALL(*mock_media_playlist2,
WriteToFile(StrEq(
base::FilePath::FromUTF8Unsafe(kAnyOutputDir)
.Append(base::FilePath::FromUTF8Unsafe("playlist2.m3u8"))
.AsUTF8Unsafe())))
.WillOnce(Return(true));
EXPECT_TRUE(notifier_.NotifyNewSegment(stream_id2, "segment_name", kStartTime,
kDuration, kSize));
}
INSTANTIATE_TEST_CASE_P(PlaylistTypes,
LiveOrEventSimpleHlsNotifierTest,
::testing::Values(HlsProfile::kLiveProfile,
HlsProfile::kEventProfile));
} // namespace hls
} // namespace shaka

View File

@ -121,6 +121,7 @@ void HlsNotifyMuxerListener::OnMediaEnd(bool has_init_range,
uint64_t index_range_end,
float duration_seconds,
uint64_t file_size) {
// TODO(kqyang): Should we just Flush here to avoid calling Flush explicitly?
// Don't flush the notifier here. Flushing here would write all the playlists
// before all Media Playlists are read. Which could cause problems
// setting the correct EXT-X-TARGETDURATION.

View File

@ -24,8 +24,7 @@ namespace {
class MockHlsNotifier : public hls::HlsNotifier {
public:
MockHlsNotifier()
: HlsNotifier(hls::HlsNotifier::HlsProfile::kOnDemandProfile) {}
MockHlsNotifier() : HlsNotifier(hls::HlsProfile::kOnDemandProfile) {}
MOCK_METHOD0(Init, bool());
MOCK_METHOD5(NotifyNewStream,

View File

@ -20,15 +20,22 @@ namespace media {
#define ASSERT_FILE_EQ(file_name, array) \
do { \
std::string temp_data; \
ASSERT_TRUE(File::ReadFileToString((file_name), &temp_data)); \
ASSERT_TRUE(media::File::ReadFileToString((file_name), &temp_data)); \
const char* array_ptr = reinterpret_cast<const char*>(array); \
ASSERT_EQ(std::string(array_ptr, arraysize(array)), temp_data); \
} while (false)
#define ASSERT_FILE_STREQ(file_name, str) \
do { \
std::string temp_data; \
ASSERT_TRUE(media::File::ReadFileToString((file_name), &temp_data)); \
ASSERT_EQ(str, temp_data); \
} while (false)
#define ASSERT_FILE_ENDS_WITH(file_name, array) \
do { \
std::string temp_data; \
ASSERT_TRUE(File::ReadFileToString((file_name), &temp_data)); \
ASSERT_TRUE(media::File::ReadFileToString((file_name), &temp_data)); \
EXPECT_THAT(temp_data, \
::testing::EndsWith(std::string( \
reinterpret_cast<const char*>(array), sizeof(array)))); \

View File

@ -529,8 +529,7 @@ class Representation {
uint64_t duration,
uint64_t size) const;
// Remove elements from |segment_infos_| if
// mpd_options_.time_shift_buffer_depth is specified. Increments
// Remove elements from |segment_infos_| for dynamic live profile. Increments
// |start_number_| by the number of segments removed.
void SlideWindow();

View File

@ -207,6 +207,20 @@ bool ValidateParams(const PackagingParams& packaging_params,
return true;
}
hls::HlsProfile GetHlsNotifierProfile(HlsPlaylistType playlist_type) {
switch (playlist_type) {
case HlsPlaylistType::kVod:
return hls::HlsProfile::kOnDemandProfile;
case HlsPlaylistType::kEvent:
return hls::HlsProfile::kEventProfile;
case HlsPlaylistType::kLive:
return hls::HlsProfile::kLiveProfile;
}
LOG(WARNING) << "Unrecognized playlist type ("
<< static_cast<int>(playlist_type) << "). Assuming VOD.";
return hls::HlsProfile::kOnDemandProfile;
}
class StreamDescriptorCompareFn {
public:
bool operator()(const StreamDescriptor& a, const StreamDescriptor& b) {
@ -635,7 +649,8 @@ Status Packager::Initialize(
base::FilePath master_playlist_name = master_playlist_path.BaseName();
internal->hls_notifier.reset(new hls::SimpleHlsNotifier(
hls::HlsNotifier::HlsProfile::kOnDemandProfile, hls_params.base_url,
media::GetHlsNotifierProfile(hls_params.playlist_type),
hls_params.time_shift_buffer_depth, hls_params.base_url,
master_playlist_path.DirName().AsEndingWithSeparator().AsUTF8Unsafe(),
master_playlist_name.AsUTF8Unsafe()));
}

View File

@ -118,13 +118,26 @@ struct MpdParams {
bool generate_dash_if_iop_compliant_mpd = true;
};
/// Defines the EXT-X-PLAYLIST-TYPE in the HLS specification. For
/// HlsPlaylistType of kLive, EXT-X-PLAYLIST-TYPE tag is omitted.
enum class HlsPlaylistType {
kVod,
kEvent,
kLive,
};
/// HLS related parameters.
struct HlsParams {
/// HLS playlist type. See HLS specification for details.
HlsPlaylistType playlist_type = HlsPlaylistType::kVod;
/// HLS master playlist output path.
std::string master_playlist_output;
/// The base URL for the Media Playlists and media files listed in the
/// playlists. This is the prefix for the files.
std::string base_url;
/// Defines the live window, or the guaranteed duration of the time shifting
/// buffer for 'live' playlists.
double time_shift_buffer_depth = 0;
};
/// Encryption / decryption key providers.