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