Make HLS with key rotation work
- Remove the TS key rotation not supported check as there is nothing to be done for key rotation in TS for SAMPLE-AES. - Fix IV updated problem in new segments even if crypto period does not change. - Avoid duplicate EXT-X-KEY tags if it does not change. - Make EXT-X-DISCONTINUITY-SEQUENCE match with number of removed EXT-X-DISCONTINUITY. - Added end to end test for HLS with key rotation. Change-Id: I73cb82e9f5575fcdf63ee643228efe78e6766302
This commit is contained in:
parent
5bad2fbd5c
commit
e56d1faaf0
|
@ -461,6 +461,34 @@ class PackagerFunctionalTest(PackagerAppTest):
|
||||||
os.path.join(self.tmp_dir, 'video.m3u8'),
|
os.path.join(self.tmp_dir, 'video.m3u8'),
|
||||||
'bear-640x360-v-live-golden.m3u8')
|
'bear-640x360-v-live-golden.m3u8')
|
||||||
|
|
||||||
|
def testPackageAvcTsLivePlaylistWithKeyRotation(self):
|
||||||
|
self.packager.Package(
|
||||||
|
self._GetStreams(
|
||||||
|
['audio', 'video'],
|
||||||
|
output_format='ts',
|
||||||
|
live=True,
|
||||||
|
test_files=['bear-640x360.ts']),
|
||||||
|
self._GetFlags(
|
||||||
|
encryption=True,
|
||||||
|
key_rotation=True,
|
||||||
|
output_hls=True,
|
||||||
|
hls_playlist_type='LIVE',
|
||||||
|
time_shift_buffer_depth=0.5))
|
||||||
|
self._DiffLiveGold(self.output[0],
|
||||||
|
'bear-640x360-a-enc-rotation-golden',
|
||||||
|
output_format='ts')
|
||||||
|
self._DiffLiveGold(self.output[1],
|
||||||
|
'bear-640x360-v-enc-rotation-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-enc-rotation-golden.m3u8')
|
||||||
|
self._DiffGold(
|
||||||
|
os.path.join(self.tmp_dir, 'video.m3u8'),
|
||||||
|
'bear-640x360-v-live-enc-rotation-golden.m3u8')
|
||||||
|
|
||||||
def testPackageAvcTsEventPlaylist(self):
|
def testPackageAvcTsEventPlaylist(self):
|
||||||
self.assertPackageSuccess(
|
self.assertPackageSuccess(
|
||||||
self._GetStreams(
|
self._GetStreams(
|
||||||
|
@ -664,7 +692,7 @@ class PackagerFunctionalTest(PackagerAppTest):
|
||||||
'bear-640x360-v-enc-golden',
|
'bear-640x360-v-enc-golden',
|
||||||
output_format='ts')
|
output_format='ts')
|
||||||
self._DiffGold(self.hls_master_playlist_output,
|
self._DiffGold(self.hls_master_playlist_output,
|
||||||
'bear-640x360-av-enc-master-golden.m3u8')
|
'bear-640x360-av-master-golden.m3u8')
|
||||||
self._DiffGold(
|
self._DiffGold(
|
||||||
os.path.join(self.tmp_dir, 'audio.m3u8'),
|
os.path.join(self.tmp_dir, 'audio.m3u8'),
|
||||||
'bear-640x360-a-enc-golden.m3u8')
|
'bear-640x360-a-enc-golden.m3u8')
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,11 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXT-X-VERSION:6
|
||||||
|
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
|
||||||
|
#EXT-X-TARGETDURATION:2
|
||||||
|
#EXT-X-MEDIA-SEQUENCE:1
|
||||||
|
#EXTINF:0.998,
|
||||||
|
output_audio-2.ts
|
||||||
|
#EXT-X-DISCONTINUITY
|
||||||
|
#EXT-X-KEY:METHOD=SAMPLE-AES,URI="data:text/plain;base64,MzQ1Njc4OTAxMjM0NTYxMg==",IV=0x3334353637383930,KEYFORMAT="identity"
|
||||||
|
#EXTINF:0.789,
|
||||||
|
output_audio-3.ts
|
|
@ -1,5 +0,0 @@
|
||||||
#EXTM3U
|
|
||||||
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
|
|
||||||
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="stream_0",URI="audio.m3u8"
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=1217518,CODECS="avc1.64001e,mp4a.40.2",RESOLUTION=640x360,AUDIO="audio"
|
|
||||||
video.m3u8
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,12 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXT-X-VERSION:6
|
||||||
|
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
|
||||||
|
#EXT-X-TARGETDURATION:2
|
||||||
|
#EXT-X-MEDIA-SEQUENCE:1
|
||||||
|
#EXT-X-DISCONTINUITY-SEQUENCE:1
|
||||||
|
#EXT-X-KEY:METHOD=SAMPLE-AES,URI="data:text/plain;base64,MjM0NTY3ODkwMTIzNDU2MQ==",IV=0x3334353637383930,KEYFORMAT="identity"
|
||||||
|
#EXTINF:1.001,
|
||||||
|
output_video-2.ts
|
||||||
|
#EXT-X-KEY:METHOD=SAMPLE-AES,URI="data:text/plain;base64,MzQ1Njc4OTAxMjM0NTYxMg==",IV=0x3334353637383930,KEYFORMAT="identity"
|
||||||
|
#EXTINF:0.734,
|
||||||
|
output_video-3.ts
|
|
@ -36,7 +36,7 @@ uint32_t GetTimeScale(const MediaInfo& media_info) {
|
||||||
std::string CreatePlaylistHeader(const std::string& init_segment_name,
|
std::string CreatePlaylistHeader(const std::string& init_segment_name,
|
||||||
uint32_t target_duration,
|
uint32_t target_duration,
|
||||||
MediaPlaylist::MediaPlaylistType type,
|
MediaPlaylist::MediaPlaylistType type,
|
||||||
int sequence_number,
|
int media_sequence_number,
|
||||||
int discontinuity_sequence_number) {
|
int discontinuity_sequence_number) {
|
||||||
const std::string version = GetPackagerVersion();
|
const std::string version = GetPackagerVersion();
|
||||||
std::string version_line;
|
std::string version_line;
|
||||||
|
@ -62,9 +62,9 @@ std::string CreatePlaylistHeader(const std::string& init_segment_name,
|
||||||
header += "#EXT-X-PLAYLIST-TYPE:EVENT\n";
|
header += "#EXT-X-PLAYLIST-TYPE:EVENT\n";
|
||||||
break;
|
break;
|
||||||
case MediaPlaylist::MediaPlaylistType::kLive:
|
case MediaPlaylist::MediaPlaylistType::kLive:
|
||||||
if (sequence_number > 0) {
|
if (media_sequence_number > 0) {
|
||||||
base::StringAppendF(&header, "#EXT-X-MEDIA-SEQUENCE:%d\n",
|
base::StringAppendF(&header, "#EXT-X-MEDIA-SEQUENCE:%d\n",
|
||||||
sequence_number);
|
media_sequence_number);
|
||||||
}
|
}
|
||||||
if (discontinuity_sequence_number > 0) {
|
if (discontinuity_sequence_number > 0) {
|
||||||
base::StringAppendF(&header, "#EXT-X-DISCONTINUITY-SEQUENCE:%d\n",
|
base::StringAppendF(&header, "#EXT-X-DISCONTINUITY-SEQUENCE:%d\n",
|
||||||
|
@ -186,6 +186,27 @@ std::string EncryptionInfoEntry::ToString() {
|
||||||
return ext_key + ",KEYFORMAT=\"" + key_format_ + "\"\n";
|
return ext_key + ",KEYFORMAT=\"" + key_format_ + "\"\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DiscontinuityEntry : public HlsEntry {
|
||||||
|
public:
|
||||||
|
DiscontinuityEntry();
|
||||||
|
|
||||||
|
~DiscontinuityEntry() override;
|
||||||
|
|
||||||
|
std::string ToString() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
DISALLOW_COPY_AND_ASSIGN(DiscontinuityEntry);
|
||||||
|
};
|
||||||
|
|
||||||
|
DiscontinuityEntry::DiscontinuityEntry()
|
||||||
|
: HlsEntry(HlsEntry::EntryType::kExtDiscontinuity) {}
|
||||||
|
|
||||||
|
DiscontinuityEntry::~DiscontinuityEntry() {}
|
||||||
|
|
||||||
|
std::string DiscontinuityEntry::ToString() {
|
||||||
|
return "#EXT-X-DISCONTINUITY\n";
|
||||||
|
}
|
||||||
|
|
||||||
double LatestSegmentStartTime(
|
double LatestSegmentStartTime(
|
||||||
const std::list<std::unique_ptr<HlsEntry>>& entries) {
|
const std::list<std::unique_ptr<HlsEntry>>& entries) {
|
||||||
DCHECK(!entries.empty());
|
DCHECK(!entries.empty());
|
||||||
|
@ -276,69 +297,19 @@ void MediaPlaylist::AddSegment(const std::string& file_name,
|
||||||
SlideWindow();
|
SlideWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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<decltype(entries_),
|
|
||||||
std::list<std::unique_ptr<HlsEntry>>>::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,
|
void MediaPlaylist::AddEncryptionInfo(MediaPlaylist::EncryptionMethod method,
|
||||||
const std::string& url,
|
const std::string& url,
|
||||||
const std::string& key_id,
|
const std::string& key_id,
|
||||||
const std::string& iv,
|
const std::string& iv,
|
||||||
const std::string& key_format,
|
const std::string& key_format,
|
||||||
const std::string& key_format_versions) {
|
const std::string& key_format_versions) {
|
||||||
|
if (!inserted_discontinuity_tag_) {
|
||||||
|
// Insert discontinuity tag only for the first EXT-X-KEY, only if there
|
||||||
|
// are non-encrypted media segments.
|
||||||
|
if (!entries_.empty())
|
||||||
|
entries_.emplace_back(new DiscontinuityEntry());
|
||||||
|
inserted_discontinuity_tag_ = true;
|
||||||
|
}
|
||||||
entries_.emplace_back(new EncryptionInfoEntry(
|
entries_.emplace_back(new EncryptionInfoEntry(
|
||||||
method, url, key_id, iv, key_format, key_format_versions));
|
method, url, key_id, iv, key_format, key_format_versions));
|
||||||
}
|
}
|
||||||
|
@ -350,22 +321,11 @@ bool MediaPlaylist::WriteToFile(const std::string& file_path) {
|
||||||
|
|
||||||
std::string header = CreatePlaylistHeader(
|
std::string header = CreatePlaylistHeader(
|
||||||
media_info_.init_segment_name(), target_duration_, type_,
|
media_info_.init_segment_name(), target_duration_, type_,
|
||||||
sequence_number_, discontinuity_sequence_number_);
|
media_sequence_number_, discontinuity_sequence_number_);
|
||||||
|
|
||||||
std::string body;
|
std::string body;
|
||||||
if (!entries_.empty()) {
|
for (const auto& entry : entries_)
|
||||||
const bool first_is_ext_key =
|
|
||||||
entries_.front()->type() == HlsEntry::EntryType::kExtKey;
|
|
||||||
bool inserted_discontinuity_tag = false;
|
|
||||||
for (const auto& entry : entries_) {
|
|
||||||
if (!first_is_ext_key && !inserted_discontinuity_tag &&
|
|
||||||
entry->type() == HlsEntry::EntryType::kExtKey) {
|
|
||||||
body.append("#EXT-X-DISCONTINUITY\n");
|
|
||||||
inserted_discontinuity_tag = true;
|
|
||||||
}
|
|
||||||
body.append(entry->ToString());
|
body.append(entry->ToString());
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string content = header + body;
|
std::string content = header + body;
|
||||||
|
|
||||||
|
@ -475,18 +435,11 @@ void MediaPlaylist::SlideWindow() {
|
||||||
for (; last != entries_.end(); ++last) {
|
for (; last != entries_.end(); ++last) {
|
||||||
HlsEntry::EntryType entry_type = last->get()->type();
|
HlsEntry::EntryType entry_type = last->get()->type();
|
||||||
if (entry_type == HlsEntry::EntryType::kExtKey) {
|
if (entry_type == HlsEntry::EntryType::kExtKey) {
|
||||||
if (prev_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.clear();
|
||||||
}
|
|
||||||
}
|
|
||||||
ext_x_keys.push_back(std::move(*last));
|
ext_x_keys.push_back(std::move(*last));
|
||||||
|
} else if (entry_type == HlsEntry::EntryType::kExtDiscontinuity) {
|
||||||
|
++discontinuity_sequence_number_;
|
||||||
} else {
|
} else {
|
||||||
DCHECK_EQ(entry_type, HlsEntry::EntryType::kExtInf);
|
DCHECK_EQ(entry_type, HlsEntry::EntryType::kExtInf);
|
||||||
const SegmentInfoEntry* segment_info =
|
const SegmentInfoEntry* segment_info =
|
||||||
|
@ -503,7 +456,7 @@ void MediaPlaylist::SlideWindow() {
|
||||||
// Add key entries back.
|
// Add key entries back.
|
||||||
entries_.insert(entries_.begin(), std::make_move_iterator(ext_x_keys.begin()),
|
entries_.insert(entries_.begin(), std::make_move_iterator(ext_x_keys.begin()),
|
||||||
std::make_move_iterator(ext_x_keys.end()));
|
std::make_move_iterator(ext_x_keys.end()));
|
||||||
sequence_number_ += num_segments_removed;
|
media_sequence_number_ += num_segments_removed;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace hls
|
} // namespace hls
|
||||||
|
|
|
@ -27,6 +27,7 @@ class HlsEntry {
|
||||||
enum class EntryType {
|
enum class EntryType {
|
||||||
kExtInf,
|
kExtInf,
|
||||||
kExtKey,
|
kExtKey,
|
||||||
|
kExtDiscontinuity,
|
||||||
};
|
};
|
||||||
virtual ~HlsEntry();
|
virtual ~HlsEntry();
|
||||||
|
|
||||||
|
@ -105,10 +106,6 @@ class MediaPlaylist {
|
||||||
uint64_t duration,
|
uint64_t duration,
|
||||||
uint64_t size);
|
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
|
/// All segments added after calling this method must be decryptable with
|
||||||
/// the key that can be fetched from |url|, until calling this again.
|
/// the key that can be fetched from |url|, until calling this again.
|
||||||
/// @param method is the encryption method.
|
/// @param method is the encryption method.
|
||||||
|
@ -180,7 +177,8 @@ class MediaPlaylist {
|
||||||
MediaPlaylistStreamType stream_type_ =
|
MediaPlaylistStreamType stream_type_ =
|
||||||
MediaPlaylistStreamType::kPlaylistUnknown;
|
MediaPlaylistStreamType::kPlaylistUnknown;
|
||||||
std::string codec_;
|
std::string codec_;
|
||||||
int sequence_number_ = 0;
|
int media_sequence_number_ = 0;
|
||||||
|
bool inserted_discontinuity_tag_ = false;
|
||||||
int discontinuity_sequence_number_ = 0;
|
int discontinuity_sequence_number_ = 0;
|
||||||
|
|
||||||
double longest_segment_duration_ = 0.0;
|
double longest_segment_duration_ = 0.0;
|
||||||
|
|
|
@ -274,31 +274,6 @@ TEST_F(MediaPlaylistTest, WriteToFileWithClearLead) {
|
||||||
ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput);
|
ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
TEST_F(MediaPlaylistTest, RemoveOldestSegment) {
|
|
||||||
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, 30 * kTimeScale,
|
|
||||||
5 * kMBytes);
|
|
||||||
media_playlist_.RemoveOldestSegment();
|
|
||||||
|
|
||||||
const char kExpectedOutput[] =
|
|
||||||
"#EXTM3U\n"
|
|
||||||
"#EXT-X-VERSION:6\n"
|
|
||||||
"## Generated with https://github.com/google/shaka-packager version "
|
|
||||||
"test\n"
|
|
||||||
"#EXT-X-TARGETDURATION:30\n"
|
|
||||||
"#EXT-X-PLAYLIST-TYPE:VOD\n"
|
|
||||||
"#EXTINF:30.000,\n"
|
|
||||||
"file2.ts\n"
|
|
||||||
"#EXT-X-ENDLIST\n";
|
|
||||||
|
|
||||||
const char kMemoryFilePath[] = "memory://media.m3u8";
|
|
||||||
EXPECT_TRUE(media_playlist_.WriteToFile(kMemoryFilePath));
|
|
||||||
ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(MediaPlaylistTest, GetLanguage) {
|
TEST_F(MediaPlaylistTest, GetLanguage) {
|
||||||
MediaInfo media_info;
|
MediaInfo media_info;
|
||||||
media_info.set_reference_time_scale(kTimeScale);
|
media_info.set_reference_time_scale(kTimeScale);
|
||||||
|
@ -510,6 +485,8 @@ TEST_F(LiveMediaPlaylistTest, TimeShiftedWithEncryptionInfo) {
|
||||||
TEST_F(LiveMediaPlaylistTest, TimeShiftedWithEncryptionInfoShifted) {
|
TEST_F(LiveMediaPlaylistTest, TimeShiftedWithEncryptionInfoShifted) {
|
||||||
ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_));
|
ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_));
|
||||||
|
|
||||||
|
media_playlist_.AddSegment("file1.ts", 0, 10 * kTimeScale, kMBytes);
|
||||||
|
|
||||||
media_playlist_.AddEncryptionInfo(MediaPlaylist::EncryptionMethod::kSampleAes,
|
media_playlist_.AddEncryptionInfo(MediaPlaylist::EncryptionMethod::kSampleAes,
|
||||||
"http://example.com", "", "0x12345678",
|
"http://example.com", "", "0x12345678",
|
||||||
"com.widevine", "1/2/4");
|
"com.widevine", "1/2/4");
|
||||||
|
@ -517,7 +494,8 @@ TEST_F(LiveMediaPlaylistTest, TimeShiftedWithEncryptionInfoShifted) {
|
||||||
MediaPlaylist::EncryptionMethod::kSampleAes, "http://mydomain.com",
|
MediaPlaylist::EncryptionMethod::kSampleAes, "http://mydomain.com",
|
||||||
"0xfedc", "0x12345678", "com.widevine.someother", "1");
|
"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_.AddEncryptionInfo(MediaPlaylist::EncryptionMethod::kSampleAes,
|
media_playlist_.AddEncryptionInfo(MediaPlaylist::EncryptionMethod::kSampleAes,
|
||||||
"http://example.com", "", "0x22345678",
|
"http://example.com", "", "0x22345678",
|
||||||
|
@ -526,7 +504,7 @@ TEST_F(LiveMediaPlaylistTest, TimeShiftedWithEncryptionInfoShifted) {
|
||||||
MediaPlaylist::EncryptionMethod::kSampleAes, "http://mydomain.com",
|
MediaPlaylist::EncryptionMethod::kSampleAes, "http://mydomain.com",
|
||||||
"0xfedd", "0x22345678", "com.widevine.someother", "1");
|
"0xfedd", "0x22345678", "com.widevine.someother", "1");
|
||||||
|
|
||||||
media_playlist_.AddSegment("file2.ts", 10 * kTimeScale, 20 * kTimeScale,
|
media_playlist_.AddSegment("file3.ts", 30 * kTimeScale, 20 * kTimeScale,
|
||||||
2 * kMBytes);
|
2 * kMBytes);
|
||||||
|
|
||||||
media_playlist_.AddEncryptionInfo(MediaPlaylist::EncryptionMethod::kSampleAes,
|
media_playlist_.AddEncryptionInfo(MediaPlaylist::EncryptionMethod::kSampleAes,
|
||||||
|
@ -536,7 +514,7 @@ TEST_F(LiveMediaPlaylistTest, TimeShiftedWithEncryptionInfoShifted) {
|
||||||
MediaPlaylist::EncryptionMethod::kSampleAes, "http://mydomain.com",
|
MediaPlaylist::EncryptionMethod::kSampleAes, "http://mydomain.com",
|
||||||
"0xfede", "0x32345678", "com.widevine.someother", "1");
|
"0xfede", "0x32345678", "com.widevine.someother", "1");
|
||||||
|
|
||||||
media_playlist_.AddSegment("file3.ts", 30 * kTimeScale, 20 * kTimeScale,
|
media_playlist_.AddSegment("file4.ts", 50 * kTimeScale, 20 * kTimeScale,
|
||||||
2 * kMBytes);
|
2 * kMBytes);
|
||||||
const char kExpectedOutput[] =
|
const char kExpectedOutput[] =
|
||||||
"#EXTM3U\n"
|
"#EXTM3U\n"
|
||||||
|
@ -544,7 +522,7 @@ TEST_F(LiveMediaPlaylistTest, TimeShiftedWithEncryptionInfoShifted) {
|
||||||
"## Generated with https://github.com/google/shaka-packager version "
|
"## Generated with https://github.com/google/shaka-packager version "
|
||||||
"test\n"
|
"test\n"
|
||||||
"#EXT-X-TARGETDURATION:20\n"
|
"#EXT-X-TARGETDURATION:20\n"
|
||||||
"#EXT-X-MEDIA-SEQUENCE:1\n"
|
"#EXT-X-MEDIA-SEQUENCE:2\n"
|
||||||
"#EXT-X-DISCONTINUITY-SEQUENCE:1\n"
|
"#EXT-X-DISCONTINUITY-SEQUENCE:1\n"
|
||||||
"#EXT-X-KEY:METHOD=SAMPLE-AES,"
|
"#EXT-X-KEY:METHOD=SAMPLE-AES,"
|
||||||
"URI=\"http://example.com\",IV=0x22345678,KEYFORMATVERSIONS=\"1/2/4\","
|
"URI=\"http://example.com\",IV=0x22345678,KEYFORMATVERSIONS=\"1/2/4\","
|
||||||
|
@ -554,7 +532,7 @@ TEST_F(LiveMediaPlaylistTest, TimeShiftedWithEncryptionInfoShifted) {
|
||||||
"KEYFORMATVERSIONS=\"1\","
|
"KEYFORMATVERSIONS=\"1\","
|
||||||
"KEYFORMAT=\"com.widevine.someother\"\n"
|
"KEYFORMAT=\"com.widevine.someother\"\n"
|
||||||
"#EXTINF:20.000,\n"
|
"#EXTINF:20.000,\n"
|
||||||
"file2.ts\n"
|
"file3.ts\n"
|
||||||
"#EXT-X-KEY:METHOD=SAMPLE-AES,"
|
"#EXT-X-KEY:METHOD=SAMPLE-AES,"
|
||||||
"URI=\"http://example.com\",IV=0x32345678,KEYFORMATVERSIONS=\"1/2/4\","
|
"URI=\"http://example.com\",IV=0x32345678,KEYFORMATVERSIONS=\"1/2/4\","
|
||||||
"KEYFORMAT=\"com.widevine\"\n"
|
"KEYFORMAT=\"com.widevine\"\n"
|
||||||
|
@ -563,7 +541,7 @@ TEST_F(LiveMediaPlaylistTest, TimeShiftedWithEncryptionInfoShifted) {
|
||||||
"KEYFORMATVERSIONS=\"1\","
|
"KEYFORMATVERSIONS=\"1\","
|
||||||
"KEYFORMAT=\"com.widevine.someother\"\n"
|
"KEYFORMAT=\"com.widevine.someother\"\n"
|
||||||
"#EXTINF:20.000,\n"
|
"#EXTINF:20.000,\n"
|
||||||
"file3.ts\n";
|
"file4.ts\n";
|
||||||
|
|
||||||
const char kMemoryFilePath[] = "memory://media.m3u8";
|
const char kMemoryFilePath[] = "memory://media.m3u8";
|
||||||
EXPECT_TRUE(media_playlist_.WriteToFile(kMemoryFilePath));
|
EXPECT_TRUE(media_playlist_.WriteToFile(kMemoryFilePath));
|
||||||
|
|
|
@ -46,20 +46,23 @@ Status Muxer::Process(std::unique_ptr<StreamData> stream_data) {
|
||||||
kInitialEncryptionInfo, encryption_config.protection_scheme,
|
kInitialEncryptionInfo, encryption_config.protection_scheme,
|
||||||
encryption_config.key_id, encryption_config.constant_iv,
|
encryption_config.key_id, encryption_config.constant_iv,
|
||||||
encryption_config.key_system_info);
|
encryption_config.key_system_info);
|
||||||
|
current_key_id_ = encryption_config.key_id;
|
||||||
}
|
}
|
||||||
return InitializeMuxer();
|
return InitializeMuxer();
|
||||||
case StreamDataType::kSegmentInfo: {
|
case StreamDataType::kSegmentInfo: {
|
||||||
auto& segment_info = stream_data->segment_info;
|
auto& segment_info = stream_data->segment_info;
|
||||||
if (muxer_listener_) {
|
if (muxer_listener_ && segment_info->is_encrypted) {
|
||||||
const EncryptionConfig* encryption_config =
|
const EncryptionConfig* encryption_config =
|
||||||
segment_info->key_rotation_encryption_config.get();
|
segment_info->key_rotation_encryption_config.get();
|
||||||
if (encryption_config) {
|
// Only call OnEncryptionInfoReady again when key updates.
|
||||||
|
if (encryption_config && encryption_config->key_id != current_key_id_) {
|
||||||
muxer_listener_->OnEncryptionInfoReady(
|
muxer_listener_->OnEncryptionInfoReady(
|
||||||
!kInitialEncryptionInfo, encryption_config->protection_scheme,
|
!kInitialEncryptionInfo, encryption_config->protection_scheme,
|
||||||
encryption_config->key_id, encryption_config->constant_iv,
|
encryption_config->key_id, encryption_config->constant_iv,
|
||||||
encryption_config->key_system_info);
|
encryption_config->key_system_info);
|
||||||
|
current_key_id_ = encryption_config->key_id;
|
||||||
}
|
}
|
||||||
if (segment_info->is_encrypted && !encryption_started_) {
|
if (!encryption_started_) {
|
||||||
encryption_started_ = true;
|
encryption_started_ = true;
|
||||||
muxer_listener_->OnEncryptionStart();
|
muxer_listener_->OnEncryptionStart();
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,6 +88,7 @@ class Muxer : public MediaHandler {
|
||||||
|
|
||||||
MuxerOptions options_;
|
MuxerOptions options_;
|
||||||
std::vector<std::shared_ptr<StreamInfo>> streams_;
|
std::vector<std::shared_ptr<StreamInfo>> streams_;
|
||||||
|
std::vector<uint8_t> current_key_id_;
|
||||||
bool encryption_started_ = false;
|
bool encryption_started_ = false;
|
||||||
bool cancelled_;
|
bool cancelled_;
|
||||||
|
|
||||||
|
|
|
@ -222,6 +222,7 @@ Status EncryptionHandler::ProcessMediaSample(MediaSample* sample) {
|
||||||
return status;
|
return status;
|
||||||
if (!CreateEncryptor(encryption_key))
|
if (!CreateEncryptor(encryption_key))
|
||||||
return Status(error::ENCRYPTION_FAILURE, "Failed to create encryptor");
|
return Status(error::ENCRYPTION_FAILURE, "Failed to create encryptor");
|
||||||
|
prev_crypto_period_index_ = current_crypto_period_index;
|
||||||
}
|
}
|
||||||
check_new_crypto_period_ = false;
|
check_new_crypto_period_ = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -304,7 +304,8 @@ class EncryptionHandlerEncryptionTest
|
||||||
} else {
|
} else {
|
||||||
EXPECT_CALL(*mock_header_parser, GetHeaderSize(_))
|
EXPECT_CALL(*mock_header_parser, GetHeaderSize(_))
|
||||||
.WillOnce(Return(kSliceHeaderSize1))
|
.WillOnce(Return(kSliceHeaderSize1))
|
||||||
.WillOnce(Return(kSliceHeaderSize2));
|
.WillOnce(Return(kSliceHeaderSize2))
|
||||||
|
.WillRepeatedly(Return(kSliceHeaderSize2));
|
||||||
}
|
}
|
||||||
InjectVideoSliceHeaderParserForTesting(std::move(mock_header_parser));
|
InjectVideoSliceHeaderParserForTesting(std::move(mock_header_parser));
|
||||||
break;
|
break;
|
||||||
|
@ -519,7 +520,9 @@ TEST_P(EncryptionHandlerEncryptionTest, ClearLeadWithNoKeyRotation) {
|
||||||
|
|
||||||
TEST_P(EncryptionHandlerEncryptionTest, ClearLeadWithKeyRotation) {
|
TEST_P(EncryptionHandlerEncryptionTest, ClearLeadWithKeyRotation) {
|
||||||
const double kClearLeadInSeconds = 1.5 * kSegmentDuration / kTimeScale;
|
const double kClearLeadInSeconds = 1.5 * kSegmentDuration / kTimeScale;
|
||||||
const double kCryptoPeriodDurationInSeconds = kSegmentDuration / kTimeScale;
|
const int kSegmentsPerCryptoPeriod = 2; // 2 segments.
|
||||||
|
const double kCryptoPeriodDurationInSeconds =
|
||||||
|
kSegmentsPerCryptoPeriod * kSegmentDuration / kTimeScale;
|
||||||
EncryptionOptions encryption_options;
|
EncryptionOptions encryption_options;
|
||||||
encryption_options.protection_scheme = protection_scheme_;
|
encryption_options.protection_scheme = protection_scheme_;
|
||||||
encryption_options.clear_lead_in_seconds = kClearLeadInSeconds;
|
encryption_options.clear_lead_in_seconds = kClearLeadInSeconds;
|
||||||
|
@ -546,19 +549,20 @@ TEST_P(EncryptionHandlerEncryptionTest, ClearLeadWithKeyRotation) {
|
||||||
|
|
||||||
InjectCodecParser();
|
InjectCodecParser();
|
||||||
|
|
||||||
// There are three segments. Only the third segment is encrypted.
|
// There are five segments with the first two not encrypted.
|
||||||
// Crypto period duration is the same as segment duration, so there are three
|
for (int i = 0; i < 5; ++i) {
|
||||||
// crypto periods, although only the last is encrypted.
|
if ((i % kSegmentsPerCryptoPeriod) == 0) {
|
||||||
for (int i = 0; i < 3; ++i) {
|
EXPECT_CALL(mock_key_source_,
|
||||||
EXPECT_CALL(mock_key_source_, GetCryptoPeriodKey(i, _, _))
|
GetCryptoPeriodKey(i / kSegmentsPerCryptoPeriod, _, _))
|
||||||
.WillOnce(DoAll(SetArgPointee<2>(GetMockEncryptionKey()),
|
.WillOnce(DoAll(SetArgPointee<2>(GetMockEncryptionKey()),
|
||||||
Return(Status::OK)));
|
Return(Status::OK)));
|
||||||
|
}
|
||||||
// Use single-frame segment for testing.
|
// Use single-frame segment for testing.
|
||||||
ASSERT_OK(Process(GetMediaSampleStreamData(
|
ASSERT_OK(Process(GetMediaSampleStreamData(
|
||||||
kStreamIndex, i * kSegmentDuration, kSegmentDuration)));
|
kStreamIndex, i * kSegmentDuration, kSegmentDuration)));
|
||||||
ASSERT_OK(Process(GetSegmentInfoStreamData(
|
ASSERT_OK(Process(GetSegmentInfoStreamData(
|
||||||
kStreamIndex, i * kSegmentDuration, kSegmentDuration, !kIsSubsegment)));
|
kStreamIndex, i * kSegmentDuration, kSegmentDuration, !kIsSubsegment)));
|
||||||
const bool is_encrypted = i == 2;
|
const bool is_encrypted = i >= 2;
|
||||||
const auto& output_stream_data = GetOutputStreamDataVector();
|
const auto& output_stream_data = GetOutputStreamDataVector();
|
||||||
EXPECT_THAT(output_stream_data,
|
EXPECT_THAT(output_stream_data,
|
||||||
ElementsAre(IsMediaSample(kStreamIndex, i * kSegmentDuration,
|
ElementsAre(IsMediaSample(kStreamIndex, i * kSegmentDuration,
|
||||||
|
|
|
@ -33,12 +33,9 @@ HlsNotifyMuxerListener::~HlsNotifyMuxerListener() {}
|
||||||
// These methods work together to notify that the media is encrypted.
|
// These methods work together to notify that the media is encrypted.
|
||||||
// If OnEncryptionInfoReady() is called before the media has been started, then
|
// If OnEncryptionInfoReady() is called before the media has been started, then
|
||||||
// the information is stored and handled when OnEncryptionStart() is called.
|
// the information is stored and handled when OnEncryptionStart() is called.
|
||||||
// if OnEncryptionStart() is called before the media has been started then
|
// If OnEncryptionStart() is called before the media has been started then
|
||||||
// OnMediaStart() is responsible for notifying that the segments are encrypted
|
// OnMediaStart() is responsible for notifying that the segments are encrypted
|
||||||
// right away i.e. call OnEncryptionStart().
|
// right away i.e. call OnEncryptionStart().
|
||||||
// For now (because Live HLS is not implemented yet) this should be called once,
|
|
||||||
// before media is started. So the logic after the first if statement should not
|
|
||||||
// be taken.
|
|
||||||
void HlsNotifyMuxerListener::OnEncryptionInfoReady(
|
void HlsNotifyMuxerListener::OnEncryptionInfoReady(
|
||||||
bool is_initial_encryption_info,
|
bool is_initial_encryption_info,
|
||||||
FourCC protection_scheme,
|
FourCC protection_scheme,
|
||||||
|
|
|
@ -38,15 +38,14 @@ class MuxerListener {
|
||||||
|
|
||||||
virtual ~MuxerListener() {};
|
virtual ~MuxerListener() {};
|
||||||
|
|
||||||
/// Called when the media's encryption information is ready. This should be
|
/// Called when the media's encryption information is ready.
|
||||||
/// called before OnMediaStart(), if the media is encrypted.
|
/// OnEncryptionInfoReady with @a initial_encryption_info being true should be
|
||||||
/// All the parameters may be empty just to notify that the media is
|
/// called before OnMediaStart(), if the media is encrypted. All the
|
||||||
/// encrypted.
|
/// parameters may be empty just to notify that the media is encrypted. For
|
||||||
/// For ISO BMFF (MP4) media:
|
/// ISO BMFF (MP4) media: If @a is_initial_encryption_info is true then @a
|
||||||
/// If @a is_initial_encryption_info is true then @a key_id is the default_KID
|
/// key_id is the default_KID in 'tenc' box. If @a is_initial_encryption_info
|
||||||
/// in 'tenc' box.
|
/// is false then @a key_id is the new key ID for the for the next crypto
|
||||||
/// If @a is_initial_encryption_info is false then @a key_id is the new key ID
|
/// period.
|
||||||
/// for the for the next crypto period.
|
|
||||||
/// @param is_initial_encryption_info is true if this is the first encryption
|
/// @param is_initial_encryption_info is true if this is the first encryption
|
||||||
/// info for the media. In general, this flag should always be true for
|
/// info for the media. In general, this flag should always be true for
|
||||||
/// non-key-rotated media and should be called only once.
|
/// non-key-rotated media and should be called only once.
|
||||||
|
|
|
@ -41,11 +41,6 @@ Status TsMuxer::AddSample(size_t stream_id,
|
||||||
Status TsMuxer::FinalizeSegment(size_t stream_id,
|
Status TsMuxer::FinalizeSegment(size_t stream_id,
|
||||||
std::shared_ptr<SegmentInfo> segment_info) {
|
std::shared_ptr<SegmentInfo> segment_info) {
|
||||||
DCHECK_EQ(stream_id, 0u);
|
DCHECK_EQ(stream_id, 0u);
|
||||||
if (segment_info->key_rotation_encryption_config) {
|
|
||||||
NOTIMPLEMENTED() << "Key rotation is not implemented for TS.";
|
|
||||||
return Status(error::UNIMPLEMENTED,
|
|
||||||
"Key rotation is not implemented for TS");
|
|
||||||
}
|
|
||||||
return segment_info->is_subsegment
|
return segment_info->is_subsegment
|
||||||
? Status::OK
|
? Status::OK
|
||||||
: segmenter_->FinalizeSegment(segment_info->start_timestamp,
|
: segmenter_->FinalizeSegment(segment_info->start_timestamp,
|
||||||
|
|
Loading…
Reference in New Issue