Shaka Packager SDK
Loading...
Searching...
No Matches
media_playlist.cc
1// Copyright 2016 Google LLC. All rights reserved.
2//
3// Use of this source code is governed by a BSD-style
4// license that can be found in the LICENSE file or at
5// https://developers.google.com/open-source/licenses/bsd
6
7#include <packager/hls/base/media_playlist.h>
8
9#include <algorithm>
10#include <cinttypes>
11#include <cmath>
12#include <memory>
13#include <optional>
14
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>
19
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>
26
27namespace shaka {
28namespace hls {
29
30namespace {
31int32_t GetTimeScale(const MediaInfo& media_info) {
32 if (media_info.has_reference_time_scale())
33 return media_info.reference_time_scale();
34
35 if (media_info.has_video_info())
36 return media_info.video_info().time_scale();
37
38 if (media_info.has_audio_info())
39 return media_info.audio_info().time_scale();
40 return 0;
41}
42
43std::string AdjustVideoCodec(const std::string& codec) {
44 // Apple does not like video formats with the parameter sets stored in the
45 // samples. It also fails mediastreamvalidator checks and some Apple devices /
46 // platforms refused to play.
47 // See https://apple.co/30n90DC 1.10 and
48 // https://github.com/shaka-project/shaka-packager/issues/587#issuecomment-489182182.
49 // Replaced with the corresponding formats with the parameter sets stored in
50 // the sample descriptions instead.
51 std::string adjusted_codec = codec;
52 std::string fourcc = codec.substr(0, 4);
53 if (fourcc == "avc3")
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 "
61 << adjusted_codec;
62 }
63 return adjusted_codec;
64}
65
66// Duplicated from MpdUtils because:
67// 1. MpdUtils header depends on libxml header, which is not in the deps here
68// 2. GetLanguage depends on MediaInfo from packager/mpd/
69// 3. Moving GetLanguage to LanguageUtils would create a a media => mpd dep.
70// TODO(https://github.com/shaka-project/shaka-packager/issues/373): Fix this
71// dependency situation and factor this out to a common location.
72std::string GetLanguage(const MediaInfo& media_info) {
73 std::string lang;
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();
78 }
79 return LanguageToShortestForm(lang);
80}
81
82void 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());
86 out->append("\n");
87 } else if (media_info.has_media_file_url() && media_info.has_init_range()) {
88 // It only makes sense for single segment media to have EXT-X-MAP if
89 // there is init_range.
90 Tag tag("#EXT-X-MAP", out);
91 tag.AddQuotedString("URI", media_info.media_file_url().data());
92
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;
97
98 tag.AddQuotedNumberPair("BYTERANGE", length, '@', begin);
99 }
100
101 out->append("\n");
102 } else {
103 // This media info does not need an ext-x-map tag.
104 }
105}
106
107std::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()) {
118 version_line =
119 absl::StrFormat("## Generated with %s version %s\n",
120 GetPackagerProjectUrl().c_str(), version.c_str());
121 }
122
123 // 6 is required for EXT-X-MAP without EXT-X-I-FRAMES-ONLY.
124 std::string header = absl::StrFormat(
125 "#EXTM3U\n"
126 "#EXT-X-VERSION:6\n"
127 "%s"
128 "#EXT-X-TARGETDURATION:%d\n",
129 version_line.c_str(), target_duration);
130
131 switch (type) {
132 case HlsPlaylistType::kVod:
133 header += "#EXT-X-PLAYLIST-TYPE:VOD\n";
134 break;
135 case HlsPlaylistType::kEvent:
136 header += "#EXT-X-PLAYLIST-TYPE:EVENT\n";
137 break;
138 case HlsPlaylistType::kLive:
139 if (media_sequence_number > 0) {
140 absl::StrAppendFormat(&header, "#EXT-X-MEDIA-SEQUENCE:%d\n",
141 media_sequence_number);
142 }
143 if (discontinuity_sequence_number > 0) {
144 absl::StrAppendFormat(&header, "#EXT-X-DISCONTINUITY-SEQUENCE:%d\n",
145 discontinuity_sequence_number);
146 }
147 break;
148 default:
149 NOTIMPLEMENTED() << "Unexpected MediaPlaylistType "
150 << static_cast<int>(type);
151 }
152 if (stream_type ==
153 MediaPlaylist::MediaPlaylistStreamType::kVideoIFramesOnly) {
154 absl::StrAppendFormat(&header, "#EXT-X-I-FRAMES-ONLY\n");
155 }
156 if (start_time_offset.has_value()) {
157 absl::StrAppendFormat(&header, "#EXT-X-START:TIME-OFFSET=%f\n",
158 start_time_offset.value());
159 }
160
161 // Put EXT-X-MAP at the end since the rest of the playlist is about the
162 // segment and key info.
163 AppendExtXMap(media_info, &header);
164
165 return header;
166}
167
168} // namespace
169
170HlsEntry::HlsEntry(HlsEntry::EntryType type) : type_(type) {}
171HlsEntry::~HlsEntry() {}
172
173class SegmentInfoEntry : public HlsEntry {
174 public:
175 // If |use_byte_range| true then this will append EXT-X-BYTERANGE
176 // after EXTINF.
177 // It uses |previous_segment_end_offset| to determine if it has to also
178 // specify the start byte offset in the tag.
179 // |start_time| is in timescale.
180 // |duration_seconds| is duration in seconds.
181 SegmentInfoEntry(const std::string& file_name,
182 int64_t start_time,
183 double duration_seconds,
184 bool use_byte_range,
185 uint64_t start_byte_offset,
186 uint64_t segment_file_size,
187 uint64_t previous_segment_end_offset);
188
189 std::string ToString() override;
190 int64_t start_time() const { return start_time_; }
191 double duration_seconds() const { return duration_seconds_; }
192 void set_duration_seconds(double duration_seconds) {
193 duration_seconds_ = duration_seconds;
194 }
195
196 private:
197 SegmentInfoEntry(const SegmentInfoEntry&) = delete;
198 SegmentInfoEntry& operator=(const SegmentInfoEntry&) = delete;
199
200 const std::string file_name_;
201 const int64_t start_time_;
202 double duration_seconds_;
203 const bool use_byte_range_;
204 const uint64_t start_byte_offset_;
205 const uint64_t segment_file_size_;
206 const uint64_t previous_segment_end_offset_;
207};
208
209SegmentInfoEntry::SegmentInfoEntry(const std::string& file_name,
210 int64_t start_time,
211 double duration_seconds,
212 bool use_byte_range,
213 uint64_t start_byte_offset,
214 uint64_t segment_file_size,
215 uint64_t previous_segment_end_offset)
216 : HlsEntry(HlsEntry::EntryType::kExtInf),
217 file_name_(file_name),
218 start_time_(start_time),
219 duration_seconds_(duration_seconds),
220 use_byte_range_(use_byte_range),
221 start_byte_offset_(start_byte_offset),
222 segment_file_size_(segment_file_size),
223 previous_segment_end_offset_(previous_segment_end_offset) {}
224
225std::string SegmentInfoEntry::ToString() {
226 std::string result = absl::StrFormat("#EXTINF:%.3f,", duration_seconds_);
227
228 if (use_byte_range_) {
229 absl::StrAppendFormat(&result, "\n#EXT-X-BYTERANGE:%" PRIu64,
230 segment_file_size_);
231 if (previous_segment_end_offset_ + 1 != start_byte_offset_) {
232 absl::StrAppendFormat(&result, "@%" PRIu64, start_byte_offset_);
233 }
234 }
235
236 absl::StrAppendFormat(&result, "\n%s", file_name_.c_str());
237
238 return result;
239}
240
241class DiscontinuityEntry : public HlsEntry {
242 public:
243 DiscontinuityEntry();
244
245 std::string ToString() override;
246
247 private:
248 DiscontinuityEntry(const DiscontinuityEntry&) = delete;
249 DiscontinuityEntry& operator=(const DiscontinuityEntry&) = delete;
250};
251
252DiscontinuityEntry::DiscontinuityEntry()
253 : HlsEntry(HlsEntry::EntryType::kExtDiscontinuity) {}
254
255std::string DiscontinuityEntry::ToString() {
256 return "#EXT-X-DISCONTINUITY";
257}
258
259ProgramDateTimeEntry::ProgramDateTimeEntry(const absl::Time& program_time)
260 : HlsEntry(HlsEntry::EntryType::kProgramDateTime),
261 program_time_(program_time) {}
262
263std::string ProgramDateTimeEntry::ToString() {
264 absl::CivilSecond cs =
265 absl::ToCivilSecond(program_time_, absl::UTCTimeZone());
266
267 int64_t total_ms = absl::ToUnixMillis(program_time_);
268 int ms = static_cast<int>(total_ms % 1000);
269 if (ms < 0)
270 ms += 1000; // correction for possible negative times
271
272 return absl::StrFormat(
273 "#EXT-X-PROGRAM-DATE-TIME:%04d-%02d-%02dT%02d:%02d:%02d.%03dZ", cs.year(),
274 cs.month(), cs.day(), cs.hour(), cs.minute(), cs.second(), ms);
275}
276
277class PlacementOpportunityEntry : public HlsEntry {
278 public:
279 PlacementOpportunityEntry();
280
281 std::string ToString() override;
282
283 private:
284 PlacementOpportunityEntry(const PlacementOpportunityEntry&) = delete;
285 PlacementOpportunityEntry& operator=(const PlacementOpportunityEntry&) =
286 delete;
287};
288
289PlacementOpportunityEntry::PlacementOpportunityEntry()
290 : HlsEntry(HlsEntry::EntryType::kExtPlacementOpportunity) {}
291
292std::string PlacementOpportunityEntry::ToString() {
293 return "#EXT-X-PLACEMENT-OPPORTUNITY";
294}
295
296EncryptionInfoEntry::EncryptionInfoEntry(MediaPlaylist::EncryptionMethod method,
297 const std::string& url,
298 const std::string& key_id,
299 const std::string& iv,
300 const std::string& key_format,
301 const std::string& key_format_versions)
302 : HlsEntry(HlsEntry::EntryType::kExtKey),
303 method_(method),
304 url_(url),
305 key_id_(key_id),
306 iv_(iv),
307 key_format_(key_format),
308 key_format_versions_(key_format_versions) {}
309
310std::string EncryptionInfoEntry::ToString() {
311 return ToString("");
312}
313
314std::string EncryptionInfoEntry::ToString(std::string tag_name) {
315 std::string tag_string;
316 if (tag_name.empty())
317 tag_name = "#EXT-X-KEY";
318 Tag tag(tag_name, &tag_string);
319
320 if (method_ == MediaPlaylist::EncryptionMethod::kSampleAes) {
321 tag.AddString("METHOD", "SAMPLE-AES");
322 } else if (method_ == MediaPlaylist::EncryptionMethod::kAes128) {
323 tag.AddString("METHOD", "AES-128");
324 } else if (method_ == MediaPlaylist::EncryptionMethod::kSampleAesCenc) {
325 tag.AddString("METHOD", "SAMPLE-AES-CTR");
326 } else {
327 DCHECK(method_ == MediaPlaylist::EncryptionMethod::kNone);
328 tag.AddString("METHOD", "NONE");
329 }
330
331 tag.AddQuotedString("URI", url_);
332
333 if (!key_id_.empty()) {
334 tag.AddString("KEYID", key_id_);
335 }
336 if (!iv_.empty()) {
337 tag.AddString("IV", iv_);
338 }
339 if (!key_format_versions_.empty()) {
340 tag.AddQuotedString("KEYFORMATVERSIONS", key_format_versions_);
341 }
342 if (!key_format_.empty()) {
343 tag.AddQuotedString("KEYFORMAT", key_format_);
344 }
345
346 return tag_string;
347}
348
349MediaPlaylist::MediaPlaylist(const HlsParams& hls_params,
350 const std::string& file_name,
351 const std::string& name,
352 const std::string& group_id)
353 : hls_params_(hls_params),
354 file_name_(file_name),
355 name_(name),
356 group_id_(group_id),
357 media_sequence_number_(hls_params_.media_sequence_number),
358 reference_time_(absl::InfinitePast()) {
359 // When there's a forced media_sequence_number, start with discontinuity
360 if (media_sequence_number_ > 0)
361 entries_.emplace_back(new DiscontinuityEntry());
362}
363
364MediaPlaylist::~MediaPlaylist() {}
365
367 MediaPlaylistStreamType stream_type) {
368 stream_type_ = stream_type;
369}
370
371void MediaPlaylist::SetCodecForTesting(const std::string& codec) {
372 codec_ = codec;
373}
374
375void MediaPlaylist::SetLanguageForTesting(const std::string& language) {
376 language_ = language;
377}
378
380 const std::vector<std::string>& characteristics) {
381 characteristics_ = characteristics;
382}
383
385 media_info_.set_index(index);
386}
387
388void MediaPlaylist::SetForcedSubtitleForTesting(const bool forced_subtitle) {
389 forced_subtitle_ = forced_subtitle;
390}
391
393 MediaPlaylist::EncryptionMethod method,
394 const std::string& url,
395 const std::string& key_id,
396 const std::string& iv,
397 const std::string& key_format,
398 const std::string& key_format_versions) {
399 entries_.emplace_back(new EncryptionInfoEntry(
400 method, url, key_id, iv, key_format, key_format_versions));
401}
402
403bool MediaPlaylist::SetMediaInfo(const MediaInfo& media_info) {
404 const int32_t time_scale = GetTimeScale(media_info);
405 if (time_scale == 0) {
406 LOG(ERROR) << "MediaInfo does not contain a valid timescale.";
407 return false;
408 }
409
410 if (media_info.has_video_info()) {
411 stream_type_ = MediaPlaylistStreamType::kVideo;
412 codec_ = AdjustVideoCodec(media_info.video_info().codec());
413 if (media_info.video_info().has_supplemental_codec() &&
414 media_info.video_info().has_compatible_brand()) {
415 supplemental_codec_ =
416 AdjustVideoCodec(media_info.video_info().supplemental_codec());
417 compatible_brand_ = static_cast<media::FourCC>(
418 media_info.video_info().compatible_brand());
419 }
420 } else if (media_info.has_audio_info()) {
421 stream_type_ = MediaPlaylistStreamType::kAudio;
422 codec_ = media_info.audio_info().codec();
423 } else {
424 stream_type_ = MediaPlaylistStreamType::kSubtitle;
425 codec_ = media_info.text_info().codec();
426 }
427
428 time_scale_ = time_scale;
429 media_info_ = media_info;
430 language_ = GetLanguage(media_info);
431 use_byte_range_ = !media_info_.has_segment_template_url() &&
432 media_info_.container_type() != MediaInfo::CONTAINER_TEXT;
433 characteristics_ =
434 std::vector<std::string>(media_info_.hls_characteristics().begin(),
435 media_info_.hls_characteristics().end());
436
437 forced_subtitle_ = media_info_.forced_subtitle();
438
439 return true;
440}
441
442void MediaPlaylist::SetSampleDuration(int32_t sample_duration) {
443 if (media_info_.has_video_info())
444 media_info_.mutable_video_info()->set_frame_duration(sample_duration);
445}
446
447void MediaPlaylist::AddSegment(const std::string& file_name,
448 int64_t start_time,
449 int64_t duration,
450 uint64_t start_byte_offset,
451 uint64_t size) {
452 if (stream_type_ == MediaPlaylistStreamType::kVideoIFramesOnly) {
453 if (key_frames_.empty())
454 return;
455
456 AdjustLastSegmentInfoEntryDuration(key_frames_.front().timestamp);
457
458 for (auto iter = key_frames_.begin(); iter != key_frames_.end(); ++iter) {
459 // Last entry duration may be adjusted later when the next iframe becomes
460 // available.
461 const int64_t next_timestamp = std::next(iter) == key_frames_.end()
462 ? (start_time + duration)
463 : std::next(iter)->timestamp;
464 AddSegmentInfoEntry(file_name, iter->timestamp,
465 next_timestamp - iter->timestamp,
466 iter->start_byte_offset, iter->size);
467 }
468 key_frames_.clear();
469 return;
470 }
471 return AddSegmentInfoEntry(file_name, start_time, duration, start_byte_offset,
472 size);
473}
474
475void MediaPlaylist::SetReferenceTime(const absl::Time& reference_time) {
476 reference_time_ = reference_time;
477}
478
479void MediaPlaylist::AddKeyFrame(int64_t timestamp,
480 uint64_t start_byte_offset,
481 uint64_t size) {
482 if (stream_type_ != MediaPlaylistStreamType::kVideoIFramesOnly) {
483 if (stream_type_ != MediaPlaylistStreamType::kVideo) {
484 LOG(WARNING)
485 << "I-Frames Only playlist applies to video renditions only.";
486 return;
487 }
488 stream_type_ = MediaPlaylistStreamType::kVideoIFramesOnly;
489 use_byte_range_ = true;
490 }
491 key_frames_.push_back({timestamp, start_byte_offset, size, std::string("")});
492}
493
494void MediaPlaylist::AddEncryptionInfo(MediaPlaylist::EncryptionMethod method,
495 const std::string& url,
496 const std::string& key_id,
497 const std::string& iv,
498 const std::string& key_format,
499 const std::string& key_format_versions) {
500 if (!inserted_discontinuity_tag_) {
501 // Insert discontinuity tag only for the first EXT-X-KEY, only if there
502 // are non-encrypted media segments.
503 if (!entries_.empty())
504 entries_.emplace_back(new DiscontinuityEntry());
505 inserted_discontinuity_tag_ = true;
506 }
507 entries_.emplace_back(new EncryptionInfoEntry(
508 method, url, key_id, iv, key_format, key_format_versions));
509}
510
512 entries_.emplace_back(new PlacementOpportunityEntry());
513}
514
515bool MediaPlaylist::WriteToFile(const std::filesystem::path& file_path,
516 bool event_to_vod_on_end_of_stream,
517 bool end_stream) {
518 if (!target_duration_set_) {
520 }
521
522 HlsPlaylistType playlist_type = hls_params_.playlist_type;
523 if (event_to_vod_on_end_of_stream && end_stream &&
524 playlist_type == HlsPlaylistType::kEvent) {
525 playlist_type = HlsPlaylistType::kVod;
526 }
527
528 std::string content = CreatePlaylistHeader(
529 media_info_, target_duration_, playlist_type, stream_type_,
530 media_sequence_number_, discontinuity_sequence_number_,
531 hls_params_.start_time_offset);
532
533 for (const auto& entry : entries_)
534 absl::StrAppendFormat(&content, "%s\n", entry->ToString().c_str());
535
536 if (playlist_type == HlsPlaylistType::kVod) {
537 content += "#EXT-X-ENDLIST\n";
538 }
539
540 if (!File::WriteFileAtomically(file_path.string().c_str(), content)) {
541 LOG(ERROR) << "Failed to write playlist to: " << file_path.string();
542 return false;
543 }
544 return true;
545}
546
548 if (media_info_.has_bandwidth())
549 return media_info_.bandwidth();
550 return bandwidth_estimator_.Max();
551}
552
554 return bandwidth_estimator_.Estimate();
555}
556
558 return longest_segment_duration_seconds_;
559}
560
561void MediaPlaylist::SetTargetDuration(int32_t target_duration) {
562 if (target_duration_set_) {
563 if (target_duration_ == target_duration)
564 return;
565 VLOG(1) << "Updating target duration from " << target_duration_ << " to "
566 << target_duration;
567 }
568 target_duration_ = target_duration;
569 target_duration_set_ = true;
570}
571
573 return media_info_.audio_info().num_channels();
574}
575
577 return media_info_.audio_info().codec_specific_data().ec3_joc_complexity();
578}
579
581 return media_info_.audio_info().codec_specific_data().ac4_ims_flag();
582}
583
585 return media_info_.audio_info().codec_specific_data().ac4_cbi_flag();
586}
587
589 uint32_t* height) const {
590 DCHECK(width);
591 DCHECK(height);
592 if (media_info_.has_video_info()) {
593 const double pixel_aspect_ratio =
594 media_info_.video_info().pixel_height() > 0
595 ? static_cast<double>(media_info_.video_info().pixel_width()) /
596 media_info_.video_info().pixel_height()
597 : 1.0;
598 *width = static_cast<uint32_t>(media_info_.video_info().width() *
599 pixel_aspect_ratio);
600 *height = media_info_.video_info().height();
601 return true;
602 }
603 return false;
604}
605
606std::string MediaPlaylist::GetVideoRange() const {
607 // Dolby Vision (dvh1 or dvhe) is always HDR.
608 if (codec_.find("dvh") == 0)
609 return "PQ";
610
611 // HLS specification:
612 // https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-02#section-4.4.4.2
613 switch (media_info_.video_info().transfer_characteristics()) {
614 case 1:
615 case 6:
616 case 13:
617 case 14:
618 // Dolby Vision profile 8.4 may have a transfer_characteristics 14, the
619 // actual value refers to preferred_transfer_characteristic value in SEI
620 // message, using compatible brand as a workaround
621 if (!supplemental_codec_.empty() &&
622 compatible_brand_ == media::FOURCC_db4g)
623 return "HLG";
624 else
625 return "SDR";
626 case 15:
627 return "SDR";
628 case 16:
629 return "PQ";
630 case 18:
631 return "HLG";
632 default:
633 // Leave it empty if we do not have the transfer characteristics
634 // information.
635 return "";
636 }
637}
638
640 if (media_info_.video_info().frame_duration() == 0)
641 return 0;
642 return static_cast<double>(time_scale_) /
643 media_info_.video_info().frame_duration();
644}
645
646void MediaPlaylist::AddSegmentInfoEntry(const std::string& segment_file_name,
647 int64_t start_time,
648 int64_t duration,
649 uint64_t start_byte_offset,
650 uint64_t size) {
651 if (time_scale_ == 0) {
652 LOG(WARNING) << "Timescale is not set and the duration for " << duration
653 << " cannot be calculated. The output will be wrong.";
654
655 entries_.emplace_back(new SegmentInfoEntry(
656 segment_file_name, 0.0, 0.0, use_byte_range_, start_byte_offset, size,
657 previous_segment_end_offset_));
658 return;
659 }
660
661 // In order for the oldest segment to be accessible for at least
662 // |time_shift_buffer_depth| seconds, the latest segment should not be in the
663 // sliding window since the player could be playing any part of the latest
664 // segment. So the current segment duration is added to the sum of segment
665 // durations (in the manifest/playlist) after sliding the window.
666 SlideWindow();
667
668 const double segment_duration_seconds =
669 static_cast<double>(duration) / time_scale_;
670 longest_segment_duration_seconds_ =
671 std::max(longest_segment_duration_seconds_, segment_duration_seconds);
672 bandwidth_estimator_.AddBlock(size, segment_duration_seconds);
673 current_buffer_depth_ += segment_duration_seconds;
674
675 if (!entries_.empty() &&
676 entries_.back()->type() == HlsEntry::EntryType::kExtInf) {
677 const SegmentInfoEntry* segment_info =
678 static_cast<SegmentInfoEntry*>(entries_.back().get());
679 if (segment_info->start_time() > start_time) {
680 LOG(WARNING)
681 << "Insert a discontinuity tag after the segment with start time "
682 << segment_info->start_time() << " as the next segment starts at "
683 << start_time << ".";
684 entries_.emplace_back(new DiscontinuityEntry());
685 }
686 }
687
688 if (hls_params_.add_program_date_time &&
689 reference_time_ != absl::InfinitePast()) {
690 // See if we need to add a program date time tag. It is added before the
691 // first segment, and after every discontinuity.
692 bool is_first_segment = true;
693 bool is_discontinuity = false;
694 if (!entries_.empty()) {
695 for (auto it = entries_.rbegin(); it != entries_.rend(); ++it) {
696 if ((*it)->type() == HlsEntry::EntryType::kExtInf) {
697 is_first_segment = false;
698 break;
699 }
700 }
701
702 const auto& last = *entries_.back();
703 if (last.type() == HlsEntry::EntryType::kExtDiscontinuity) {
704 is_discontinuity = true;
705 } else if (entries_.size() >= 2) {
706 const auto& second_last = **std::prev(entries_.cend(), 2);
707 if (last.type() == HlsEntry::EntryType::kExtKey &&
708 second_last.type() == HlsEntry::EntryType::kExtDiscontinuity) {
709 is_discontinuity = true;
710 }
711 }
712 }
713
714 if (is_first_segment || is_discontinuity) {
715 const absl::Time program_time =
716 reference_time_ +
717 absl::Seconds(static_cast<double>(start_time) / time_scale_);
718 entries_.emplace_back(new ProgramDateTimeEntry(program_time));
719 }
720 }
721
722 entries_.emplace_back(new SegmentInfoEntry(
723 segment_file_name, start_time, segment_duration_seconds, use_byte_range_,
724 start_byte_offset, size, previous_segment_end_offset_));
725 previous_segment_end_offset_ = start_byte_offset + size - 1;
726}
727
728void MediaPlaylist::AdjustLastSegmentInfoEntryDuration(int64_t next_timestamp) {
729 if (time_scale_ == 0)
730 return;
731
732 const double next_timestamp_seconds =
733 static_cast<double>(next_timestamp) / time_scale_;
734
735 for (auto iter = entries_.rbegin(); iter != entries_.rend(); ++iter) {
736 if (iter->get()->type() == HlsEntry::EntryType::kExtInf) {
737 SegmentInfoEntry* segment_info =
738 reinterpret_cast<SegmentInfoEntry*>(iter->get());
739
740 const double segment_duration_seconds =
741 next_timestamp_seconds -
742 static_cast<double>(segment_info->start_time()) / time_scale_;
743 // It could be negative if timestamp messed up.
744 if (segment_duration_seconds > 0)
745 segment_info->set_duration_seconds(segment_duration_seconds);
746 longest_segment_duration_seconds_ =
747 std::max(longest_segment_duration_seconds_, segment_duration_seconds);
748 break;
749 }
750 }
751}
752
753// TODO(kqyang): Right now this class manages the segments including the
754// deletion of segments when it is no longer needed. However, this class does
755// not have access to the segment file paths, which is already translated to
756// segment URLs by HlsNotifier. We have to re-generate segment file paths from
757// segment template here in order to delete the old segments.
758// To make the pipeline cleaner, we should move all file manipulations including
759// segment management to an intermediate layer between HlsNotifier and
760// MediaPlaylist.
761void MediaPlaylist::SlideWindow() {
762 if (hls_params_.time_shift_buffer_depth <= 0.0 ||
763 hls_params_.playlist_type != HlsPlaylistType::kLive) {
764 return;
765 }
766 DCHECK_GT(time_scale_, 0);
767
768 if (current_buffer_depth_ <= hls_params_.time_shift_buffer_depth)
769 return;
770
771 // Temporary list to hold the EXT-X-KEYs. For example, this allows us to
772 // remove <3> without removing <1> and <2> below (<1> and <2> are moved to the
773 // temporary list and added back later).
774 // #EXT-X-KEY <1>
775 // #EXT-X-KEY <2>
776 // #EXTINF <3>
777 // #EXTINF <4>
778 std::list<std::unique_ptr<HlsEntry>> ext_x_keys;
779 // Consecutive key entries are either fully removed or not removed at all.
780 // Keep track of entry types so we know if it is consecutive key entries.
781 HlsEntry::EntryType prev_entry_type = HlsEntry::EntryType::kExtInf;
782
783 std::list<std::unique_ptr<HlsEntry>>::iterator last = entries_.begin();
784 for (; last != entries_.end(); ++last) {
785 HlsEntry::EntryType entry_type = last->get()->type();
786 if (entry_type == HlsEntry::EntryType::kExtKey) {
787 if (prev_entry_type != HlsEntry::EntryType::kExtKey)
788 ext_x_keys.clear();
789 ext_x_keys.push_back(std::move(*last));
790 } else if (entry_type == HlsEntry::EntryType::kExtDiscontinuity) {
791 ++discontinuity_sequence_number_;
792 } else {
793 DCHECK_EQ(static_cast<int>(entry_type),
794 static_cast<int>(HlsEntry::EntryType::kExtInf));
795
796 const SegmentInfoEntry& segment_info =
797 *reinterpret_cast<SegmentInfoEntry*>(last->get());
798 // Remove the current segment only if it falls completely out of time
799 // shift buffer range.
800 const bool segment_within_time_shift_buffer =
801 current_buffer_depth_ - segment_info.duration_seconds() <
802 hls_params_.time_shift_buffer_depth;
803 if (segment_within_time_shift_buffer)
804 break;
805 current_buffer_depth_ -= segment_info.duration_seconds();
806 RemoveOldSegment(segment_info.start_time());
807 media_sequence_number_++;
808 }
809 prev_entry_type = entry_type;
810 }
811 entries_.erase(entries_.begin(), last);
812 // Add key entries back.
813 entries_.insert(entries_.begin(), std::make_move_iterator(ext_x_keys.begin()),
814 std::make_move_iterator(ext_x_keys.end()));
815}
816
817void MediaPlaylist::RemoveOldSegment(int64_t start_time) {
818 if (hls_params_.preserved_segments_outside_live_window == 0)
819 return;
820 if (stream_type_ == MediaPlaylistStreamType::kVideoIFramesOnly)
821 return;
822
823 segments_to_be_removed_.push_back(media::GetSegmentName(
824 media_info_.segment_template(), start_time, media_sequence_number_ + 1,
825 media_info_.bandwidth()));
826 while (segments_to_be_removed_.size() >
827 hls_params_.preserved_segments_outside_live_window) {
828 const std::string& file_name = segments_to_be_removed_.front();
829 VLOG(2) << "Deleting " << file_name;
830 // DASH and HLS outputs could both be tracking the same files and are in a
831 // race to delete them. Delete() returns false if the file does not exist,
832 // but we only want to retry if the file does exist (indicating a failure
833 // to delete, rather than the file already being gone). GetFileSize()
834 // returns >= 0 if the file exists, and < 0 if it does not.
835 if (!File::Delete(file_name.c_str()) &&
836 File::GetFileSize(file_name.c_str()) >= 0) {
837 LOG(WARNING) << "Failed to delete " << file_name << "; Will retry later.";
838 break;
839 }
840 segments_to_be_removed_.pop_front();
841 }
842}
843
844} // namespace hls
845} // namespace shaka
void AddBlock(uint64_t size_in_bytes, double duration)
virtual bool WriteToFile(const std::filesystem::path &file_path, bool event_to_vod_on_end_of_stream, bool end_stream)
virtual bool GetAC4ImsFlag() const
void SetStreamTypeForTesting(MediaPlaylistStreamType stream_type)
For testing only.
virtual void AddEncryptionInfo(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)
void SetCharacteristicsForTesting(const std::vector< std::string > &characteristics)
For testing only.
virtual void AddKeyFrame(int64_t timestamp, uint64_t start_byte_offset, uint64_t size)
virtual double GetFrameRate() const
void SetIndexForTesting(uint32_t index)
virtual uint64_t AvgBitrate() const
virtual bool GetDisplayResolution(uint32_t *width, uint32_t *height) const
virtual double GetLongestSegmentDuration() const
virtual int GetEC3JocComplexity() const
virtual void SetReferenceTime(const absl::Time &reference_time)
virtual uint64_t MaxBitrate() const
void SetLanguageForTesting(const std::string &language)
For testing only.
virtual int GetNumChannels() const
virtual std::string GetVideoRange() const
void SetCodecForTesting(const std::string &codec)
For testing only.
virtual void AddPlacementOpportunity()
const std::string & language() const
void AddEncryptionInfoForTesting(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)
For testing only.
virtual bool SetMediaInfo(const MediaInfo &media_info)
virtual void SetTargetDuration(int32_t target_duration)
virtual bool GetAC4CbiFlag() const
void SetForcedSubtitleForTesting(const bool forced_subtitle)
For testing only.
virtual void SetSampleDuration(int32_t sample_duration)
virtual void AddSegment(const std::string &file_name, int64_t start_time, int64_t duration, uint64_t start_byte_offset, uint64_t size)
All the methods that are virtual are virtual for mocking.
std::string LanguageToShortestForm(const std::string &language)