From 2adaf1712df40c7e08a4cfbadd8adc7998bb9198 Mon Sep 17 00:00:00 2001 From: Rintaro Kuroiwa Date: Fri, 25 Mar 2016 01:35:44 -0700 Subject: [PATCH] HLS MediaPlaylist class - A class to generate Media Playlists. Issue #85 Change-Id: I689e97c767049bc21de279c743cbabf4ca4711be --- packager/hls/base/media_playlist.cc | 316 +++++++++++++++++++ packager/hls/base/media_playlist.h | 175 ++++++++++ packager/hls/base/media_playlist_unittest.cc | 285 +++++++++++++++++ packager/hls/hls.gyp | 41 +++ packager/packager.gyp | 2 + 5 files changed, 819 insertions(+) create mode 100644 packager/hls/base/media_playlist.cc create mode 100644 packager/hls/base/media_playlist.h create mode 100644 packager/hls/base/media_playlist_unittest.cc create mode 100644 packager/hls/hls.gyp diff --git a/packager/hls/base/media_playlist.cc b/packager/hls/base/media_playlist.cc new file mode 100644 index 0000000000..16ce34c1b6 --- /dev/null +++ b/packager/hls/base/media_playlist.cc @@ -0,0 +1,316 @@ +// Copyright 2016 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "packager/hls/base/media_playlist.h" + +#include +#include + +#include "packager/base/logging.h" +#include "packager/base/strings/stringprintf.h" +#include "packager/media/file/file.h" + +namespace edash_packager { +namespace hls { + +namespace { +uint32_t GetTimeScale(const MediaInfo& media_info) { + if (media_info.has_reference_time_scale()) + return media_info.reference_time_scale(); + + if (media_info.has_video_info()) + return media_info.video_info().time_scale(); + + if (media_info.has_audio_info()) + return media_info.audio_info().time_scale(); + return 0u; +} + +class SegmentInfoEntry : public HlsEntry { + public: + SegmentInfoEntry(const std::string& file_name, double duration); + ~SegmentInfoEntry() override; + + std::string ToString() override; + + private: + const std::string file_name_; + const double duration_; + + DISALLOW_COPY_AND_ASSIGN(SegmentInfoEntry); +}; + +SegmentInfoEntry::SegmentInfoEntry(const std::string& file_name, + double duration) + : HlsEntry(HlsEntry::EntryType::kExtInf), + file_name_(file_name), + duration_(duration) {} +SegmentInfoEntry::~SegmentInfoEntry() {} + +std::string SegmentInfoEntry::ToString() { + return base::StringPrintf("#EXTINF:%.3f\n%s\n", duration_, + file_name_.c_str()); +} + +class EncryptionInfoEntry : public HlsEntry { + public: + EncryptionInfoEntry(MediaPlaylist::EncryptionMethod method, + const std::string& url, + const std::string& iv, + const std::string& key_format, + const std::string& key_format_versions); + + ~EncryptionInfoEntry() override; + + std::string ToString() override; + + private: + const MediaPlaylist::EncryptionMethod method_; + const std::string url_; + const std::string iv_; + const std::string key_format_; + const std::string key_format_versions_; + + DISALLOW_COPY_AND_ASSIGN(EncryptionInfoEntry); +}; + +EncryptionInfoEntry::EncryptionInfoEntry(MediaPlaylist::EncryptionMethod method, + const std::string& url, + const std::string& iv, + const std::string& key_format, + const std::string& key_format_versions) + : HlsEntry(HlsEntry::EntryType::kExtKey), + method_(method), + url_(url), + iv_(iv), + key_format_(key_format), + key_format_versions_(key_format_versions) {} + +EncryptionInfoEntry::~EncryptionInfoEntry() {} + +std::string EncryptionInfoEntry::ToString() { + std::string method_attribute; + if (method_ == MediaPlaylist::EncryptionMethod::kSampleAes) { + method_attribute = "METHOD=SAMPLE-AES"; + } else if (method_ == MediaPlaylist::EncryptionMethod::kAes128) { + method_attribute = "METHOD=AES-128"; + } else { + DCHECK(method_ == MediaPlaylist::EncryptionMethod::kNone); + method_attribute = "METHOD=NONE"; + } + std::string ext_key = "#EXT-X-KEY:" + method_attribute + ",URI=\"" + url_ + + "\""; + if (!iv_.empty()) { + ext_key += ",IV=" + iv_; + } + if (!key_format_versions_.empty()) { + ext_key += ",KEYFORMATVERSIONS=\"" + key_format_versions_ + "\""; + } + if (key_format_.empty()) + return ext_key + "\n"; + + return ext_key + ",KEYFORMAT=\"" + key_format_ + "\"\n"; +} + +} // namespace + +HlsEntry::HlsEntry(HlsEntry::EntryType type) : type_(type) {} +HlsEntry::~HlsEntry() {} + +MediaPlaylist::MediaPlaylist(const std::string& file_name, + const std::string& name, + const std::string& group_id) + : file_name_(file_name), name_(name), group_id_(group_id) {} +MediaPlaylist::~MediaPlaylist() {} + +void MediaPlaylist::SetTypeForTesting(MediaPlaylistType type) { + type_ = type; +} + +void MediaPlaylist::SetCodecForTesting(const std::string& codec) { + codec_ = codec; +} + +bool MediaPlaylist::SetMediaInfo(const MediaInfo& media_info) { + const uint32_t time_scale = GetTimeScale(media_info); + if (time_scale == 0) { + LOG(ERROR) << "MediaInfo does not contain a valid timescale."; + return false; + } + + if (media_info.has_video_info()) { + type_ = MediaPlaylistType::kPlayListVideo; + codec_ = media_info.video_info().codec(); + } else if (media_info.has_audio_info()) { + type_ = MediaPlaylistType::kPlayListAudio; + codec_ = media_info.audio_info().codec(); + } else { + NOTIMPLEMENTED(); + return false; + } + + time_scale_ = time_scale; + media_info_ = media_info; + return true; +} + +void MediaPlaylist::AddSegment(const std::string& file_name, + 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."; + + scoped_ptr info(new SegmentInfoEntry(file_name, 0.0)); + entries_.push_back(info.Pass()); + return; + } + + const double segment_duration = static_cast(duration) / time_scale_; + if (segment_duration > longest_segment_duration_) + longest_segment_duration_ = segment_duration; + + total_duration_in_seconds_ += segment_duration; + total_segments_size_ += size; + ++total_num_segments_; + + scoped_ptr info( + new SegmentInfoEntry(file_name, segment_duration)); + entries_.push_back(info.Pass()); +} + +// TODO(rkuroiwa): This works for single key format but won't work for multiple +// key formats (e.g. different DRM systems). +// Candidate algorithm: +// Assume entries_ is std::list (static_assert below). +// Create a map from key_format to EncryptionInfoEntry (iterator actually). +// Iterate over entries_ until it hits SegmentInfoEntry. While iterating over +// entries_ if there are multiple EncryptionInfoEntry with the same key_format, +// erase the older ones using the iterator. +// Note that when erasing std::list iterators, only the deleted iterators are +// invalidated. +void MediaPlaylist::RemoveOldestSegment() { + static_assert( + std::is_same>>::value, + "This algorithm assumes std::list."); + if (entries_.empty()) + return; + if (entries_.front()->type() == HlsEntry::EntryType::kExtInf) { + entries_.pop_front(); + return; + } + + // Make sure that the first EXT-X-KEY entry doesn't get popped out until the + // next EXT-X-KEY entry because the first EXT-X-KEY applies to all the + // segments following until the next one. + + if (entries_.size() == 1) { + // More segments might get added, leave the entry in. + return; + } + + if (entries_.size() == 2) { + auto entries_itr = entries_.begin(); + ++entries_itr; + if ((*entries_itr)->type() == HlsEntry::EntryType::kExtKey) { + entries_.pop_front(); + } else { + entries_.erase(entries_itr); + } + return; + } + + auto entries_itr = entries_.begin(); + ++entries_itr; + if ((*entries_itr)->type() == HlsEntry::EntryType::kExtInf) { + DCHECK((*entries_itr)->type() == HlsEntry::EntryType::kExtInf); + entries_.erase(entries_itr); + return; + } + + ++entries_itr; + // This assumes that there is a segment between 2 EXT-X-KEY entries. + // Which should be the case due to logic in AddEncryptionInfo(). + DCHECK((*entries_itr)->type() == HlsEntry::EntryType::kExtInf); + entries_.erase(entries_itr); + entries_.pop_front(); +} + +void MediaPlaylist::AddEncryptionInfo(MediaPlaylist::EncryptionMethod method, + const std::string& url, + const std::string& iv, + const std::string& key_format, + const std::string& key_format_versions) { + if (!entries_.empty()) { + // No reason to have two consecutive EXT-X-KEY entries. Remove the previous + // one. + if (entries_.back()->type() == HlsEntry::EntryType::kExtKey) + entries_.pop_back(); + } + scoped_ptr info(new EncryptionInfoEntry( + method, url, iv, key_format, key_format_versions)); + entries_.push_back(info.Pass()); +} + +bool MediaPlaylist::WriteToFile(media::File* file) { + if (!target_duration_set_) { + SetTargetDuration(ceil(GetLongestSegmentDuration())); + } + + std::string header = base::StringPrintf("#EXTM3U\n" + "#EXT-X-TARGETDURATION:%d\n", + target_duration_); + std::string body; + for (const auto& entry : entries_) { + body.append(entry->ToString()); + } + + std::string content = header + body; + int64_t bytes_written = file->Write(content.data(), content.size()); + if (bytes_written < 0) { + LOG(ERROR) << "Error while writing playlist to file."; + return false; + } + + // TODO(rkuroiwa): There are at least 2 while (remaining_bytes > 0) logic in + // this library to handle partial writes by File. Dedup them and use it here + // has well. + if (static_cast(bytes_written) < content.size()) { + LOG(ERROR) << "Failed to write the whole playlist. Wrote " << bytes_written + << " but the playlist is " << content.size() << " bytes."; + return false; + } + + return true; +} + +uint64_t MediaPlaylist::Bitrate() const { + if (media_info_.has_bandwidth()) + return media_info_.bandwidth(); + if (total_duration_in_seconds_ == 0.0) + return 0; + if (total_segments_size_ == 0) + return 0; + return total_segments_size_ / total_duration_in_seconds_; +} + +double MediaPlaylist::GetLongestSegmentDuration() { + return longest_segment_duration_; +} + +bool 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; + } + target_duration_ = target_duration; + target_duration_set_ = true; + return true; +} + +} // namespace hls +} // namespace edash_packager diff --git a/packager/hls/base/media_playlist.h b/packager/hls/base/media_playlist.h new file mode 100644 index 0000000000..37c3b4113d --- /dev/null +++ b/packager/hls/base/media_playlist.h @@ -0,0 +1,175 @@ +// Copyright 2016 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#ifndef PACKAGER_HLS_BASE_MEDIA_PLAYLIST_H_ +#define PACKAGER_HLS_BASE_MEDIA_PLAYLIST_H_ + +#include +#include + +#include "packager/base/macros.h" +#include "packager/base/memory/scoped_ptr.h" +#include "packager/mpd/base/media_info.pb.h" + +namespace edash_packager { + +namespace media { +class File; +} // namespace media + +namespace hls { + +class HlsEntry { + public: + enum class EntryType { + kExtInf, + kExtKey, + }; + virtual ~HlsEntry(); + + EntryType type() const { return type_; } + virtual std::string ToString() = 0; + + protected: + explicit HlsEntry(EntryType type); + + private: + EntryType type_; +}; + +/// Methods are virtual for mocking. +class MediaPlaylist { + public: + enum class MediaPlaylistType { + kPlaylistUnknown, + kPlayListAudio, + kPlayListVideo, + kPlayListSubtitle, + }; + enum class EncryptionMethod { + kNone, // No encryption, i.e. clear. + kAes128, // Completely encrypted using AES-CBC. + kSampleAes, // Encrypted using Sample AES method. + }; + + /// @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 + /// necessarily the same as @a file_name. + /// @param group_id is the group ID for this playlist. This is the value of + /// GROUP-ID attribute for EXT-X-MEDIA. + MediaPlaylist( + const std::string& file_name, + const std::string& name, + const std::string& group_id); + virtual ~MediaPlaylist(); + + const std::string& file_name() const { return file_name_; } + const std::string& name() const { return name_; } + const std::string& group_id() const { return group_id_; } + MediaPlaylistType type() const { return type_; } + const std::string& codec() const { return codec_; } + + /// For testing only. + void SetTypeForTesting(MediaPlaylistType type); + + /// For testing only. + void SetCodecForTesting(const std::string& codec); + + /// This must succeed before calling any other public methods. + /// @param media_info is the info of the segments that are going to be added + /// to this playlist. + /// @return true on success, false otherwise. + virtual bool SetMediaInfo(const MediaInfo& media_info); + + /// Segments must be added in order. + /// @param file_name is the file name of the segment. + /// @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 duration, + uint64_t size); + + /// Removes the oldest segment from the playlist. Useful for manually managing + /// the length of the playlist. + virtual void RemoveOldestSegment(); + + /// All segments added after calling this method must be decryptable with + /// the key that can be fetched from |url|, until calling this again. + /// @param method is the encryption method. + /// @param url specifies where the key is i.e. the value of the URI attribute. + /// @param iv is the initialization vector in human readable format, i.e. the + /// value for IV attribute. This may be empty. + /// @param key_format is the key format, i.e. the KEYFORMAT value. This may be + /// empty. + /// @param key_format_versions is the KEYFORMATVERIONS value. This may be + /// empty. + virtual void AddEncryptionInfo(EncryptionMethod method, + const std::string& url, + const std::string& iv, + const std::string& key_format, + const std::string& key_format_versions); + + /// Write the playlist to |file|. + /// 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. + /// @return true on success, false otherwise. + virtual bool WriteToFile(media::File* file); + + /// If bitrate is specified in MediaInfo then it will use that value. + /// Otherwise, it is calculated from the duration and the size of the + /// segments added to this object. + /// @return the bitrate of this MediaPlaylist. + virtual uint64_t Bitrate() const; + + /// @return the longest segment’s duration. This will return 0 if no + /// segments have been added. + virtual double GetLongestSegmentDuration(); + + /// Set the target duration of this 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. + /// @param target_duration is the target duration for this playlist. + /// @return true if set, false otherwise. + virtual bool SetTargetDuration(uint32_t target_duration); + + private: + // Mainly for MasterPlaylist to use these values. + const std::string file_name_; + const std::string name_; + const std::string group_id_; + MediaInfo media_info_; + MediaPlaylistType type_ = MediaPlaylistType::kPlaylistUnknown; + std::string codec_; + + double longest_segment_duration_ = 0.0; + uint32_t time_scale_ = 0; + + uint64_t total_segments_size_ = 0; + double total_duration_in_seconds_ = 0.0; + int total_num_segments_; + + // See SetTargetDuration() comments. + bool target_duration_set_ = false; + uint32_t target_duration_ = 0; + + std::list> entries_; + + DISALLOW_COPY_AND_ASSIGN(MediaPlaylist); +}; + +} // namespace hls +} // namespace edash_packager + +#endif // PACKAGER_HLS_BASE_MEDIA_PLAYLIST_H_ diff --git a/packager/hls/base/media_playlist_unittest.cc b/packager/hls/base/media_playlist_unittest.cc new file mode 100644 index 0000000000..a69c8937c1 --- /dev/null +++ b/packager/hls/base/media_playlist_unittest.cc @@ -0,0 +1,285 @@ +// Copyright 2016 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include +#include + +#include "packager/media/file/file.h" +#include "packager/hls/base/media_playlist.h" + +namespace edash_packager { +namespace hls { + +using ::testing::_; +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()); +}; + +MATCHER_P(MatchesString, expected_string, "") { + const std::string arg_string(static_cast(arg)); + *result_listener << "which is " << arg_string.size() + << " long and the content is " << arg_string; + return expected_string == std::string(static_cast(arg)); +} + +} // namespace + +class MediaPlaylistTest : public ::testing::Test { + protected: + MediaPlaylistTest() + : default_file_name_(kDefaultPlaylistFileName), + default_name_("default_name"), + default_group_id_("default_group_id"), + media_playlist_(default_file_name_, default_name_, default_group_id_) {} + + void SetUp() override { + 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_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); + } + + const std::string default_file_name_; + const std::string default_name_; + const std::string default_group_id_; + MediaPlaylist media_playlist_; + + MediaInfo valid_video_media_info_; +}; + +// Verify that SetMediaInfo() fails if timescale is not present. +TEST_F(MediaPlaylistTest, NoTimeScale) { + MediaInfo media_info; + EXPECT_FALSE(media_playlist_.SetMediaInfo(media_info)); +} + +// The current implementation only handles video and audio. +TEST_F(MediaPlaylistTest, NoAudioOrVideo) { + MediaInfo media_info; + media_info.set_reference_time_scale(90000); + MediaInfo::TextInfo* text_info = media_info.mutable_text_info(); + text_info->set_format("vtt"); + EXPECT_FALSE(media_playlist_.SetMediaInfo(media_info)); +} + +TEST_F(MediaPlaylistTest, SetMediaInfo) { + MediaInfo media_info; + media_info.set_reference_time_scale(90000); + MediaInfo::VideoInfo* video_info = media_info.mutable_video_info(); + video_info->set_width(1280); + video_info->set_height(720); + EXPECT_TRUE(media_playlist_.SetMediaInfo(media_info)); +} + +// 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); +} + +// Verify that AddEncryptionInfo works (not crash). +TEST_F(MediaPlaylistTest, AddEncryptionInfo) { + ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_)); + media_playlist_.AddEncryptionInfo(MediaPlaylist::EncryptionMethod::kSampleAes, + "http://example.com", "0xabcedf", "", ""); +} + +TEST_F(MediaPlaylistTest, WriteToFile) { + ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_)); + const std::string kExpectedOutput = + "#EXTM3U\n" + "#EXT-X-TARGETDURATION:0\n"; + + MockFile file; + EXPECT_CALL(file, + Write(MatchesString(kExpectedOutput), kExpectedOutput.size())) + .WillOnce(ReturnArg<1>()); + EXPECT_TRUE(media_playlist_.WriteToFile(&file)); +} + +// If bitrate (bandwidth) is not set in the MediaInfo, use it. +TEST_F(MediaPlaylistTest, UseBitrateInMediaInfo) { + valid_video_media_info_.set_bandwidth(8191); + ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_)); + EXPECT_EQ(8191u, media_playlist_.Bitrate()); +} + +// If bitrate (bandwidth) is not set in the MediaInfo, then calculate from the +// 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); + + // 200KB per second. + EXPECT_EQ(200000u, 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); + + 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-TARGETDURATION:20\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 = + "#EXTM3U\n" + "#EXT-X-TARGETDURATION:30\n" + "#EXTINF:10.000\n" + "file1.ts\n" + "#EXTINF:30.000\n" + "file2.ts\n"; + + MockFile file; + EXPECT_CALL(file, + Write(MatchesString(kExpectedOutput), kExpectedOutput.size())) + .WillOnce(ReturnArg<1>()); + EXPECT_TRUE(media_playlist_.WriteToFile(&file)); +} + +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 = + "#EXTM3U\n" + "#EXT-X-TARGETDURATION:30\n" + "#EXT-X-KEY:METHOD=SAMPLE-AES," + "URI=\"http://example.com\",IV=0x12345678,KEYFORMATVERSIONS=\"1/2/4\"," + "KEYFORMAT=\"com.widevine\"\n" + "#EXTINF:10.000\n" + "file1.ts\n" + "#EXTINF:30.000\n" + "file2.ts\n"; + + MockFile file; + EXPECT_CALL(file, + Write(MatchesString(kExpectedOutput), kExpectedOutput.size())) + .WillOnce(ReturnArg<1>()); + EXPECT_TRUE(media_playlist_.WriteToFile(&file)); +} + +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 = + "#EXTM3U\n" + "#EXT-X-TARGETDURATION:30\n" + "#EXT-X-KEY:METHOD=SAMPLE-AES," + "URI=\"http://example.com\",KEYFORMAT=\"com.widevine\"\n" + "#EXTINF:10.000\n" + "file1.ts\n" + "#EXTINF:30.000\n" + "file2.ts\n"; + + MockFile file; + EXPECT_CALL(file, + Write(MatchesString(kExpectedOutput), kExpectedOutput.size())) + .WillOnce(ReturnArg<1>()); + EXPECT_TRUE(media_playlist_.WriteToFile(&file)); +} + +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_.RemoveOldestSegment(); + + const std::string kExpectedOutput = + "#EXTM3U\n" + "#EXT-X-TARGETDURATION:30\n" + "#EXTINF:30.000\n" + "file2.ts\n"; + + MockFile file; + EXPECT_CALL(file, + Write(MatchesString(kExpectedOutput), kExpectedOutput.size())) + .WillOnce(ReturnArg<1>()); + EXPECT_TRUE(media_playlist_.WriteToFile(&file)); +} + +} // namespace hls +} // namespace edash_packager diff --git a/packager/hls/hls.gyp b/packager/hls/hls.gyp new file mode 100644 index 0000000000..58a423ca4f --- /dev/null +++ b/packager/hls/hls.gyp @@ -0,0 +1,41 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +{ + 'includes': [ + '../common.gypi', + ], + 'targets': [ + { + 'target_name': 'hls_builder', + 'type': '<(component)', + 'sources': [ + 'base/media_playlist.cc', + 'base/media_playlist.h', + ], + 'dependencies': [ + '../base/base.gyp:base', + '../media/file/file.gyp:file', + '../mpd/mpd.gyp:media_info_proto', + ], + }, + { + 'target_name': 'hls_unittest', + 'type': '<(gtest_target_type)', + 'sources': [ + 'base/media_playlist_unittest.cc', + ], + 'dependencies': [ + '../base/base.gyp:base', + '../media/test/media_test.gyp:run_tests_with_atexit_manager', + '../mpd/mpd.gyp:media_info_proto', + '../testing/gmock.gyp:gmock', + '../testing/gtest.gyp:gtest', + 'hls_builder', + ], + }, + ], +} diff --git a/packager/packager.gyp b/packager/packager.gyp index ce1ffc01b8..e73bbedc73 100644 --- a/packager/packager.gyp +++ b/packager/packager.gyp @@ -34,6 +34,7 @@ 'app/widevine_encryption_flags.h', ], 'dependencies': [ + 'hls/hls.gyp:hls_builder', 'media/event/media_event.gyp:media_event', 'media/file/file.gyp:file', 'media/filters/filters.gyp:filters', @@ -93,6 +94,7 @@ 'target_name': 'packager_builder_tests', 'type': 'none', 'dependencies': [ + 'hls/hls.gyp:hls_unittest', 'media/base/media_base.gyp:media_base_unittest', 'media/event/media_event.gyp:media_event_unittest', 'media/file/file.gyp:file_unittest',