diff --git a/packager/app/hls_flags.cc b/packager/app/hls_flags.cc index 0afad75f01..f61214b7ea 100644 --- a/packager/app/hls_flags.cc +++ b/packager/app/hls_flags.cc @@ -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."); diff --git a/packager/app/hls_flags.h b/packager/app/hls_flags.h index 043bc97bff..9fd71e1052 100644 --- a/packager/app/hls_flags.h +++ b/packager/app/hls_flags.h @@ -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_ diff --git a/packager/app/packager_main.cc b/packager/app/packager_main.cc index 7cc85b0d47..5e6698398a 100644 --- a/packager/app/packager_main.cc +++ b/packager/app/packager_main.cc @@ -259,8 +259,19 @@ base::Optional 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; diff --git a/packager/app/test/packager_test.py b/packager/app/test/packager_test.py index 1844044f3a..1279425f61 100755 --- a/packager/app/test/packager_test.py +++ b/packager/app/test/packager_test.py @@ -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'], diff --git a/packager/app/test/testdata/bear-640x360-a-event-golden.m3u8 b/packager/app/test/testdata/bear-640x360-a-event-golden.m3u8 new file mode 100644 index 0000000000..cb2d47ca5d --- /dev/null +++ b/packager/app/test/testdata/bear-640x360-a-event-golden.m3u8 @@ -0,0 +1,11 @@ +#EXTM3U +#EXT-X-VERSION:6 +## Generated with https://github.com/google/shaka-packager version -- +#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 diff --git a/packager/app/test/testdata/bear-640x360-a-live-golden.m3u8 b/packager/app/test/testdata/bear-640x360-a-live-golden.m3u8 new file mode 100644 index 0000000000..c2993e32b6 --- /dev/null +++ b/packager/app/test/testdata/bear-640x360-a-live-golden.m3u8 @@ -0,0 +1,9 @@ +#EXTM3U +#EXT-X-VERSION:6 +## Generated with https://github.com/google/shaka-packager version -- +#EXT-X-TARGETDURATION:2 +#EXT-X-MEDIA-SEQUENCE:1 +#EXTINF:0.998, +output_audio-2.ts +#EXTINF:0.789, +output_audio-3.ts diff --git a/packager/app/test/testdata/bear-640x360-v-event-golden.m3u8 b/packager/app/test/testdata/bear-640x360-v-event-golden.m3u8 new file mode 100644 index 0000000000..ee5bc4294c --- /dev/null +++ b/packager/app/test/testdata/bear-640x360-v-event-golden.m3u8 @@ -0,0 +1,11 @@ +#EXTM3U +#EXT-X-VERSION:6 +## Generated with https://github.com/google/shaka-packager version -- +#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 diff --git a/packager/app/test/testdata/bear-640x360-v-live-golden.m3u8 b/packager/app/test/testdata/bear-640x360-v-live-golden.m3u8 new file mode 100644 index 0000000000..70b5992c3f --- /dev/null +++ b/packager/app/test/testdata/bear-640x360-v-live-golden.m3u8 @@ -0,0 +1,9 @@ +#EXTM3U +#EXT-X-VERSION:6 +## Generated with https://github.com/google/shaka-packager version -- +#EXT-X-TARGETDURATION:2 +#EXT-X-MEDIA-SEQUENCE:1 +#EXTINF:1.001, +output_video-2.ts +#EXTINF:0.734, +output_video-3.ts diff --git a/packager/hls/base/hls_notifier.h b/packager/hls/base/hls_notifier.h index 00d7f812b9..26dfac5480 100644 --- a/packager/hls/base/hls_notifier.h +++ b/packager/hls/base/hls_notifier.h @@ -15,14 +15,16 @@ namespace shaka { namespace hls { +// 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: - enum class HlsProfile { - kOnDemandProfile, - kLiveProfile, - }; - 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: diff --git a/packager/hls/base/master_playlist.cc b/packager/hls/base/master_playlist.cc index d94003d14a..6b0f4d2b2b 100644 --- a/packager/hls/base/master_playlist.cc +++ b/packager/hls/base/master_playlist.cc @@ -8,9 +8,6 @@ #include -#include -#include - #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(ceil(longest_segment_duration))); - LOG_IF(WARNING, !set_target_duration) - << "Target duration was already set for " << file_path; - } - - std::unique_ptr 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 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 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; } diff --git a/packager/hls/base/master_playlist.h b/packager/hls/base/master_playlist.h index 42eafd3849..bcd8affd32 100644 --- a/packager/hls/base/master_playlist.h +++ b/packager/hls/base/master_playlist.h @@ -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 + . - /// 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 + . /// 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 all_playlists_; std::list video_playlists_; // The key is the audio group name. std::map> audio_playlist_groups_; - bool has_set_playlist_target_duration_ = false; - DISALLOW_COPY_AND_ASSIGN(MasterPlaylist); }; diff --git a/packager/hls/base/master_playlist_unittest.cc b/packager/hls/base/master_playlist_unittest.cc index 48a912f380..f82b2dae8c 100644 --- a/packager/hls/base/master_playlist_unittest.cc +++ b/packager/hls/base/master_playlist_unittest.cc @@ -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 diff --git a/packager/hls/base/media_playlist.cc b/packager/hls/base/media_playlist.cc index 723d91b3d1..03053b0b33 100644 --- a/packager/hls/base/media_playlist.cc +++ b/packager/hls/base/media_playlist.cc @@ -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, - uint32_t target_duration, - MediaPlaylist::MediaPlaylistType type) { +std::string CreatePlaylistHeader(const std::string& init_segment_name, + uint32_t target_duration, + 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) { - header += "#EXT-X-PLAYLIST-TYPE:VOD\n"; + 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(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>& 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(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(start_time) / time_scale_; const double segment_duration_seconds = static_cast(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 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> 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>::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(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 diff --git a/packager/hls/base/media_playlist.h b/packager/hls/base/media_playlist.h index 069866f956..6a2736e293 100644 --- a/packager/hls/base/media_playlist.h +++ b/packager/hls/base/media_playlist.h @@ -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; diff --git a/packager/hls/base/media_playlist_unittest.cc b/packager/hls/base/media_playlist_unittest.cc index 34f412f514..cfe2a04851 100644 --- a/packager/hls/base/media_playlist_unittest.cc +++ b/packager/hls/base/media_playlist_unittest.cc @@ -7,8 +7,8 @@ #include #include -#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(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 diff --git a/packager/hls/base/mock_media_playlist.cc b/packager/hls/base/mock_media_playlist.cc index ccc07da0a2..d04055f0e7 100644 --- a/packager/hls/base/mock_media_playlist.cc +++ b/packager/hls/base/mock_media_playlist.cc @@ -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 diff --git a/packager/hls/base/mock_media_playlist.h b/packager/hls/base/mock_media_playlist.h index 268fd8dfc2..5e00706406 100644 --- a/packager/hls/base/mock_media_playlist.h +++ b/packager/hls/base/mock_media_playlist.h @@ -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)); }; diff --git a/packager/hls/base/simple_hls_notifier.cc b/packager/hls/base/simple_hls_notifier.cc index d77b61552b..4d61e72472 100644 --- a/packager/hls/base/simple_hls_notifier.cc +++ b/packager/hls/base/simple_hls_notifier.cc @@ -6,6 +6,8 @@ #include "packager/hls/base/simple_hls_notifier.h" +#include + #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 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( - new MediaPlaylist(type, file_name, name, group_id)); + return std::unique_ptr(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 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(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 diff --git a/packager/hls/base/simple_hls_notifier.h b/packager/hls/base/simple_hls_notifier.h index 411a0cdf46..714481ea06 100644 --- a/packager/hls/base/simple_hls_notifier.h +++ b/packager/hls/base/simple_hls_notifier.h @@ -29,6 +29,7 @@ class MediaPlaylistFactory { virtual ~MediaPlaylistFactory(); virtual std::unique_ptr 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 media_playlist_factory_; std::unique_ptr master_playlist_; diff --git a/packager/hls/base/simple_hls_notifier_unittest.cc b/packager/hls/base/simple_hls_notifier_unittest.cc index 44abea21b6..4f3e943e72 100644 --- a/packager/hls/base/simple_hls_notifier_unittest.cc +++ b/packager/hls/base/simple_hls_notifier_unittest.cc @@ -8,6 +8,7 @@ #include #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 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( - 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(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 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 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 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 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(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 { + 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 mock_master_playlist( new MockMasterPlaylist()); - EXPECT_CALL(*mock_master_playlist, - WriteAllPlaylists(StrEq(kTestPrefix), StrEq(kAnyOutputDir))) - .WillOnce(Return(true)); std::unique_ptr 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(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 mock_master_playlist( + new MockMasterPlaylist()); + std::unique_ptr 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(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(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 diff --git a/packager/media/event/hls_notify_muxer_listener.cc b/packager/media/event/hls_notify_muxer_listener.cc index 8aa6d08ae3..30103640bc 100644 --- a/packager/media/event/hls_notify_muxer_listener.cc +++ b/packager/media/event/hls_notify_muxer_listener.cc @@ -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. diff --git a/packager/media/event/hls_notify_muxer_listener_unittest.cc b/packager/media/event/hls_notify_muxer_listener_unittest.cc index 134d912e9d..9a0c25be53 100644 --- a/packager/media/event/hls_notify_muxer_listener_unittest.cc +++ b/packager/media/event/hls_notify_muxer_listener_unittest.cc @@ -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, diff --git a/packager/media/file/file_test_util.h b/packager/media/file/file_test_util.h index 697beb41d9..748646b144 100644 --- a/packager/media/file/file_test_util.h +++ b/packager/media/file/file_test_util.h @@ -17,18 +17,25 @@ namespace shaka { namespace media { -#define ASSERT_FILE_EQ(file_name, array) \ - do { \ - std::string temp_data; \ - ASSERT_TRUE(File::ReadFileToString((file_name), &temp_data)); \ - const char* array_ptr = reinterpret_cast(array); \ - ASSERT_EQ(std::string(array_ptr, arraysize(array)), temp_data); \ +#define ASSERT_FILE_EQ(file_name, array) \ + do { \ + std::string temp_data; \ + ASSERT_TRUE(media::File::ReadFileToString((file_name), &temp_data)); \ + const char* array_ptr = reinterpret_cast(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(array), sizeof(array)))); \ diff --git a/packager/mpd/base/mpd_builder.h b/packager/mpd/base/mpd_builder.h index e3a8a6431d..7a443658fb 100644 --- a/packager/mpd/base/mpd_builder.h +++ b/packager/mpd/base/mpd_builder.h @@ -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(); diff --git a/packager/packager.cc b/packager/packager.cc index 09280e0613..a8427994c7 100644 --- a/packager/packager.cc +++ b/packager/packager.cc @@ -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(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())); } diff --git a/packager/packager.h b/packager/packager.h index e2f1aa8b75..5237cd379d 100644 --- a/packager/packager.h +++ b/packager/packager.h @@ -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.