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:
KongQun Yang 2017-06-20 16:30:03 -07:00
parent 5bad2fbd5c
commit e56d1faaf0
20 changed files with 134 additions and 159 deletions

View File

@ -461,6 +461,34 @@ class PackagerFunctionalTest(PackagerAppTest):
os.path.join(self.tmp_dir, 'video.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):
self.assertPackageSuccess(
self._GetStreams(
@ -664,7 +692,7 @@ class PackagerFunctionalTest(PackagerAppTest):
'bear-640x360-v-enc-golden',
output_format='ts')
self._DiffGold(self.hls_master_playlist_output,
'bear-640x360-av-enc-master-golden.m3u8')
'bear-640x360-av-master-golden.m3u8')
self._DiffGold(
os.path.join(self.tmp_dir, 'audio.m3u8'),
'bear-640x360-a-enc-golden.m3u8')

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,11 @@
#EXTM3U
#EXT-X-VERSION:6
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
#EXT-X-TARGETDURATION:2
#EXT-X-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

View File

@ -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.

View File

@ -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

View File

@ -36,7 +36,7 @@ uint32_t GetTimeScale(const MediaInfo& media_info) {
std::string CreatePlaylistHeader(const std::string& init_segment_name,
uint32_t target_duration,
MediaPlaylist::MediaPlaylistType type,
int sequence_number,
int media_sequence_number,
int discontinuity_sequence_number) {
const std::string version = GetPackagerVersion();
std::string version_line;
@ -62,9 +62,9 @@ std::string CreatePlaylistHeader(const std::string& init_segment_name,
header += "#EXT-X-PLAYLIST-TYPE:EVENT\n";
break;
case MediaPlaylist::MediaPlaylistType::kLive:
if (sequence_number > 0) {
if (media_sequence_number > 0) {
base::StringAppendF(&header, "#EXT-X-MEDIA-SEQUENCE:%d\n",
sequence_number);
media_sequence_number);
}
if (discontinuity_sequence_number > 0) {
base::StringAppendF(&header, "#EXT-X-DISCONTINUITY-SEQUENCE:%d\n",
@ -186,6 +186,27 @@ std::string EncryptionInfoEntry::ToString() {
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(
const std::list<std::unique_ptr<HlsEntry>>& entries) {
DCHECK(!entries.empty());
@ -276,69 +297,19 @@ void MediaPlaylist::AddSegment(const std::string& file_name,
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,
const std::string& url,
const std::string& key_id,
const std::string& iv,
const std::string& key_format,
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(
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(
media_info_.init_segment_name(), target_duration_, type_,
sequence_number_, discontinuity_sequence_number_);
media_sequence_number_, discontinuity_sequence_number_);
std::string body;
if (!entries_.empty()) {
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());
}
}
for (const auto& entry : entries_)
body.append(entry->ToString());
std::string content = header + body;
@ -475,18 +435,11 @@ void MediaPlaylist::SlideWindow() {
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();
}
}
if (prev_entry_type != HlsEntry::EntryType::kExtKey)
ext_x_keys.clear();
ext_x_keys.push_back(std::move(*last));
} else if (entry_type == HlsEntry::EntryType::kExtDiscontinuity) {
++discontinuity_sequence_number_;
} else {
DCHECK_EQ(entry_type, HlsEntry::EntryType::kExtInf);
const SegmentInfoEntry* segment_info =
@ -503,7 +456,7 @@ void MediaPlaylist::SlideWindow() {
// 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;
media_sequence_number_ += num_segments_removed;
}
} // namespace hls

View File

@ -27,6 +27,7 @@ class HlsEntry {
enum class EntryType {
kExtInf,
kExtKey,
kExtDiscontinuity,
};
virtual ~HlsEntry();
@ -105,10 +106,6 @@ class MediaPlaylist {
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.
@ -180,7 +177,8 @@ class MediaPlaylist {
MediaPlaylistStreamType stream_type_ =
MediaPlaylistStreamType::kPlaylistUnknown;
std::string codec_;
int sequence_number_ = 0;
int media_sequence_number_ = 0;
bool inserted_discontinuity_tag_ = false;
int discontinuity_sequence_number_ = 0;
double longest_segment_duration_ = 0.0;

View File

@ -274,31 +274,6 @@ TEST_F(MediaPlaylistTest, WriteToFileWithClearLead) {
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) {
MediaInfo media_info;
media_info.set_reference_time_scale(kTimeScale);
@ -510,6 +485,8 @@ TEST_F(LiveMediaPlaylistTest, TimeShiftedWithEncryptionInfo) {
TEST_F(LiveMediaPlaylistTest, TimeShiftedWithEncryptionInfoShifted) {
ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_));
media_playlist_.AddSegment("file1.ts", 0, 10 * kTimeScale, kMBytes);
media_playlist_.AddEncryptionInfo(MediaPlaylist::EncryptionMethod::kSampleAes,
"http://example.com", "", "0x12345678",
"com.widevine", "1/2/4");
@ -517,7 +494,8 @@ TEST_F(LiveMediaPlaylistTest, TimeShiftedWithEncryptionInfoShifted) {
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_.AddEncryptionInfo(MediaPlaylist::EncryptionMethod::kSampleAes,
"http://example.com", "", "0x22345678",
@ -526,7 +504,7 @@ TEST_F(LiveMediaPlaylistTest, TimeShiftedWithEncryptionInfoShifted) {
MediaPlaylist::EncryptionMethod::kSampleAes, "http://mydomain.com",
"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);
media_playlist_.AddEncryptionInfo(MediaPlaylist::EncryptionMethod::kSampleAes,
@ -536,7 +514,7 @@ TEST_F(LiveMediaPlaylistTest, TimeShiftedWithEncryptionInfoShifted) {
MediaPlaylist::EncryptionMethod::kSampleAes, "http://mydomain.com",
"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);
const char kExpectedOutput[] =
"#EXTM3U\n"
@ -544,7 +522,7 @@ TEST_F(LiveMediaPlaylistTest, TimeShiftedWithEncryptionInfoShifted) {
"## Generated with https://github.com/google/shaka-packager version "
"test\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-KEY:METHOD=SAMPLE-AES,"
"URI=\"http://example.com\",IV=0x22345678,KEYFORMATVERSIONS=\"1/2/4\","
@ -554,7 +532,7 @@ TEST_F(LiveMediaPlaylistTest, TimeShiftedWithEncryptionInfoShifted) {
"KEYFORMATVERSIONS=\"1\","
"KEYFORMAT=\"com.widevine.someother\"\n"
"#EXTINF:20.000,\n"
"file2.ts\n"
"file3.ts\n"
"#EXT-X-KEY:METHOD=SAMPLE-AES,"
"URI=\"http://example.com\",IV=0x32345678,KEYFORMATVERSIONS=\"1/2/4\","
"KEYFORMAT=\"com.widevine\"\n"
@ -563,7 +541,7 @@ TEST_F(LiveMediaPlaylistTest, TimeShiftedWithEncryptionInfoShifted) {
"KEYFORMATVERSIONS=\"1\","
"KEYFORMAT=\"com.widevine.someother\"\n"
"#EXTINF:20.000,\n"
"file3.ts\n";
"file4.ts\n";
const char kMemoryFilePath[] = "memory://media.m3u8";
EXPECT_TRUE(media_playlist_.WriteToFile(kMemoryFilePath));

View File

@ -46,20 +46,23 @@ Status Muxer::Process(std::unique_ptr<StreamData> stream_data) {
kInitialEncryptionInfo, encryption_config.protection_scheme,
encryption_config.key_id, encryption_config.constant_iv,
encryption_config.key_system_info);
current_key_id_ = encryption_config.key_id;
}
return InitializeMuxer();
case StreamDataType::kSegmentInfo: {
auto& segment_info = stream_data->segment_info;
if (muxer_listener_) {
if (muxer_listener_ && segment_info->is_encrypted) {
const EncryptionConfig* encryption_config =
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(
!kInitialEncryptionInfo, encryption_config->protection_scheme,
encryption_config->key_id, encryption_config->constant_iv,
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;
muxer_listener_->OnEncryptionStart();
}

View File

@ -88,6 +88,7 @@ class Muxer : public MediaHandler {
MuxerOptions options_;
std::vector<std::shared_ptr<StreamInfo>> streams_;
std::vector<uint8_t> current_key_id_;
bool encryption_started_ = false;
bool cancelled_;

View File

@ -222,6 +222,7 @@ Status EncryptionHandler::ProcessMediaSample(MediaSample* sample) {
return status;
if (!CreateEncryptor(encryption_key))
return Status(error::ENCRYPTION_FAILURE, "Failed to create encryptor");
prev_crypto_period_index_ = current_crypto_period_index;
}
check_new_crypto_period_ = false;
}

View File

@ -304,7 +304,8 @@ class EncryptionHandlerEncryptionTest
} else {
EXPECT_CALL(*mock_header_parser, GetHeaderSize(_))
.WillOnce(Return(kSliceHeaderSize1))
.WillOnce(Return(kSliceHeaderSize2));
.WillOnce(Return(kSliceHeaderSize2))
.WillRepeatedly(Return(kSliceHeaderSize2));
}
InjectVideoSliceHeaderParserForTesting(std::move(mock_header_parser));
break;
@ -519,7 +520,9 @@ TEST_P(EncryptionHandlerEncryptionTest, ClearLeadWithNoKeyRotation) {
TEST_P(EncryptionHandlerEncryptionTest, ClearLeadWithKeyRotation) {
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;
encryption_options.protection_scheme = protection_scheme_;
encryption_options.clear_lead_in_seconds = kClearLeadInSeconds;
@ -546,19 +549,20 @@ TEST_P(EncryptionHandlerEncryptionTest, ClearLeadWithKeyRotation) {
InjectCodecParser();
// There are three segments. Only the third segment is encrypted.
// Crypto period duration is the same as segment duration, so there are three
// crypto periods, although only the last is encrypted.
for (int i = 0; i < 3; ++i) {
EXPECT_CALL(mock_key_source_, GetCryptoPeriodKey(i, _, _))
.WillOnce(DoAll(SetArgPointee<2>(GetMockEncryptionKey()),
Return(Status::OK)));
// There are five segments with the first two not encrypted.
for (int i = 0; i < 5; ++i) {
if ((i % kSegmentsPerCryptoPeriod) == 0) {
EXPECT_CALL(mock_key_source_,
GetCryptoPeriodKey(i / kSegmentsPerCryptoPeriod, _, _))
.WillOnce(DoAll(SetArgPointee<2>(GetMockEncryptionKey()),
Return(Status::OK)));
}
// Use single-frame segment for testing.
ASSERT_OK(Process(GetMediaSampleStreamData(
kStreamIndex, i * kSegmentDuration, kSegmentDuration)));
ASSERT_OK(Process(GetSegmentInfoStreamData(
kStreamIndex, i * kSegmentDuration, kSegmentDuration, !kIsSubsegment)));
const bool is_encrypted = i == 2;
const bool is_encrypted = i >= 2;
const auto& output_stream_data = GetOutputStreamDataVector();
EXPECT_THAT(output_stream_data,
ElementsAre(IsMediaSample(kStreamIndex, i * kSegmentDuration,

View File

@ -33,12 +33,9 @@ HlsNotifyMuxerListener::~HlsNotifyMuxerListener() {}
// These methods work together to notify that the media is encrypted.
// If OnEncryptionInfoReady() is called before the media has been started, then
// 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
// 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(
bool is_initial_encryption_info,
FourCC protection_scheme,

View File

@ -38,15 +38,14 @@ class MuxerListener {
virtual ~MuxerListener() {};
/// Called when the media's encryption information is ready. This should be
/// called before OnMediaStart(), if the media is encrypted.
/// All the parameters may be empty just to notify that the media is
/// encrypted.
/// For ISO BMFF (MP4) media:
/// If @a is_initial_encryption_info is true then @a key_id is the default_KID
/// in 'tenc' box.
/// If @a is_initial_encryption_info is false then @a key_id is the new key ID
/// for the for the next crypto period.
/// Called when the media's encryption information is ready.
/// OnEncryptionInfoReady with @a initial_encryption_info being true should be
/// called before OnMediaStart(), if the media is encrypted. All the
/// parameters may be empty just to notify that the media is encrypted. For
/// ISO BMFF (MP4) media: If @a is_initial_encryption_info is true then @a
/// key_id is the default_KID in 'tenc' box. If @a is_initial_encryption_info
/// is false then @a key_id is the new key ID for the for the next crypto
/// period.
/// @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
/// non-key-rotated media and should be called only once.

View File

@ -41,11 +41,6 @@ Status TsMuxer::AddSample(size_t stream_id,
Status TsMuxer::FinalizeSegment(size_t stream_id,
std::shared_ptr<SegmentInfo> segment_info) {
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
? Status::OK
: segmenter_->FinalizeSegment(segment_info->start_timestamp,