7 #include <packager/hls/base/media_playlist.h>
15 #include <absl/log/check.h>
16 #include <absl/log/log.h>
17 #include <absl/strings/numbers.h>
18 #include <absl/strings/str_format.h>
20 #include <packager/file.h>
21 #include <packager/hls/base/tag.h>
22 #include <packager/macros/logging.h>
23 #include <packager/media/base/language_utils.h>
24 #include <packager/media/base/muxer_util.h>
25 #include <packager/version/version.h>
31 int32_t GetTimeScale(
const MediaInfo& media_info) {
32 if (media_info.has_reference_time_scale())
33 return media_info.reference_time_scale();
35 if (media_info.has_video_info())
36 return media_info.video_info().time_scale();
38 if (media_info.has_audio_info())
39 return media_info.audio_info().time_scale();
43 std::string AdjustVideoCodec(
const std::string& codec) {
51 std::string adjusted_codec = codec;
52 std::string fourcc = codec.substr(0, 4);
54 adjusted_codec =
"avc1" + codec.substr(4);
55 else if (fourcc ==
"hev1")
56 adjusted_codec =
"hvc1" + codec.substr(4);
57 else if (fourcc ==
"dvhe")
58 adjusted_codec =
"dvh1" + codec.substr(4);
59 if (adjusted_codec != codec) {
60 VLOG(1) <<
"Adusting video codec string from " << codec <<
" to "
63 return adjusted_codec;
72 std::string GetLanguage(
const MediaInfo& media_info) {
74 if (media_info.has_audio_info()) {
75 lang = media_info.audio_info().language();
76 }
else if (media_info.has_text_info()) {
77 lang = media_info.text_info().language();
82 void AppendExtXMap(
const MediaInfo& media_info, std::string* out) {
83 if (media_info.has_init_segment_url()) {
84 Tag tag(
"#EXT-X-MAP", out);
85 tag.AddQuotedString(
"URI", media_info.init_segment_url().data());
87 }
else if (media_info.has_media_file_url() && media_info.has_init_range()) {
90 Tag tag(
"#EXT-X-MAP", out);
91 tag.AddQuotedString(
"URI", media_info.media_file_url().data());
93 if (media_info.has_init_range()) {
94 const uint64_t begin = media_info.init_range().begin();
95 const uint64_t end = media_info.init_range().end();
96 const uint64_t length = end - begin + 1;
98 tag.AddQuotedNumberPair(
"BYTERANGE", length,
'@', begin);
107 std::string CreatePlaylistHeader(
108 const MediaInfo& media_info,
109 int32_t target_duration,
110 HlsPlaylistType type,
111 MediaPlaylist::MediaPlaylistStreamType stream_type,
112 uint32_t media_sequence_number,
113 int discontinuity_sequence_number,
114 std::optional<double> start_time_offset) {
115 const std::string version = GetPackagerVersion();
116 std::string version_line;
117 if (!version.empty()) {
119 absl::StrFormat(
"## Generated with %s version %s\n",
120 GetPackagerProjectUrl().c_str(), version.c_str());
124 std::string header = absl::StrFormat(
128 "#EXT-X-TARGETDURATION:%d\n",
129 version_line.c_str(), target_duration);
132 case HlsPlaylistType::kVod:
133 header +=
"#EXT-X-PLAYLIST-TYPE:VOD\n";
135 case HlsPlaylistType::kEvent:
136 header +=
"#EXT-X-PLAYLIST-TYPE:EVENT\n";
138 case HlsPlaylistType::kLive:
139 if (media_sequence_number > 0) {
140 absl::StrAppendFormat(&header,
"#EXT-X-MEDIA-SEQUENCE:%d\n",
141 media_sequence_number);
143 if (discontinuity_sequence_number > 0) {
144 absl::StrAppendFormat(&header,
"#EXT-X-DISCONTINUITY-SEQUENCE:%d\n",
145 discontinuity_sequence_number);
149 NOTIMPLEMENTED() <<
"Unexpected MediaPlaylistType "
150 <<
static_cast<int>(type);
153 MediaPlaylist::MediaPlaylistStreamType::kVideoIFramesOnly) {
154 absl::StrAppendFormat(&header,
"#EXT-X-I-FRAMES-ONLY\n");
156 if (start_time_offset.has_value()) {
157 absl::StrAppendFormat(&header,
"#EXT-X-START:TIME-OFFSET=%f\n",
158 start_time_offset.value());
163 AppendExtXMap(media_info, &header);
171 HlsEntry::HlsEntry(HlsEntry::EntryType type) : type_(type) {}
172 HlsEntry::~HlsEntry() {}
174 class SegmentInfoEntry :
public HlsEntry {
182 SegmentInfoEntry(
const std::string& file_name,
184 double duration_seconds,
186 uint64_t start_byte_offset,
187 uint64_t segment_file_size,
188 uint64_t previous_segment_end_offset);
190 std::string ToString()
override;
191 int64_t start_time()
const {
return start_time_; }
192 double duration_seconds()
const {
return duration_seconds_; }
193 void set_duration_seconds(
double duration_seconds) {
194 duration_seconds_ = duration_seconds;
198 SegmentInfoEntry(
const SegmentInfoEntry&) =
delete;
199 SegmentInfoEntry& operator=(
const SegmentInfoEntry&) =
delete;
201 const std::string file_name_;
202 const int64_t start_time_;
203 double duration_seconds_;
204 const bool use_byte_range_;
205 const uint64_t start_byte_offset_;
206 const uint64_t segment_file_size_;
207 const uint64_t previous_segment_end_offset_;
210 SegmentInfoEntry::SegmentInfoEntry(
const std::string& file_name,
212 double duration_seconds,
214 uint64_t start_byte_offset,
215 uint64_t segment_file_size,
216 uint64_t previous_segment_end_offset)
217 : HlsEntry(HlsEntry::EntryType::kExtInf),
218 file_name_(file_name),
219 start_time_(start_time),
220 duration_seconds_(duration_seconds),
221 use_byte_range_(use_byte_range),
222 start_byte_offset_(start_byte_offset),
223 segment_file_size_(segment_file_size),
224 previous_segment_end_offset_(previous_segment_end_offset) {}
226 std::string SegmentInfoEntry::ToString() {
227 std::string result = absl::StrFormat(
"#EXTINF:%.3f,", duration_seconds_);
229 if (use_byte_range_) {
230 absl::StrAppendFormat(&result,
"\n#EXT-X-BYTERANGE:%" PRIu64,
232 if (previous_segment_end_offset_ + 1 != start_byte_offset_) {
233 absl::StrAppendFormat(&result,
"@%" PRIu64, start_byte_offset_);
237 absl::StrAppendFormat(&result,
"\n%s", file_name_.c_str());
243 class DiscontinuityEntry :
public HlsEntry {
245 DiscontinuityEntry();
247 std::string ToString()
override;
250 DiscontinuityEntry(
const DiscontinuityEntry&) =
delete;
251 DiscontinuityEntry& operator=(
const DiscontinuityEntry&) =
delete;
254 DiscontinuityEntry::DiscontinuityEntry()
255 : HlsEntry(HlsEntry::EntryType::kExtDiscontinuity) {}
257 std::string DiscontinuityEntry::ToString() {
258 return "#EXT-X-DISCONTINUITY";
261 class PlacementOpportunityEntry :
public HlsEntry {
263 PlacementOpportunityEntry();
265 std::string ToString()
override;
268 PlacementOpportunityEntry(
const PlacementOpportunityEntry&) =
delete;
269 PlacementOpportunityEntry& operator=(
const PlacementOpportunityEntry&) =
273 PlacementOpportunityEntry::PlacementOpportunityEntry()
274 : HlsEntry(HlsEntry::EntryType::kExtPlacementOpportunity) {}
276 std::string PlacementOpportunityEntry::ToString() {
277 return "#EXT-X-PLACEMENT-OPPORTUNITY";
280 EncryptionInfoEntry::EncryptionInfoEntry(MediaPlaylist::EncryptionMethod method,
281 const std::string& url,
282 const std::string& key_id,
283 const std::string& iv,
284 const std::string& key_format,
285 const std::string& key_format_versions)
286 : HlsEntry(HlsEntry::EntryType::kExtKey),
291 key_format_(key_format),
292 key_format_versions_(key_format_versions) {}
294 std::string EncryptionInfoEntry::ToString() {
298 std::string EncryptionInfoEntry::ToString(std::string tag_name) {
299 std::string tag_string;
300 if (tag_name.empty())
301 tag_name =
"#EXT-X-KEY";
302 Tag tag(tag_name, &tag_string);
304 if (method_ == MediaPlaylist::EncryptionMethod::kSampleAes) {
305 tag.AddString(
"METHOD",
"SAMPLE-AES");
306 }
else if (method_ == MediaPlaylist::EncryptionMethod::kAes128) {
307 tag.AddString(
"METHOD",
"AES-128");
308 }
else if (method_ == MediaPlaylist::EncryptionMethod::kSampleAesCenc) {
309 tag.AddString(
"METHOD",
"SAMPLE-AES-CTR");
311 DCHECK(method_ == MediaPlaylist::EncryptionMethod::kNone);
312 tag.AddString(
"METHOD",
"NONE");
315 tag.AddQuotedString(
"URI", url_);
317 if (!key_id_.empty()) {
318 tag.AddString(
"KEYID", key_id_);
321 tag.AddString(
"IV", iv_);
323 if (!key_format_versions_.empty()) {
324 tag.AddQuotedString(
"KEYFORMATVERSIONS", key_format_versions_);
326 if (!key_format_.empty()) {
327 tag.AddQuotedString(
"KEYFORMAT", key_format_);
333 MediaPlaylist::MediaPlaylist(
const HlsParams& hls_params,
334 const std::string& file_name,
335 const std::string& name,
336 const std::string& group_id)
337 : hls_params_(hls_params),
338 file_name_(file_name),
341 media_sequence_number_(hls_params_.media_sequence_number) {
343 if (media_sequence_number_ > 0)
344 entries_.emplace_back(
new DiscontinuityEntry());
347 MediaPlaylist::~MediaPlaylist() {}
350 MediaPlaylistStreamType stream_type) {
351 stream_type_ = stream_type;
363 const std::vector<std::string>& characteristics) {
364 characteristics_ = characteristics;
368 forced_subtitle_ = forced_subtitle;
372 MediaPlaylist::EncryptionMethod method,
373 const std::string& url,
374 const std::string& key_id,
375 const std::string& iv,
376 const std::string& key_format,
377 const std::string& key_format_versions) {
379 method, url, key_id, iv, key_format, key_format_versions));
383 const int32_t time_scale = GetTimeScale(media_info);
384 if (time_scale == 0) {
385 LOG(ERROR) <<
"MediaInfo does not contain a valid timescale.";
389 if (media_info.has_video_info()) {
390 stream_type_ = MediaPlaylistStreamType::kVideo;
391 codec_ = AdjustVideoCodec(media_info.video_info().codec());
392 if (media_info.video_info().has_supplemental_codec() &&
393 media_info.video_info().has_compatible_brand()) {
394 supplemental_codec_ =
395 AdjustVideoCodec(media_info.video_info().supplemental_codec());
396 compatible_brand_ =
static_cast<media::FourCC
>(
397 media_info.video_info().compatible_brand());
399 }
else if (media_info.has_audio_info()) {
400 stream_type_ = MediaPlaylistStreamType::kAudio;
401 codec_ = media_info.audio_info().codec();
403 stream_type_ = MediaPlaylistStreamType::kSubtitle;
404 codec_ = media_info.text_info().codec();
407 time_scale_ = time_scale;
408 media_info_ = media_info;
409 language_ = GetLanguage(media_info);
410 use_byte_range_ = !media_info_.has_segment_template_url() &&
411 media_info_.container_type() != MediaInfo::CONTAINER_TEXT;
413 std::vector<std::string>(media_info_.hls_characteristics().begin(),
414 media_info_.hls_characteristics().end());
416 forced_subtitle_ = media_info_.forced_subtitle();
422 if (media_info_.has_video_info())
423 media_info_.mutable_video_info()->set_frame_duration(sample_duration);
429 uint64_t start_byte_offset,
431 if (stream_type_ == MediaPlaylistStreamType::kVideoIFramesOnly) {
432 if (key_frames_.empty())
435 AdjustLastSegmentInfoEntryDuration(key_frames_.front().timestamp);
437 for (
auto iter = key_frames_.begin(); iter != key_frames_.end(); ++iter) {
440 const int64_t next_timestamp = std::next(iter) == key_frames_.end()
441 ? (start_time + duration)
442 : std::next(iter)->timestamp;
443 AddSegmentInfoEntry(file_name, iter->timestamp,
444 next_timestamp - iter->timestamp,
445 iter->start_byte_offset, iter->size);
450 return AddSegmentInfoEntry(file_name, start_time, duration, start_byte_offset,
455 uint64_t start_byte_offset,
457 if (stream_type_ != MediaPlaylistStreamType::kVideoIFramesOnly) {
458 if (stream_type_ != MediaPlaylistStreamType::kVideo) {
460 <<
"I-Frames Only playlist applies to video renditions only.";
463 stream_type_ = MediaPlaylistStreamType::kVideoIFramesOnly;
464 use_byte_range_ =
true;
466 key_frames_.push_back({timestamp, start_byte_offset, size, std::string(
"")});
470 const std::string& url,
471 const std::string& key_id,
472 const std::string& iv,
473 const std::string& key_format,
474 const std::string& key_format_versions) {
475 if (!inserted_discontinuity_tag_) {
478 if (!entries_.empty())
479 entries_.emplace_back(
new DiscontinuityEntry());
480 inserted_discontinuity_tag_ =
true;
483 method, url, key_id, iv, key_format, key_format_versions));
487 entries_.emplace_back(
new PlacementOpportunityEntry());
491 if (!target_duration_set_) {
495 std::string content = CreatePlaylistHeader(
496 media_info_, target_duration_, hls_params_.playlist_type, stream_type_,
497 media_sequence_number_, discontinuity_sequence_number_,
498 hls_params_.start_time_offset);
500 for (
const auto& entry : entries_)
501 absl::StrAppendFormat(&content,
"%s\n", entry->ToString().c_str());
503 if (hls_params_.playlist_type == HlsPlaylistType::kVod) {
504 content +=
"#EXT-X-ENDLIST\n";
507 if (!File::WriteFileAtomically(file_path.string().c_str(), content)) {
508 LOG(ERROR) <<
"Failed to write playlist to: " << file_path.string();
515 if (media_info_.has_bandwidth())
516 return media_info_.bandwidth();
517 return bandwidth_estimator_.
Max();
521 return bandwidth_estimator_.
Estimate();
525 return longest_segment_duration_seconds_;
529 if (target_duration_set_) {
530 if (target_duration_ == target_duration)
532 VLOG(1) <<
"Updating target duration from " << target_duration_ <<
" to "
535 target_duration_ = target_duration;
536 target_duration_set_ =
true;
540 return media_info_.audio_info().num_channels();
544 return media_info_.audio_info().codec_specific_data().ec3_joc_complexity();
548 return media_info_.audio_info().codec_specific_data().ac4_ims_flag();
552 return media_info_.audio_info().codec_specific_data().ac4_cbi_flag();
556 uint32_t* height)
const {
559 if (media_info_.has_video_info()) {
560 const double pixel_aspect_ratio =
561 media_info_.video_info().pixel_height() > 0
562 ?
static_cast<double>(media_info_.video_info().pixel_width()) /
563 media_info_.video_info().pixel_height()
565 *width =
static_cast<uint32_t
>(media_info_.video_info().width() *
567 *height = media_info_.video_info().height();
575 if (codec_.find(
"dvh") == 0)
580 switch (media_info_.video_info().transfer_characteristics()) {
588 if (!supplemental_codec_.empty() &&
589 compatible_brand_ == media::FOURCC_db4g)
607 if (media_info_.video_info().frame_duration() == 0)
609 return static_cast<double>(time_scale_) /
610 media_info_.video_info().frame_duration();
613 void MediaPlaylist::AddSegmentInfoEntry(
const std::string& segment_file_name,
616 uint64_t start_byte_offset,
618 if (time_scale_ == 0) {
619 LOG(WARNING) <<
"Timescale is not set and the duration for " << duration
620 <<
" cannot be calculated. The output will be wrong.";
622 entries_.emplace_back(
new SegmentInfoEntry(
623 segment_file_name, 0.0, 0.0, use_byte_range_, start_byte_offset, size,
624 previous_segment_end_offset_));
635 const double segment_duration_seconds =
636 static_cast<double>(duration) / time_scale_;
637 longest_segment_duration_seconds_ =
638 std::max(longest_segment_duration_seconds_, segment_duration_seconds);
639 bandwidth_estimator_.
AddBlock(size, segment_duration_seconds);
640 current_buffer_depth_ += segment_duration_seconds;
642 if (!entries_.empty() &&
643 entries_.back()->type() == HlsEntry::EntryType::kExtInf) {
644 const SegmentInfoEntry* segment_info =
645 static_cast<SegmentInfoEntry*
>(entries_.back().get());
646 if (segment_info->start_time() > start_time) {
648 <<
"Insert a discontinuity tag after the segment with start time "
649 << segment_info->start_time() <<
" as the next segment starts at "
650 << start_time <<
".";
651 entries_.emplace_back(
new DiscontinuityEntry());
655 entries_.emplace_back(
new SegmentInfoEntry(
656 segment_file_name, start_time, segment_duration_seconds, use_byte_range_,
657 start_byte_offset, size, previous_segment_end_offset_));
658 previous_segment_end_offset_ = start_byte_offset + size - 1;
661 void MediaPlaylist::AdjustLastSegmentInfoEntryDuration(int64_t next_timestamp) {
662 if (time_scale_ == 0)
665 const double next_timestamp_seconds =
666 static_cast<double>(next_timestamp) / time_scale_;
668 for (
auto iter = entries_.rbegin(); iter != entries_.rend(); ++iter) {
669 if (iter->get()->type() == HlsEntry::EntryType::kExtInf) {
670 SegmentInfoEntry* segment_info =
671 reinterpret_cast<SegmentInfoEntry*
>(iter->get());
673 const double segment_duration_seconds =
674 next_timestamp_seconds -
675 static_cast<double>(segment_info->start_time()) / time_scale_;
677 if (segment_duration_seconds > 0)
678 segment_info->set_duration_seconds(segment_duration_seconds);
679 longest_segment_duration_seconds_ =
680 std::max(longest_segment_duration_seconds_, segment_duration_seconds);
694 void MediaPlaylist::SlideWindow() {
695 if (hls_params_.time_shift_buffer_depth <= 0.0 ||
696 hls_params_.playlist_type != HlsPlaylistType::kLive) {
699 DCHECK_GT(time_scale_, 0);
701 if (current_buffer_depth_ <= hls_params_.time_shift_buffer_depth)
711 std::list<std::unique_ptr<HlsEntry>> ext_x_keys;
714 HlsEntry::EntryType prev_entry_type = HlsEntry::EntryType::kExtInf;
716 std::list<std::unique_ptr<HlsEntry>>::iterator last = entries_.begin();
717 for (; last != entries_.end(); ++last) {
718 HlsEntry::EntryType entry_type = last->get()->type();
719 if (entry_type == HlsEntry::EntryType::kExtKey) {
720 if (prev_entry_type != HlsEntry::EntryType::kExtKey)
722 ext_x_keys.push_back(std::move(*last));
723 }
else if (entry_type == HlsEntry::EntryType::kExtDiscontinuity) {
724 ++discontinuity_sequence_number_;
726 DCHECK_EQ(
static_cast<int>(entry_type),
727 static_cast<int>(HlsEntry::EntryType::kExtInf));
729 const SegmentInfoEntry& segment_info =
730 *
reinterpret_cast<SegmentInfoEntry*
>(last->get());
733 const bool segment_within_time_shift_buffer =
734 current_buffer_depth_ - segment_info.duration_seconds() <
735 hls_params_.time_shift_buffer_depth;
736 if (segment_within_time_shift_buffer)
738 current_buffer_depth_ -= segment_info.duration_seconds();
739 RemoveOldSegment(segment_info.start_time());
740 media_sequence_number_++;
742 prev_entry_type = entry_type;
744 entries_.erase(entries_.begin(), last);
746 entries_.insert(entries_.begin(), std::make_move_iterator(ext_x_keys.begin()),
747 std::make_move_iterator(ext_x_keys.end()));
750 void MediaPlaylist::RemoveOldSegment(int64_t start_time) {
751 if (hls_params_.preserved_segments_outside_live_window == 0)
753 if (stream_type_ == MediaPlaylistStreamType::kVideoIFramesOnly)
756 segments_to_be_removed_.push_back(media::GetSegmentName(
757 media_info_.segment_template(), start_time, media_sequence_number_ + 1,
758 media_info_.bandwidth()));
759 while (segments_to_be_removed_.size() >
760 hls_params_.preserved_segments_outside_live_window) {
761 VLOG(2) <<
"Deleting " << segments_to_be_removed_.front();
762 if (!File::Delete(segments_to_be_removed_.front().c_str())) {
763 LOG(WARNING) <<
"Failed to delete " << segments_to_be_removed_.front()
764 <<
"; Will retry later.";
767 segments_to_be_removed_.pop_front();
void AddBlock(uint64_t size_in_bytes, double duration)
uint64_t Estimate() const
All the methods that are virtual are virtual for mocking.
std::string LanguageToShortestForm(const std::string &language)