From a6646841b73ce3c4507447db6bf76a3dbb56fe26 Mon Sep 17 00:00:00 2001 From: Rintaro Kuroiwa Date: Fri, 25 Mar 2016 01:40:15 -0700 Subject: [PATCH] HlsNotifier and implementation - HlsNotifier interface, similar to MpdNotifier. - SimpleHlsNotifier class can generate playlists. It can also handle encrypted media with Widevine encryption. Issue #85 Change-Id: I9305ddf42796370d3e5df360633c80997c909365 --- packager/hls/base/hls_notifier.h | 88 +++++ packager/hls/base/master_playlist.h | 13 +- packager/hls/base/simple_hls_notifier.cc | 166 ++++++++ packager/hls/base/simple_hls_notifier.h | 87 +++++ .../hls/base/simple_hls_notifier_unittest.cc | 369 ++++++++++++++++++ packager/hls/hls.gyp | 4 + 6 files changed, 721 insertions(+), 6 deletions(-) create mode 100644 packager/hls/base/hls_notifier.h create mode 100644 packager/hls/base/simple_hls_notifier.cc create mode 100644 packager/hls/base/simple_hls_notifier.h create mode 100644 packager/hls/base/simple_hls_notifier_unittest.cc diff --git a/packager/hls/base/hls_notifier.h b/packager/hls/base/hls_notifier.h new file mode 100644 index 0000000000..3d97b0127f --- /dev/null +++ b/packager/hls/base/hls_notifier.h @@ -0,0 +1,88 @@ +// 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_HLS_NOTIFIER_H_ +#define PACKAGER_HLS_BASE_HLS_NOTIFIER_H_ + +#include +#include + +#include "packager/mpd/base/media_info.pb.h" + +namespace edash_packager { +namespace hls { + +// TODO(rkuroiwa): Consider merging this with MpdNotifier. +class HlsNotifier { + public: + enum class HlsProfile { + kOnDemandProfile, + kLiveProfile, + }; + + explicit HlsNotifier(HlsProfile profile) : profile_(profile) {} + virtual ~HlsNotifier() {} + + /// Intialize the notifier. + /// @return true on sucess, false otherwise. + virtual bool Init() = 0; + + /// @param media_info specifies the stream. + /// @param playlist_name is the name of the playlist that this stream should + /// go. + /// @param stream_name is the name of this stream. + /// @param group_id is the group ID for this stream. + /// @param stream_id is set to a value so that it can be used to call the + /// other methods. If this returns false, the stream_id may be set to + /// an invalid value. + /// @return true on sucess, false otherwise. + virtual bool NotifyNewStream(const MediaInfo& media_info, + const std::string& playlist_name, + const std::string& stream_name, + const std::string& group_id, + uint32_t* stream_id) = 0; + + // |stream_id| is the value set by NotifyNewStream(). + // |segment_name| is the name of the new segment. + // |start_time| is the start time of the segment in terms of timescale passed + // in |media_info|. + // |duration| is also in terms of timescale. + // |size| is the size in bytes. + virtual bool NotifyNewSegment(uint32_t stream_id, + const std::string& segment_name, + uint64_t start_time, + uint64_t duration, + uint64_t size) = 0; + + /// @param stream_id is the value set by NotifyNewStream(). + /// @param key_id is the key ID for the stream. + /// @param system_id is the DRM system ID in e.g. PSSH boxes. For example this + /// can be used to determine the KEYFORMAT attribute for EXT-X-KEY. + /// @param iv is the new initialization vector. + /// @param protection_system_specific_data is the DRM specific data. The + /// interpretation of this data is up to the implementation, possibly + /// using @a system_id to determine how to interpret the data. + virtual bool NotifyEncryptionUpdate( + uint32_t stream_id, + const std::vector& key_id, + const std::vector& system_id, + const std::vector& iv, + const std::vector& protection_system_specific_data) = 0; + + /// Process any current buffered states/resources. + /// @return true on success, false otherwise. + virtual bool Flush() = 0; + + protected: + HlsProfile profile() const { return profile_; } + + private: + HlsProfile profile_; +}; + +} // namespace hls +} // namespace edash_packager +#endif // PACKAGER_HLS_BASE_HLS_NOTIFIER_H_ diff --git a/packager/hls/base/master_playlist.h b/packager/hls/base/master_playlist.h index ba68dcf51f..60135d285c 100644 --- a/packager/hls/base/master_playlist.h +++ b/packager/hls/base/master_playlist.h @@ -18,16 +18,17 @@ namespace hls { class MediaPlaylist; /// Class to generate HLS Master Playlist. +/// Methods are virtual for mocking. class MasterPlaylist { public: /// @param file_name is the file name of the master playlist. explicit MasterPlaylist(const std::string& file_name); - ~MasterPlaylist(); + virtual ~MasterPlaylist(); /// @param media_playlist is a MediaPlaylist that should get added to this /// master playlist. Ownership does not transfer. /// @return true on success, false otherwise. - void AddMediaPlaylist(MediaPlaylist* media_playlist); + virtual void AddMediaPlaylist(MediaPlaylist* media_playlist); /// Write out Master Playlist and all the added MediaPlaylists to /// base_url + . @@ -38,8 +39,8 @@ class MasterPlaylist { /// necessarily the same as base_url. It must be in a form that File /// interface can open. /// @return true on success, false otherwise. - bool WriteAllPlaylists(const std::string& base_url, - const std::string& output_dir); + 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. @@ -49,8 +50,8 @@ class MasterPlaylist { /// necessarily the same as base_url. It must be in a form that File /// interface can open. /// @return true on success, false otherwise. - bool WriteMasterPlaylist(const std::string& base_url, - const std::string& output_dir); + virtual bool WriteMasterPlaylist(const std::string& base_url, + const std::string& output_dir); private: const std::string file_name_; diff --git a/packager/hls/base/simple_hls_notifier.cc b/packager/hls/base/simple_hls_notifier.cc new file mode 100644 index 0000000000..e500e5e345 --- /dev/null +++ b/packager/hls/base/simple_hls_notifier.cc @@ -0,0 +1,166 @@ +// 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/simple_hls_notifier.h" + +#include "packager/base/base64.h" +#include "packager/base/logging.h" +#include "packager/base/strings/string_number_conversions.h" +#include "packager/base/strings/stringprintf.h" +#include "packager/hls/base/media_playlist.h" +#include "packager/media/base/widevine_pssh_data.pb.h" + +namespace edash_packager { +namespace hls { + +namespace { +const uint8_t kSystemIdWidevine[] = {0xed, 0xef, 0x8b, 0xa9, 0x79, 0xd6, + 0x4a, 0xce, 0xa3, 0xc8, 0x27, 0xdc, + 0xd5, 0x1d, 0x21, 0xed}; +bool IsWidevineSystemId(const std::vector& system_id) { + return system_id.size() == arraysize(kSystemIdWidevine) && + std::equal(system_id.begin(), system_id.end(), kSystemIdWidevine); +} +} // namespace + +MediaPlaylistFactory::~MediaPlaylistFactory() {} + +scoped_ptr MediaPlaylistFactory::Create( + const std::string& file_name, + const std::string& name, + const std::string& group_id) { + return scoped_ptr( + new MediaPlaylist(file_name, name, group_id)); +} + +SimpleHlsNotifier::SimpleHlsNotifier(const std::string& prefix, + const std::string& output_dir, + const std::string& master_playlist_name) + : HlsNotifier(HlsNotifier::HlsProfile::kOnDemandProfile), + prefix_(prefix), + output_dir_(output_dir), + media_playlist_factory_(new MediaPlaylistFactory()), + master_playlist_(new MasterPlaylist(master_playlist_name)) {} + +SimpleHlsNotifier::~SimpleHlsNotifier() {} + +bool SimpleHlsNotifier::Init() { + return true; +} + +bool SimpleHlsNotifier::NotifyNewStream(const MediaInfo& media_info, + const std::string& playlist_name, + const std::string& name, + const std::string& group_id, + uint32_t* stream_id) { + DCHECK(stream_id); + if (!media_info.has_media_file_name()) { + LOG(ERROR) << "MediaInfo.media_file_name is required to generate a Media " + "Playlist name."; + return false; + } + + *stream_id = sequence_number_.GetNext(); + + scoped_ptr media_playlist = + media_playlist_factory_->Create(playlist_name, name, group_id); + if (!media_playlist->SetMediaInfo(media_info)) { + LOG(ERROR) << "Failed to set media info for playlist " << playlist_name; + return false; + } + + base::AutoLock auto_lock(lock_); + master_playlist_->AddMediaPlaylist(media_playlist.get()); + media_playlist_map_.insert(std::make_pair(*stream_id, media_playlist.Pass())); + return true; +} + +bool SimpleHlsNotifier::NotifyNewSegment(uint32_t stream_id, + const std::string& segment_name, + uint64_t start_time, + uint64_t duration, + uint64_t size) { + base::AutoLock auto_lock(lock_); + auto result = media_playlist_map_.find(stream_id); + if (result == media_playlist_map_.end()) { + LOG(ERROR) << "Cannot find stream with ID: " << stream_id; + return false; + } + auto& media_playlist = result->second; + media_playlist->AddSegment(prefix_ + segment_name, duration, size); + return true; +} + +// TODO(rkuroiwa): Add static key support. for common system id. +bool SimpleHlsNotifier::NotifyEncryptionUpdate( + uint32_t stream_id, + const std::vector& key_id, + const std::vector& system_id, + const std::vector& iv, + const std::vector& protection_system_specific_data) { + base::AutoLock auto_lock(lock_); + auto result = media_playlist_map_.find(stream_id); + if (result == media_playlist_map_.end()) { + LOG(ERROR) << "Cannot find stream with ID: " << stream_id; + return false; + } + if (!IsWidevineSystemId(system_id)) { + LOG(ERROR) << "Unknown system ID: " + << base::HexEncode(system_id.data(), system_id.size()); + return false; + } + + media::WidevinePsshData pssh_data; + if (!pssh_data.ParseFromArray(protection_system_specific_data.data(), + protection_system_specific_data.size())) { + LOG(ERROR) << "Failed ot parse protection_system_specific_data."; + return false; + } + if (!pssh_data.has_provider() || !pssh_data.has_content_id() || + pssh_data.key_id_size() == 0) { + LOG(ERROR) << "Missing fields to generate URI."; + return false; + } + + std::string content_id_base64; + base::Base64Encode(pssh_data.content_id(), &content_id_base64); + std::string json_format = base::StringPrintf( + "{" + "\"provider\":\"%s\"," + "\"content_id\":\"%s\"," + "\"key_ids\":[", + pssh_data.provider().c_str(), content_id_base64.c_str()); + json_format += "\"" + base::HexEncode(key_id.data(), key_id.size()) + "\","; + for (const std::string& id: pssh_data.key_id()) { + if (key_id.size() == id.size() && + memcmp(key_id.data(), id.data(), id.size()) == 0) { + continue; + } + json_format += "\"" + base::HexEncode(id.data(), id.size()) + "\","; + } + json_format += "]}"; + std::string json_format_base64; + base::Base64Encode(json_format, &json_format_base64); + + auto& media_playlist = result->second; + std::string iv_string; + if (!iv.empty()) { + iv_string = "0x" + base::HexEncode(iv.data(), iv.size()); + } + media_playlist->AddEncryptionInfo( + MediaPlaylist::EncryptionMethod::kSampleAes, + "data:text/plain;base64," + json_format_base64, iv_string, + "com.widevine", ""); + return true; +} + +bool SimpleHlsNotifier::Flush() { + base::AutoLock auto_lock(lock_); + return master_playlist_->WriteAllPlaylists(prefix_, output_dir_); +} + +} // namespace hls +} // namespace edash_packager diff --git a/packager/hls/base/simple_hls_notifier.h b/packager/hls/base/simple_hls_notifier.h new file mode 100644 index 0000000000..44f8c345c9 --- /dev/null +++ b/packager/hls/base/simple_hls_notifier.h @@ -0,0 +1,87 @@ +// 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_SIMPLE_HLS_NOTIFIER_H_ +#define PACKAGER_HLS_BASE_SIMPLE_HLS_NOTIFIER_H_ + +#include "packager/base/atomic_sequence_num.h" +#include "packager/base/macros.h" +#include "packager/base/memory/scoped_ptr.h" +#include "packager/base/synchronization/lock.h" +#include "packager/hls/base/hls_notifier.h" +#include "packager/hls/base/master_playlist.h" + +namespace edash_packager { +namespace hls { + +/// For testing. +/// Creates MediaPlaylist. Mock this and return mock MediaPlaylist. +class MediaPlaylistFactory { + public: + virtual ~MediaPlaylistFactory(); + virtual scoped_ptr Create(const std::string& file_name, + const std::string& name, + const std::string& group_id); +}; + +/// This is thread safe. +class SimpleHlsNotifier : public HlsNotifier { + public: + /// @a prefix is used as hte prefix for all the URIs for Media Playlist. This + /// includes the segment URIs in the Media Playlists. + /// @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(const std::string& prefix, + const std::string& output_dir, + const std::string& master_playlist_name); + ~SimpleHlsNotifier() override; + + /// @name HlsNotifier implemetation overrides. + /// @{ + bool Init() override; + bool NotifyNewStream(const MediaInfo& media_info, + const std::string& playlist_name, + const std::string& stream_name, + const std::string& group_id, + uint32_t* stream_id) override; + bool NotifyNewSegment(uint32_t stream_id, + const std::string& segment_name, + uint64_t start_time, + uint64_t duration, + uint64_t size) override; + bool NotifyEncryptionUpdate( + uint32_t stream_id, + const std::vector& key_id, + const std::vector& system_id, + const std::vector& iv, + const std::vector& protection_system_specific_data) override; + bool Flush() override; + /// }@ + + private: + friend class SimpleHlsNotifierTest; + + const std::string prefix_; + const std::string output_dir_; + + scoped_ptr media_playlist_factory_; + scoped_ptr master_playlist_; + std::map> media_playlist_map_; + + base::AtomicSequenceNumber sequence_number_; + + base::Lock lock_; + + DISALLOW_COPY_AND_ASSIGN(SimpleHlsNotifier); +}; + +} // namespace hls +} // namespace edash_packager + +#endif // PACKAGER_HLS_BASE_SIMPLE_HLS_NOTIFIER_H_ diff --git a/packager/hls/base/simple_hls_notifier_unittest.cc b/packager/hls/base/simple_hls_notifier_unittest.cc new file mode 100644 index 0000000000..ad6d28fe55 --- /dev/null +++ b/packager/hls/base/simple_hls_notifier_unittest.cc @@ -0,0 +1,369 @@ +// 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/base/base64.h" +#include "packager/hls/base/mock_media_playlist.h" +#include "packager/hls/base/simple_hls_notifier.h" +#include "packager/media/base/widevine_pssh_data.pb.h" + +namespace edash_packager { +namespace hls { + +using ::testing::Return; +using ::testing::StrEq; +using ::testing::_; + +namespace { +const char kMasterPlaylistName[] = "master.m3u8"; + +class MockMasterPlaylist : public MasterPlaylist { + public: + 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_METHOD3(CreateMock, + MediaPlaylist*(const std::string& file_name, + const std::string& name, + const std::string& group_id)); + + scoped_ptr Create(const std::string& file_name, + const std::string& name, + const std::string& group_id) override { + return scoped_ptr(CreateMock(file_name, name, group_id)); + } +}; + +const char kTestPrefix[] = "http://testprefix.com/"; +const char kAnyOutputDir[] = "anything/"; + +} // namespace + +class SimpleHlsNotifierTest : public ::testing::Test { + protected: + SimpleHlsNotifierTest() + : notifier_(kTestPrefix, kAnyOutputDir, kMasterPlaylistName) {} + + void InjectMediaPlaylistFactory(scoped_ptr factory) { + notifier_.media_playlist_factory_ = factory.Pass(); + } + + void InjectMasterPlaylist(scoped_ptr playlist) { + notifier_.master_playlist_ = playlist.Pass(); + } + + const std::map>& GetMediaPlaylistMap() { + return notifier_.media_playlist_map_; + } + + SimpleHlsNotifier notifier_; +}; + +TEST_F(SimpleHlsNotifierTest, Init) { + EXPECT_TRUE(notifier_.Init()); +} + +TEST_F(SimpleHlsNotifierTest, NotifyNewStream) { + scoped_ptr mock_master_playlist(new MockMasterPlaylist()); + scoped_ptr factory(new MockMediaPlaylistFactory()); + + // Pointer released by SimpleHlsNotifier. + MockMediaPlaylist* mock_media_playlist = new MockMediaPlaylist("", "", ""); + EXPECT_CALL(*mock_master_playlist, AddMediaPlaylist(mock_media_playlist)); + + EXPECT_CALL(*mock_media_playlist, SetMediaInfo(_)).WillOnce(Return(true)); + EXPECT_CALL(*factory, CreateMock(StrEq("video_playlist.m3u8"), StrEq("name"), + StrEq("groupid"))) + .WillOnce(Return(mock_media_playlist)); + + InjectMasterPlaylist(mock_master_playlist.Pass()); + InjectMediaPlaylistFactory(factory.Pass()); + EXPECT_TRUE(notifier_.Init()); + MediaInfo media_info; + media_info.set_media_file_name("media_file.mp4"); + media_info.mutable_video_info()->set_codec("videocodec"); + uint32_t stream_id; + EXPECT_TRUE(notifier_.NotifyNewStream(media_info, "video_playlist.m3u8", + "name", "groupid", &stream_id)); + EXPECT_EQ(1u, GetMediaPlaylistMap().size()); +} + +TEST_F(SimpleHlsNotifierTest, NotifyNewSegment) { + scoped_ptr mock_master_playlist(new MockMasterPlaylist()); + scoped_ptr factory(new MockMediaPlaylistFactory()); + + // Pointer released by SimpleHlsNotifier. + MockMediaPlaylist* mock_media_playlist = new MockMediaPlaylist("", "", ""); + + EXPECT_CALL( + *mock_master_playlist, + AddMediaPlaylist(static_cast(mock_media_playlist))); + EXPECT_CALL(*mock_media_playlist, SetMediaInfo(_)).WillOnce(Return(true)); + EXPECT_CALL(*factory, CreateMock(_, _, _)) + .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), kDuration, kSize)); + + InjectMasterPlaylist(mock_master_playlist.Pass()); + InjectMediaPlaylistFactory(factory.Pass()); + EXPECT_TRUE(notifier_.Init()); + MediaInfo media_info; + media_info.set_media_file_name("media_file.mp4"); + media_info.mutable_video_info()->set_codec("videocodec"); + 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_F(SimpleHlsNotifierTest, NotifyNewSegmentWithoutStreamsRegistered) { + EXPECT_TRUE(notifier_.Init()); + EXPECT_FALSE(notifier_.NotifyNewSegment(1u, "anything", 0u, 0u, 0u)); +} + +TEST_F(SimpleHlsNotifierTest, NotifyEncryptionUpdate) { + scoped_ptr mock_master_playlist(new MockMasterPlaylist()); + scoped_ptr factory(new MockMediaPlaylistFactory()); + + // Pointer released by SimpleHlsNotifier. + MockMediaPlaylist* mock_media_playlist = new MockMediaPlaylist("", "", ""); + + EXPECT_CALL( + *mock_master_playlist, + AddMediaPlaylist(static_cast(mock_media_playlist))); + EXPECT_CALL(*mock_media_playlist, SetMediaInfo(_)).WillOnce(Return(true)); + EXPECT_CALL(*factory, CreateMock(_, _, _)) + .WillOnce(Return(mock_media_playlist)); + + InjectMasterPlaylist(mock_master_playlist.Pass()); + InjectMediaPlaylistFactory(factory.Pass()); + EXPECT_TRUE(notifier_.Init()); + MediaInfo media_info; + media_info.set_media_file_name("media_file.mp4"); + media_info.mutable_video_info()->set_codec("videocodec"); + uint32_t stream_id; + EXPECT_TRUE(notifier_.NotifyNewStream(media_info, "playlist.m3u8", "name", + "groupid", &stream_id)); + + const uint8_t kSystemIdWidevine[] = {0xed, 0xef, 0x8b, 0xa9, 0x79, 0xd6, + 0x4a, 0xce, 0xa3, 0xc8, 0x27, 0xdc, + 0xd5, 0x1d, 0x21, 0xed}; + std::vector system_id( + kSystemIdWidevine, kSystemIdWidevine + arraysize(kSystemIdWidevine)); + std::vector iv(16, 0x45); + + media::WidevinePsshData widevine_pssh_data; + widevine_pssh_data.set_provider("someprovider"); + widevine_pssh_data.set_content_id("contentid"); + const uint8_t kAnyKeyId[] = { + 0x11, 0x22, 0x33, 0x44, + }; + widevine_pssh_data.add_key_id()->assign(kAnyKeyId, + kAnyKeyId + arraysize(kAnyKeyId)); + std::string widevine_pssh_data_str; + ASSERT_TRUE(widevine_pssh_data.SerializeToString(&widevine_pssh_data_str)); + std::vector pssh_data(widevine_pssh_data_str.begin(), + widevine_pssh_data_str.end()); + + const char kExpectedJson[] = + "{" + "\"provider\":\"someprovider\"," + "\"content_id\":\"Y29udGVudGlk\"," + "\"key_ids\":[\"11223344\",]}"; + std::string expected_json_base64; + base::Base64Encode(kExpectedJson, &expected_json_base64); + + EXPECT_CALL( + *mock_media_playlist, + AddEncryptionInfo(MediaPlaylist::EncryptionMethod::kSampleAes, + StrEq("data:text/plain;base64," + expected_json_base64), + StrEq("0x45454545454545454545454545454545"), + StrEq("com.widevine"), _)); + EXPECT_TRUE(notifier_.NotifyEncryptionUpdate( + stream_id, + std::vector(kAnyKeyId, kAnyKeyId + arraysize(kAnyKeyId)), + system_id, iv, pssh_data)); +} + +// Verify that when there are multiple key IDs in PSSH, the key ID that is +// passed to NotifyEncryptionUpdate() is the first key ID in the json format. +TEST_F(SimpleHlsNotifierTest, MultipleKeyIdsInPssh) { + scoped_ptr mock_master_playlist(new MockMasterPlaylist()); + scoped_ptr factory(new MockMediaPlaylistFactory()); + + // Pointer released by SimpleHlsNotifier. + MockMediaPlaylist* mock_media_playlist = new MockMediaPlaylist("", "", ""); + + EXPECT_CALL( + *mock_master_playlist, + AddMediaPlaylist(static_cast(mock_media_playlist))); + EXPECT_CALL(*mock_media_playlist, SetMediaInfo(_)).WillOnce(Return(true)); + EXPECT_CALL(*factory, CreateMock(_, _, _)) + .WillOnce(Return(mock_media_playlist)); + + InjectMasterPlaylist(mock_master_playlist.Pass()); + InjectMediaPlaylistFactory(factory.Pass()); + EXPECT_TRUE(notifier_.Init()); + MediaInfo media_info; + media_info.set_media_file_name("media_file.mp4"); + media_info.mutable_video_info()->set_codec("videocodec"); + uint32_t stream_id; + EXPECT_TRUE(notifier_.NotifyNewStream(media_info, "playlist.m3u8", "name", + "groupid", &stream_id)); + + const uint8_t kSystemIdWidevine[] = {0xed, 0xef, 0x8b, 0xa9, 0x79, 0xd6, + 0x4a, 0xce, 0xa3, 0xc8, 0x27, 0xdc, + 0xd5, 0x1d, 0x21, 0xed}; + std::vector system_id( + kSystemIdWidevine, kSystemIdWidevine + arraysize(kSystemIdWidevine)); + std::vector iv(16, 0x45); + + media::WidevinePsshData widevine_pssh_data; + widevine_pssh_data.set_provider("someprovider"); + widevine_pssh_data.set_content_id("contentid"); + const uint8_t kFirstKeyId[] = { + 0x11, 0x11, 0x11, 0x11, + }; + const uint8_t kSecondKeyId[] = { + 0x22, 0x22, 0x22, 0x22, + }; + widevine_pssh_data.add_key_id()->assign(kFirstKeyId, + kFirstKeyId + arraysize(kFirstKeyId)); + widevine_pssh_data.add_key_id()->assign( + kSecondKeyId, kSecondKeyId + arraysize(kSecondKeyId)); + std::string widevine_pssh_data_str; + ASSERT_TRUE(widevine_pssh_data.SerializeToString(&widevine_pssh_data_str)); + std::vector pssh_data(widevine_pssh_data_str.begin(), + widevine_pssh_data_str.end()); + + const char kExpectedJson[] = + "{" + "\"provider\":\"someprovider\"," + "\"content_id\":\"Y29udGVudGlk\"," + "\"key_ids\":[\"22222222\",\"11111111\",]}"; + std::string expected_json_base64; + base::Base64Encode(kExpectedJson, &expected_json_base64); + + EXPECT_CALL( + *mock_media_playlist, + AddEncryptionInfo(MediaPlaylist::EncryptionMethod::kSampleAes, + StrEq("data:text/plain;base64," + expected_json_base64), + StrEq("0x45454545454545454545454545454545"), + StrEq("com.widevine"), _)); + EXPECT_TRUE(notifier_.NotifyEncryptionUpdate( + stream_id, + // Use the second key id here so that it will be thre first one in the + // key_ids array in the JSON. + std::vector(kSecondKeyId, + kSecondKeyId + arraysize(kSecondKeyId)), + system_id, iv, pssh_data)); +} + +TEST_F(SimpleHlsNotifierTest, NotifyEncryptionUpdateEmptyIv) { + scoped_ptr mock_master_playlist(new MockMasterPlaylist()); + scoped_ptr factory(new MockMediaPlaylistFactory()); + + // Pointer released by SimpleHlsNotifier. + MockMediaPlaylist* mock_media_playlist = new MockMediaPlaylist("", "", ""); + + EXPECT_CALL( + *mock_master_playlist, + AddMediaPlaylist(static_cast(mock_media_playlist))); + EXPECT_CALL(*mock_media_playlist, SetMediaInfo(_)).WillOnce(Return(true)); + EXPECT_CALL(*factory, CreateMock(_, _, _)) + .WillOnce(Return(mock_media_playlist)); + + InjectMasterPlaylist(mock_master_playlist.Pass()); + InjectMediaPlaylistFactory(factory.Pass()); + EXPECT_TRUE(notifier_.Init()); + MediaInfo media_info; + media_info.set_media_file_name("media_file.mp4"); + media_info.mutable_video_info()->set_codec("videocodec"); + uint32_t stream_id; + EXPECT_TRUE(notifier_.NotifyNewStream(media_info, "playlist.m3u8", "name", + "groupid", &stream_id)); + + const uint8_t kSystemIdWidevine[] = {0xed, 0xef, 0x8b, 0xa9, 0x79, 0xd6, + 0x4a, 0xce, 0xa3, 0xc8, 0x27, 0xdc, + 0xd5, 0x1d, 0x21, 0xed}; + std::vector system_id( + kSystemIdWidevine, kSystemIdWidevine + arraysize(kSystemIdWidevine)); + + media::WidevinePsshData widevine_pssh_data; + widevine_pssh_data.set_provider("someprovider"); + widevine_pssh_data.set_content_id("contentid"); + const uint8_t kAnyKeyId[] = { + 0x11, 0x22, 0x33, 0x44, + }; + widevine_pssh_data.add_key_id()->assign(kAnyKeyId, + kAnyKeyId + arraysize(kAnyKeyId)); + std::string widevine_pssh_data_str; + ASSERT_TRUE(widevine_pssh_data.SerializeToString(&widevine_pssh_data_str)); + std::vector pssh_data(widevine_pssh_data_str.begin(), + widevine_pssh_data_str.end()); + + const char kExpectedJson[] = + "{" + "\"provider\":\"someprovider\"," + "\"content_id\":\"Y29udGVudGlk\"," + "\"key_ids\":[\"11223344\",]}"; + std::string expected_json_base64; + base::Base64Encode(kExpectedJson, &expected_json_base64); + + EXPECT_CALL( + *mock_media_playlist, + AddEncryptionInfo(MediaPlaylist::EncryptionMethod::kSampleAes, + StrEq("data:text/plain;base64," + expected_json_base64), + StrEq(""), StrEq("com.widevine"), _)); + + std::vector empty_iv; + EXPECT_TRUE(notifier_.NotifyEncryptionUpdate( + stream_id, + std::vector(kAnyKeyId, kAnyKeyId + arraysize(kAnyKeyId)), + system_id, empty_iv, pssh_data)); +} + +TEST_F(SimpleHlsNotifierTest, NotifyEncryptionUpdateWithoutStreamsRegistered) { + std::vector system_id; + std::vector iv; + std::vector pssh_data; + std::vector key_id; + EXPECT_TRUE(notifier_.Init()); + EXPECT_FALSE(notifier_.NotifyEncryptionUpdate(1238u, key_id, system_id, iv, + pssh_data)); +} + +TEST_F(SimpleHlsNotifierTest, Flush) { + scoped_ptr mock_master_playlist(new MockMasterPlaylist()); + EXPECT_CALL(*mock_master_playlist, + WriteAllPlaylists(StrEq(kTestPrefix), StrEq(kAnyOutputDir))) + .WillOnce(Return(true)); + scoped_ptr factory(new MockMediaPlaylistFactory()); + + InjectMasterPlaylist(mock_master_playlist.Pass()); + EXPECT_TRUE(notifier_.Init()); + EXPECT_TRUE(notifier_.Flush()); +} + +} // namespace hls +} // namespace edash_packager diff --git a/packager/hls/hls.gyp b/packager/hls/hls.gyp index bcf59dd7c9..06fe9fa14a 100644 --- a/packager/hls/hls.gyp +++ b/packager/hls/hls.gyp @@ -13,10 +13,13 @@ 'target_name': 'hls_builder', 'type': '<(component)', 'sources': [ + 'base/hls_notifier.h', 'base/master_playlist.cc', 'base/master_playlist.h', 'base/media_playlist.cc', 'base/media_playlist.h', + 'base/simple_hls_notifier.cc', + 'base/simple_hls_notifier.h', ], 'dependencies': [ '../base/base.gyp:base', @@ -32,6 +35,7 @@ 'base/media_playlist_unittest.cc', 'base/mock_media_playlist.cc', 'base/mock_media_playlist.h', + 'base/simple_hls_notifier_unittest.cc', ], 'dependencies': [ '../base/base.gyp:base',