diff --git a/AUTHORS b/AUTHORS index a1c7831bcf2..bba8dfae8e2 100644 --- a/AUTHORS +++ b/AUTHORS @@ -30,3 +30,4 @@ Richard Eklycke Sanil Raut Sergio Ammirata The Chromium Authors <*@chromium.org> +cdnnow! <*@cdnnow.pro> diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 85f29dcd29c..0b45d53ed66 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -24,6 +24,7 @@ Alen Vrecko Anders Hasselqvist +Artem Bogdanov Bei Li Chun-da Chen Daniel CantarĂ­n diff --git a/docs/source/options/hls_options.rst b/docs/source/options/hls_options.rst index 8ec27f368df..bf538ba8652 100644 --- a/docs/source/options/hls_options.rst +++ b/docs/source/options/hls_options.rst @@ -76,6 +76,13 @@ HLS options The EXT-X-MEDIA-SEQUENCE documentation can be read here: https://tools.ietf.org/html/rfc8216#section-4.3.3.2. + +--hls_ext_x_program_date_time + + Adds an EXT-X-PROGRAM-DATE-TIME tag to every Media Segment, with fixed UTC + timezone. Time offset is the difference in milliseconds between packaging time + and live event time. + --hls_only=0|1 Optional. Defaults to 0 if not specified. If it is set to 1, indicates the diff --git a/packager/app/hls_flags.cc b/packager/app/hls_flags.cc index 2415ebe0e11..af92d298a08 100644 --- a/packager/app/hls_flags.cc +++ b/packager/app/hls_flags.cc @@ -30,3 +30,7 @@ DEFINE_int32(hls_media_sequence_number, "EXT-X-MEDIA-SEQUENCE value, which allows continuous media " "sequence across packager restarts. See #691 for more " "information about the reasoning of this and its use cases."); +DEFINE_int32(hls_ext_x_program_date_time, + INT32_MIN, + "Enable generation of EXT-X-PROGRAM-DATE-TIME tag with " + "a given time offset in milliseconds"); diff --git a/packager/app/hls_flags.h b/packager/app/hls_flags.h index c48a528e439..2ffe850c610 100644 --- a/packager/app/hls_flags.h +++ b/packager/app/hls_flags.h @@ -14,5 +14,6 @@ DECLARE_string(hls_base_url); DECLARE_string(hls_key_uri); DECLARE_string(hls_playlist_type); DECLARE_int32(hls_media_sequence_number); +DECLARE_int32(hls_ext_x_program_date_time); #endif // PACKAGER_APP_HLS_FLAGS_H_ diff --git a/packager/app/packager_main.cc b/packager/app/packager_main.cc index 9d5c8b7af4a..4027b1fa632 100644 --- a/packager/app/packager_main.cc +++ b/packager/app/packager_main.cc @@ -490,6 +490,11 @@ base::Optional GetPackagingParams() { hls_params.default_text_language = FLAGS_default_text_language; hls_params.media_sequence_number = FLAGS_hls_media_sequence_number; + if (FLAGS_hls_ext_x_program_date_time != INT32_MIN) { + hls_params.add_ext_x_program_date_time = true; + hls_params.packaging_time_offset_ms = FLAGS_hls_ext_x_program_date_time; + } + TestParams& test_params = packaging_params.test_params; test_params.dump_stream_info = FLAGS_dump_stream_info; test_params.inject_fake_clock = FLAGS_use_fake_clock_for_muxer; diff --git a/packager/hls/base/media_playlist.cc b/packager/hls/base/media_playlist.cc index 9bc585b8512..029aa2a7c5c 100644 --- a/packager/hls/base/media_playlist.cc +++ b/packager/hls/base/media_playlist.cc @@ -12,9 +12,11 @@ #include #include +#include "packager/app/hls_flags.h" #include "packager/base/logging.h" #include "packager/base/strings/string_number_conversions.h" #include "packager/base/strings/stringprintf.h" +#include "packager/base/time/time.h" #include "packager/file/file.h" #include "packager/hls/base/tag.h" #include "packager/media/base/language_utils.h" @@ -170,7 +172,8 @@ class SegmentInfoEntry : public HlsEntry { bool use_byte_range, uint64_t start_byte_offset, uint64_t segment_file_size, - uint64_t previous_segment_end_offset); + uint64_t previous_segment_end_offset, + const base::Time wall_time); std::string ToString() override; int64_t start_time() const { return start_time_; } @@ -179,6 +182,10 @@ class SegmentInfoEntry : public HlsEntry { duration_seconds_ = duration_seconds; } + const base::Time wall_time() { + return wall_time_; + } + private: SegmentInfoEntry(const SegmentInfoEntry&) = delete; SegmentInfoEntry& operator=(const SegmentInfoEntry&) = delete; @@ -190,6 +197,7 @@ class SegmentInfoEntry : public HlsEntry { const uint64_t start_byte_offset_; const uint64_t segment_file_size_; const uint64_t previous_segment_end_offset_; + const base::Time wall_time_; }; SegmentInfoEntry::SegmentInfoEntry(const std::string& file_name, @@ -198,7 +206,8 @@ SegmentInfoEntry::SegmentInfoEntry(const std::string& file_name, bool use_byte_range, uint64_t start_byte_offset, uint64_t segment_file_size, - uint64_t previous_segment_end_offset) + uint64_t previous_segment_end_offset, + const base::Time wall_time) : HlsEntry(HlsEntry::EntryType::kExtInf), file_name_(file_name), start_time_(start_time), @@ -206,7 +215,8 @@ SegmentInfoEntry::SegmentInfoEntry(const std::string& file_name, use_byte_range_(use_byte_range), start_byte_offset_(start_byte_offset), segment_file_size_(segment_file_size), - previous_segment_end_offset_(previous_segment_end_offset) {} + previous_segment_end_offset_(previous_segment_end_offset), + wall_time_(wall_time) {} std::string SegmentInfoEntry::ToString() { std::string result = base::StringPrintf("#EXTINF:%.3f,", duration_seconds_); @@ -408,7 +418,8 @@ void MediaPlaylist::AddSegment(const std::string& file_name, int64_t start_time, int64_t duration, uint64_t start_byte_offset, - uint64_t size) { + uint64_t size, + const base::Time reference_time) { if (stream_type_ == MediaPlaylistStreamType::kVideoIFramesOnly) { if (key_frames_.empty()) return; @@ -423,13 +434,13 @@ void MediaPlaylist::AddSegment(const std::string& file_name, : std::next(iter)->timestamp; AddSegmentInfoEntry(file_name, iter->timestamp, next_timestamp - iter->timestamp, - iter->start_byte_offset, iter->size); + iter->start_byte_offset, iter->size, reference_time); } key_frames_.clear(); return; } return AddSegmentInfoEntry(file_name, start_time, duration, start_byte_offset, - size); + size, reference_time); } void MediaPlaylist::AddKeyFrame(int64_t timestamp, @@ -477,8 +488,34 @@ bool MediaPlaylist::WriteToFile(const std::string& file_path) { media_info_, target_duration_, hls_params_.playlist_type, stream_type_, media_sequence_number_, discontinuity_sequence_number_); - for (const auto& entry : entries_) + bool need_ext_date_time = hls_params_.add_ext_x_program_date_time; + + for (const auto& entry : entries_) { + switch (entry->type()) { + case HlsEntry::EntryType::kExtInf: + if (need_ext_date_time) { + SegmentInfoEntry* segment_info = + reinterpret_cast(entry.get()); + base::Time::Exploded time; + segment_info->wall_time().UTCExplode(&time); + base::StringAppendF(&content, + "#EXT-X-PROGRAM-DATE-TIME:" + "%4d-%02d-%02dT%02d:%02d:%02d.%03dZ\n", + time.year, time.month, time.day_of_month, + time.hour, time.minute, time.second, + time.millisecond); + need_ext_date_time = false; + } + break; + case HlsEntry::EntryType::kExtDiscontinuity: + // we need to add a new program data time after a discontinuity + need_ext_date_time = hls_params_.add_ext_x_program_date_time; + break; + default: + break; + } base::StringAppendF(&content, "%s\n", entry->ToString().c_str()); + } if (hls_params_.playlist_type == HlsPlaylistType::kVod) { content += "#EXT-X-ENDLIST\n"; @@ -581,14 +618,15 @@ void MediaPlaylist::AddSegmentInfoEntry(const std::string& segment_file_name, int64_t start_time, int64_t duration, uint64_t start_byte_offset, - uint64_t size) { + uint64_t size, + const base::Time reference_time) { if (time_scale_ == 0) { LOG(WARNING) << "Timescale is not set and the duration for " << duration << " cannot be calculated. The output will be wrong."; entries_.emplace_back(new SegmentInfoEntry( segment_file_name, 0.0, 0.0, use_byte_range_, start_byte_offset, size, - previous_segment_end_offset_)); + previous_segment_end_offset_, reference_time)); return; } @@ -605,26 +643,27 @@ void MediaPlaylist::AddSegmentInfoEntry(const std::string& segment_file_name, std::max(longest_segment_duration_seconds_, segment_duration_seconds); bandwidth_estimator_.AddBlock(size, segment_duration_seconds); current_buffer_depth_ += segment_duration_seconds; - - if (!entries_.empty() && - entries_.back()->type() == HlsEntry::EntryType::kExtInf) { - const SegmentInfoEntry* segment_info = - static_cast(entries_.back().get()); - if (segment_info->start_time() > start_time) { - LOG(WARNING) - << "Insert a discontinuity tag after the segment with start time " - << segment_info->start_time() << " as the next segment starts at " - << start_time << "."; - entries_.emplace_back(new DiscontinuityEntry()); - } + const base::Time wall_time = reference_time + base::TimeDelta::FromSeconds(start_time / time_scale_); + + if (!entries_.empty() && start_time < last_segment_start_time_) { + LOG(WARNING) + << "Insert a discontinuity tag after the segment with start time " + << last_segment_start_time_ << " as the next segment starts at " + << start_time << "."; + entries_.emplace_back(new DiscontinuityEntry()); } + last_segment_start_time_ = start_time; entries_.emplace_back(new SegmentInfoEntry( segment_file_name, start_time, segment_duration_seconds, use_byte_range_, - start_byte_offset, size, previous_segment_end_offset_)); + start_byte_offset, size, previous_segment_end_offset_, wall_time)); previous_segment_end_offset_ = start_byte_offset + size - 1; } +uint64_t MediaPlaylist::LastSegmentStartTime() const { + return last_segment_start_time_; +} + void MediaPlaylist::AdjustLastSegmentInfoEntryDuration(int64_t next_timestamp) { if (time_scale_ == 0) return; diff --git a/packager/hls/base/media_playlist.h b/packager/hls/base/media_playlist.h index 2b419b99c2f..ed1c5a58ac9 100644 --- a/packager/hls/base/media_playlist.h +++ b/packager/hls/base/media_playlist.h @@ -13,6 +13,7 @@ #include #include "packager/base/macros.h" +#include "packager/base/time/time.h" #include "packager/hls/public/hls_params.h" #include "packager/mpd/base/bandwidth_estimator.h" #include "packager/mpd/base/media_info.pb.h" @@ -112,11 +113,14 @@ class MediaPlaylist { /// @param start_byte_offset is the offset of where the subsegment starts. /// This must be 0 if the whole segment is a subsegment. /// @param size is size in bytes. + /// @param reference_time reference time for wall clock time associated + /// to segments generation start. virtual void AddSegment(const std::string& file_name, int64_t start_time, int64_t duration, uint64_t start_byte_offset, - uint64_t size); + uint64_t size, + const base::Time reference_time); /// Keyframes must be added in order. It is also called before the containing /// segment being called. @@ -176,6 +180,9 @@ class MediaPlaylist { /// segments have been added. virtual double GetLongestSegmentDuration() const; + /// @return the start time of the last added segment + virtual uint64_t LastSegmentStartTime() const; + /// Set the target duration of this MediaPlaylist. /// In other words this is the value for EXT-X-TARGETDURATION. /// If this is not called before calling Write(), it will estimate the best @@ -227,7 +234,8 @@ class MediaPlaylist { int64_t start_time, int64_t duration, uint64_t start_byte_offset, - uint64_t size); + uint64_t size, + const base::Time reference_time); // Adjust the duration of the last SegmentInfoEntry to end on // |next_timestamp|. void AdjustLastSegmentInfoEntryDuration(int64_t next_timestamp); @@ -276,7 +284,10 @@ class MediaPlaylist { // Once a file is actually removed, it is removed from the list. std::list segments_to_be_removed_; - // Used by kVideoIFrameOnly playlists to track the i-frames (key frames). + // Store last segment start_time to be able to detect discontinuities + int64_t last_segment_start_time_ = 0; + + // Used by kVideoIFrameOnly playlists to track the i-frames (key frames). struct KeyFrameInfo { int64_t timestamp; uint64_t start_byte_offset; diff --git a/packager/hls/base/media_playlist_unittest.cc b/packager/hls/base/media_playlist_unittest.cc index 1dce438a4f4..d889aab5dea 100644 --- a/packager/hls/base/media_playlist_unittest.cc +++ b/packager/hls/base/media_playlist_unittest.cc @@ -7,6 +7,7 @@ #include #include +#include "packager/base/time/time.h" #include "packager/base/strings/stringprintf.h" #include "packager/file/file.h" #include "packager/file/file_closer.h" @@ -30,6 +31,19 @@ const double kTimeShiftBufferDepth = 20; const uint64_t kTimeScale = 90000; const uint64_t kMBytes = 1000000; const uint64_t kZeroByteOffset = 0; +const base::Time kRefTime = base::Time(); + +// Converts timestamp to ISO/IEC 8601:2004 date/time representation, +// such as YYYY-MM-DDThh:mm:ss.SSSZ +std::string timeToString(base::Time ref_time) { + const char date_time_tmpl[] = "%4d-%02d-%02dT%02d:%02d:%02d.%03dZ"; + base::Time::Exploded time; + ref_time.UTCExplode(&time); + return base::StringPrintf( + date_time_tmpl, + time.year, time.month, time.day_of_month, + time.hour, time.minute, time.second, time.millisecond); +} MATCHER_P(MatchesString, expected_string, "") { const std::string arg_string(static_cast(arg)); @@ -128,7 +142,7 @@ TEST_F(MediaPlaylistMultiSegmentTest, SetMediaInfo) { // Verify that AddSegment works (not crash). TEST_F(MediaPlaylistMultiSegmentTest, AddSegment) { ASSERT_TRUE(media_playlist_->SetMediaInfo(valid_video_media_info_)); - media_playlist_->AddSegment("file1.ts", 900000, 0, kZeroByteOffset, 1000000); + media_playlist_->AddSegment("file1.ts", 900000, 0, kZeroByteOffset, 1000000, kRefTime); } // Verify that it returns the display resolution. @@ -214,9 +228,67 @@ TEST_F(MediaPlaylistSingleSegmentTest, AddSegmentByteRange) { ASSERT_TRUE(media_playlist_->SetMediaInfo(valid_video_media_info_)); media_playlist_->AddSegment("file.mp4", 0, 10 * kTimeScale, 1000, - 1 * kMBytes); + 1 * kMBytes, kRefTime); media_playlist_->AddSegment("file.mp4", 10 * kTimeScale, 10 * kTimeScale, - 1001000, 2 * kMBytes); + 1001000, 2 * kMBytes, kRefTime); + + const char kMemoryFilePath[] = "memory://media.m3u8"; + EXPECT_TRUE(media_playlist_->WriteToFile(kMemoryFilePath)); + ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput); +} + +TEST_F(MediaPlaylistSingleSegmentTest, AddSegmentByteRangeWithExtXProgramDateTime) { + hls_params_.add_ext_x_program_date_time = true; + base::Time reference_time; + ASSERT_TRUE(base::Time::FromUTCString("2020-09-11 13:37:00.000 UTC", + &reference_time)); + + media_playlist_.reset(new MediaPlaylist(hls_params_, default_file_name_, + default_name_, default_group_id_)); + + valid_video_media_info_.set_media_file_url("file.mp4"); + valid_video_media_info_.mutable_init_range()->set_begin(0); + valid_video_media_info_.mutable_init_range()->set_end(500); + + ASSERT_TRUE(media_playlist_->SetMediaInfo(valid_video_media_info_)); + + uint64_t start_time = 60 * kTimeScale; + uint64_t duration = 10 * kTimeScale; + base::Time expected_date = reference_time + base::TimeDelta::FromSeconds(60); + + media_playlist_->AddSegment("file.mp4", start_time, duration, 1000, + 1 * kMBytes, reference_time); + media_playlist_->AddSegment("file.mp4", start_time + duration, duration, + 1001000, 2 * kMBytes, reference_time); + + // create a discontinuity + base::Time expected_date2 = expected_date + base::TimeDelta::FromSeconds(60); + media_playlist_->AddSegment("file2.mp4", 0, duration, + 0, 1 * kMBytes, expected_date2); + + std::string kExpectedOutput = base::StringPrintf( + "#EXTM3U\n" + "#EXT-X-VERSION:6\n" + "## Generated with https://github.com/google/shaka-packager version " + "test\n" + "#EXT-X-TARGETDURATION:10\n" + "#EXT-X-PLAYLIST-TYPE:VOD\n" + "#EXT-X-MAP:URI=\"file.mp4\",BYTERANGE=\"501@0\"\n" + "#EXT-X-PROGRAM-DATE-TIME:%s\n" + "#EXTINF:10.000,\n" + "#EXT-X-BYTERANGE:1000000@1000\n" + "file.mp4\n" + "#EXTINF:10.000,\n" + "#EXT-X-BYTERANGE:2000000\n" + "file.mp4\n" + "#EXT-X-DISCONTINUITY\n" + "#EXT-X-PROGRAM-DATE-TIME:%s\n" + "#EXTINF:10.000,\n" + "#EXT-X-BYTERANGE:1000000@0\n" + "file2.mp4\n" + "#EXT-X-ENDLIST\n", + timeToString(expected_date).c_str(), + timeToString(expected_date2).c_str()); const char kMemoryFilePath[] = "memory://media.m3u8"; EXPECT_TRUE(media_playlist_->WriteToFile(kMemoryFilePath)); @@ -261,9 +333,9 @@ TEST_F(MediaPlaylistMultiSegmentTest, GetBitrateFromSegments) { ASSERT_TRUE(media_playlist_->SetMediaInfo(valid_video_media_info_)); media_playlist_->AddSegment("file1.ts", 0, 10 * kTimeScale, kZeroByteOffset, - kMBytes); + kMBytes, kRefTime); media_playlist_->AddSegment("file2.ts", 10 * kTimeScale, 20 * kTimeScale, - kZeroByteOffset, 5 * kMBytes); + kZeroByteOffset, 5 * kMBytes, kRefTime); EXPECT_EQ(2000000u, media_playlist_->MaxBitrate()); EXPECT_EQ(1600000u, media_playlist_->AvgBitrate()); @@ -273,11 +345,11 @@ TEST_F(MediaPlaylistMultiSegmentTest, GetLongestSegmentDuration) { ASSERT_TRUE(media_playlist_->SetMediaInfo(valid_video_media_info_)); media_playlist_->AddSegment("file1.ts", 0, 10 * kTimeScale, kZeroByteOffset, - kMBytes); + kMBytes, kRefTime); media_playlist_->AddSegment("file2.ts", 10 * kTimeScale, 30 * kTimeScale, - kZeroByteOffset, 5 * kMBytes); + kZeroByteOffset, 5 * kMBytes, kRefTime); media_playlist_->AddSegment("file3.ts", 40 * kTimeScale, 14 * kTimeScale, - kZeroByteOffset, 3 * kMBytes); + kZeroByteOffset, 3 * kMBytes, kRefTime); EXPECT_NEAR(30.0, media_playlist_->GetLongestSegmentDuration(), 0.01); } @@ -304,9 +376,9 @@ TEST_F(MediaPlaylistMultiSegmentTest, WriteToFileWithSegments) { ASSERT_TRUE(media_playlist_->SetMediaInfo(valid_video_media_info_)); media_playlist_->AddSegment("file1.ts", 0, 10 * kTimeScale, kZeroByteOffset, - kMBytes); + kMBytes, kRefTime); media_playlist_->AddSegment("file2.ts", 10 * kTimeScale, 30 * kTimeScale, - kZeroByteOffset, 5 * kMBytes); + kZeroByteOffset, 5 * kMBytes, kRefTime); const char kExpectedOutput[] = "#EXTM3U\n" "#EXT-X-VERSION:6\n" @@ -331,10 +403,10 @@ TEST_F(MediaPlaylistMultiSegmentTest, ASSERT_TRUE(media_playlist_->SetMediaInfo(valid_video_media_info_)); media_playlist_->AddSegment("file1.ts", 0, 10 * kTimeScale, kZeroByteOffset, - kMBytes); + kMBytes, kRefTime); media_playlist_->AddPlacementOpportunity(); media_playlist_->AddSegment("file2.ts", 10 * kTimeScale, 30 * kTimeScale, - kZeroByteOffset, 5 * kMBytes); + kZeroByteOffset, 5 * kMBytes, kRefTime); const char kExpectedOutput[] = "#EXTM3U\n" "#EXT-X-VERSION:6\n" @@ -362,9 +434,9 @@ TEST_F(MediaPlaylistMultiSegmentTest, WriteToFileWithEncryptionInfo) { MediaPlaylist::EncryptionMethod::kSampleAes, "http://example.com", "", "0x12345678", "com.widevine", "1/2/4"); media_playlist_->AddSegment("file1.ts", 0, 10 * kTimeScale, kZeroByteOffset, - kMBytes); + kMBytes, kRefTime); media_playlist_->AddSegment("file2.ts", 10 * kTimeScale, 30 * kTimeScale, - kZeroByteOffset, 5 * kMBytes); + kZeroByteOffset, 5 * kMBytes, kRefTime); const char kExpectedOutput[] = "#EXTM3U\n" "#EXT-X-VERSION:6\n" @@ -394,9 +466,9 @@ TEST_F(MediaPlaylistMultiSegmentTest, WriteToFileWithEncryptionInfoEmptyIv) { MediaPlaylist::EncryptionMethod::kSampleAes, "http://example.com", "", "", "com.widevine", ""); media_playlist_->AddSegment("file1.ts", 0, 10 * kTimeScale, kZeroByteOffset, - kMBytes); + kMBytes, kRefTime); media_playlist_->AddSegment("file2.ts", 10 * kTimeScale, 30 * kTimeScale, - kZeroByteOffset, 5 * kMBytes); + kZeroByteOffset, 5 * kMBytes, kRefTime); const char kExpectedOutput[] = "#EXTM3U\n" "#EXT-X-VERSION:6\n" @@ -423,13 +495,13 @@ TEST_F(MediaPlaylistMultiSegmentTest, WriteToFileWithClearLead) { ASSERT_TRUE(media_playlist_->SetMediaInfo(valid_video_media_info_)); media_playlist_->AddSegment("file1.ts", 0, 10 * kTimeScale, kZeroByteOffset, - kMBytes); + kMBytes, kRefTime); media_playlist_->AddEncryptionInfo( MediaPlaylist::EncryptionMethod::kSampleAes, "http://example.com", "", "0x12345678", "com.widevine", "1/2/4"); media_playlist_->AddSegment("file2.ts", 10 * kTimeScale, 30 * kTimeScale, - kZeroByteOffset, 5 * kMBytes); + kZeroByteOffset, 5 * kMBytes, kRefTime); const char kExpectedOutput[] = "#EXTM3U\n" "#EXT-X-VERSION:6\n" @@ -560,9 +632,9 @@ TEST_F(MediaPlaylistMultiSegmentTest, InitSegment) { ASSERT_TRUE(media_playlist_->SetMediaInfo(valid_video_media_info_)); media_playlist_->AddSegment("file1.mp4", 0, 10 * kTimeScale, kZeroByteOffset, - kMBytes); + kMBytes, kRefTime); media_playlist_->AddSegment("file2.mp4", 10 * kTimeScale, 30 * kTimeScale, - kZeroByteOffset, 5 * kMBytes); + kZeroByteOffset, 5 * kMBytes, kRefTime); const char kExpectedOutput[] = "#EXTM3U\n" @@ -593,9 +665,9 @@ TEST_F(MediaPlaylistMultiSegmentTest, SampleAesCenc) { "0x12345678", "com.widevine", "1/2/4"); media_playlist_->AddSegment("file1.ts", 0, 10 * kTimeScale, kZeroByteOffset, - kMBytes); + kMBytes, kRefTime); media_playlist_->AddSegment("file2.ts", 10 * kTimeScale, 30 * kTimeScale, - kZeroByteOffset, 5 * kMBytes); + kZeroByteOffset, 5 * kMBytes, kRefTime); const char kExpectedOutput[] = "#EXTM3U\n" "#EXT-X-VERSION:6\n" @@ -629,9 +701,9 @@ TEST_F(MediaPlaylistMultiSegmentTest, MultipleEncryptionInfo) { "0xfedc", "0x12345678", "com.widevine.someother", "1"); media_playlist_->AddSegment("file1.ts", 0, 10 * kTimeScale, kZeroByteOffset, - kMBytes); + kMBytes, kRefTime); media_playlist_->AddSegment("file2.ts", 10 * kTimeScale, 30 * kTimeScale, - kZeroByteOffset, 5 * kMBytes); + kZeroByteOffset, 5 * kMBytes, kRefTime); const char kExpectedOutput[] = "#EXTM3U\n" "#EXT-X-VERSION:6\n" @@ -667,9 +739,9 @@ TEST_F(LiveMediaPlaylistTest, Basic) { ASSERT_TRUE(media_playlist_->SetMediaInfo(valid_video_media_info_)); media_playlist_->AddSegment("file1.ts", 0, 10 * kTimeScale, kZeroByteOffset, - kMBytes); + kMBytes, kRefTime); media_playlist_->AddSegment("file2.ts", 10 * kTimeScale, 20 * kTimeScale, - kZeroByteOffset, 2 * kMBytes); + kZeroByteOffset, 2 * kMBytes, kRefTime); const char kExpectedOutput[] = "#EXTM3U\n" "#EXT-X-VERSION:6\n" @@ -690,11 +762,11 @@ TEST_F(LiveMediaPlaylistTest, TimeShifted) { ASSERT_TRUE(media_playlist_->SetMediaInfo(valid_video_media_info_)); media_playlist_->AddSegment("file1.ts", 0, 10 * kTimeScale, kZeroByteOffset, - kMBytes); + kMBytes, kRefTime); media_playlist_->AddSegment("file2.ts", 10 * kTimeScale, 20 * kTimeScale, - kZeroByteOffset, 2 * kMBytes); + kZeroByteOffset, 2 * kMBytes, kRefTime); media_playlist_->AddSegment("file3.ts", 30 * kTimeScale, 20 * kTimeScale, - kZeroByteOffset, 2 * kMBytes); + kZeroByteOffset, 2 * kMBytes, kRefTime); const char kExpectedOutput[] = "#EXTM3U\n" "#EXT-X-VERSION:6\n" @@ -723,11 +795,11 @@ TEST_F(LiveMediaPlaylistTest, TimeShiftedWithEncryptionInfo) { "0xfedc", "0x12345678", "com.widevine.someother", "1"); media_playlist_->AddSegment("file1.ts", 0, 10 * kTimeScale, kZeroByteOffset, - kMBytes); + kMBytes, kRefTime); media_playlist_->AddSegment("file2.ts", 10 * kTimeScale, 20 * kTimeScale, - kZeroByteOffset, 2 * kMBytes); + kZeroByteOffset, 2 * kMBytes, kRefTime); media_playlist_->AddSegment("file3.ts", 30 * kTimeScale, 20 * kTimeScale, - kZeroByteOffset, 2 * kMBytes); + kZeroByteOffset, 2 * kMBytes, kRefTime); const char kExpectedOutput[] = "#EXTM3U\n" "#EXT-X-VERSION:6\n" @@ -756,7 +828,7 @@ TEST_F(LiveMediaPlaylistTest, TimeShiftedWithEncryptionInfoShifted) { ASSERT_TRUE(media_playlist_->SetMediaInfo(valid_video_media_info_)); media_playlist_->AddSegment("file1.ts", 0, 10 * kTimeScale, kZeroByteOffset, - kMBytes); + kMBytes, kRefTime); media_playlist_->AddEncryptionInfo( MediaPlaylist::EncryptionMethod::kSampleAes, "http://example.com", "", @@ -766,7 +838,7 @@ TEST_F(LiveMediaPlaylistTest, TimeShiftedWithEncryptionInfoShifted) { "0xfedc", "0x12345678", "com.widevine.someother", "1"); media_playlist_->AddSegment("file2.ts", 10 * kTimeScale, 20 * kTimeScale, - kZeroByteOffset, 2 * kMBytes); + kZeroByteOffset, 2 * kMBytes, kRefTime); media_playlist_->AddEncryptionInfo( MediaPlaylist::EncryptionMethod::kSampleAes, "http://example.com", "", @@ -776,7 +848,7 @@ TEST_F(LiveMediaPlaylistTest, TimeShiftedWithEncryptionInfoShifted) { "0xfedd", "0x22345678", "com.widevine.someother", "1"); media_playlist_->AddSegment("file3.ts", 30 * kTimeScale, 20 * kTimeScale, - kZeroByteOffset, 2 * kMBytes); + kZeroByteOffset, 2 * kMBytes, kRefTime); media_playlist_->AddEncryptionInfo( MediaPlaylist::EncryptionMethod::kSampleAes, "http://example.com", "", @@ -786,7 +858,7 @@ TEST_F(LiveMediaPlaylistTest, TimeShiftedWithEncryptionInfoShifted) { "0xfede", "0x32345678", "com.widevine.someother", "1"); media_playlist_->AddSegment("file4.ts", 50 * kTimeScale, 20 * kTimeScale, - kZeroByteOffset, 2 * kMBytes); + kZeroByteOffset, 2 * kMBytes, kRefTime); const char kExpectedOutput[] = "#EXTM3U\n" "#EXT-X-VERSION:6\n" @@ -829,9 +901,9 @@ TEST_F(EventMediaPlaylistTest, Basic) { ASSERT_TRUE(media_playlist_->SetMediaInfo(valid_video_media_info_)); media_playlist_->AddSegment("file1.ts", 0, 10 * kTimeScale, kZeroByteOffset, - kMBytes); + kMBytes, kRefTime); media_playlist_->AddSegment("file2.ts", 10 * kTimeScale, 20 * kTimeScale, - kZeroByteOffset, 2 * kMBytes); + kZeroByteOffset, 2 * kMBytes, kRefTime); const char kExpectedOutput[] = "#EXTM3U\n" "#EXT-X-VERSION:6\n" @@ -871,11 +943,11 @@ TEST_F(IFrameMediaPlaylistTest, SingleSegment) { media_playlist_->AddKeyFrame(0, 1000, 2345); media_playlist_->AddKeyFrame(2 * kTimeScale, 5000, 6345); media_playlist_->AddSegment("file.mp4", 0, 10 * kTimeScale, kZeroByteOffset, - kMBytes); + kMBytes, kRefTime); media_playlist_->AddKeyFrame(11 * kTimeScale, kMBytes + 1000, 2345); media_playlist_->AddKeyFrame(15 * kTimeScale, kMBytes + 3345, 12345); media_playlist_->AddSegment("file.mp4", 10 * kTimeScale, 10 * kTimeScale, - 1001000, 2 * kMBytes); + 1001000, 2 * kMBytes, kRefTime); const char kExpectedOutput[] = "#EXTM3U\n" @@ -905,6 +977,58 @@ TEST_F(IFrameMediaPlaylistTest, SingleSegment) { ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput); } +TEST_F(IFrameMediaPlaylistTest, SingleSegmentWithExtXProgramDateTime) { + hls_params_.add_ext_x_program_date_time = true; + base::Time reference_time; + ASSERT_TRUE(base::Time::FromUTCString("2020-09-11 13:37:00.000 UTC", + &reference_time)); + media_playlist_.reset(new MediaPlaylist(hls_params_, default_file_name_, + default_name_, default_group_id_)); + + valid_video_media_info_.set_media_file_url("file.mp4"); + valid_video_media_info_.mutable_init_range()->set_begin(0); + valid_video_media_info_.mutable_init_range()->set_end(500); + + ASSERT_TRUE(media_playlist_->SetMediaInfo(valid_video_media_info_)); + media_playlist_->AddKeyFrame(0, 1000, 2345); + media_playlist_->AddKeyFrame(2 * kTimeScale, 5000, 6345); + media_playlist_->AddSegment("file.mp4", 0, 10 * kTimeScale, kZeroByteOffset, + kMBytes, reference_time); + media_playlist_->AddKeyFrame(11 * kTimeScale, kMBytes + 1000, 2345); + media_playlist_->AddKeyFrame(15 * kTimeScale, kMBytes + 3345, 12345); + media_playlist_->AddSegment("file.mp4", 10 * kTimeScale, 10 * kTimeScale, + 1001000, 2 * kMBytes, reference_time); + + const std::string kExpectedOutput = base::StringPrintf( + "#EXTM3U\n" + "#EXT-X-VERSION:6\n" + "## Generated with https://github.com/google/shaka-packager version " + "test\n" + "#EXT-X-TARGETDURATION:9\n" + "#EXT-X-PLAYLIST-TYPE:VOD\n" + "#EXT-X-I-FRAMES-ONLY\n" + "#EXT-X-MAP:URI=\"file.mp4\",BYTERANGE=\"501@0\"\n" + "#EXT-X-PROGRAM-DATE-TIME:%s\n" + "#EXTINF:2.000,\n" + "#EXT-X-BYTERANGE:2345@1000\n" + "file.mp4\n" + "#EXTINF:9.000,\n" + "#EXT-X-BYTERANGE:6345@5000\n" + "file.mp4\n" + "#EXTINF:4.000,\n" + "#EXT-X-BYTERANGE:2345@1001000\n" + "file.mp4\n" + "#EXTINF:5.000,\n" + "#EXT-X-BYTERANGE:12345\n" + "file.mp4\n" + "#EXT-X-ENDLIST\n", + timeToString(reference_time).c_str()); + + const char kMemoryFilePath[] = "memory://media.m3u8"; + EXPECT_TRUE(media_playlist_->WriteToFile(kMemoryFilePath)); + ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput); +} + TEST_F(IFrameMediaPlaylistTest, MultiSegment) { valid_video_media_info_.set_reference_time_scale(90000); valid_video_media_info_.set_segment_template_url("file$Number$.ts"); @@ -913,11 +1037,11 @@ TEST_F(IFrameMediaPlaylistTest, MultiSegment) { media_playlist_->AddKeyFrame(0, 1000, 2345); media_playlist_->AddKeyFrame(2 * kTimeScale, 5000, 6345); media_playlist_->AddSegment("file1.ts", 0, 10 * kTimeScale, kZeroByteOffset, - kMBytes); + kMBytes, kRefTime); media_playlist_->AddKeyFrame(11 * kTimeScale, 1000, 2345); media_playlist_->AddKeyFrame(15 * kTimeScale, 3345, 12345); media_playlist_->AddSegment("file2.ts", 10 * kTimeScale, 30 * kTimeScale, - kZeroByteOffset, 5 * kMBytes); + kZeroByteOffset, 5 * kMBytes, kRefTime); const char kExpectedOutput[] = "#EXTM3U\n" @@ -954,12 +1078,12 @@ TEST_F(IFrameMediaPlaylistTest, MultiSegmentWithPlacementOpportunity) { media_playlist_->AddKeyFrame(0, 1000, 2345); media_playlist_->AddKeyFrame(2 * kTimeScale, 5000, 6345); media_playlist_->AddSegment("file1.ts", 0, 10 * kTimeScale, kZeroByteOffset, - kMBytes); + kMBytes, kRefTime); media_playlist_->AddPlacementOpportunity(); media_playlist_->AddKeyFrame(11 * kTimeScale, 1000, 2345); media_playlist_->AddKeyFrame(15 * kTimeScale, 3345, 12345); media_playlist_->AddSegment("file2.ts", 10 * kTimeScale, 30 * kTimeScale, - kZeroByteOffset, 5 * kMBytes); + kZeroByteOffset, 5 * kMBytes, kRefTime); const char kExpectedOutput[] = "#EXTM3U\n" @@ -1052,7 +1176,7 @@ class MediaPlaylistDeleteSegmentsTest TEST_P(MediaPlaylistDeleteSegmentsTest, NoSegmentsDeletedInitially) { for (int i = 0; i < kMaxNumSegmentsAvailable; ++i) { media_playlist_->AddSegment(kIgnoredSegmentName, GetTime(i), kDuration, - kZeroByteOffset, kMBytes); + kZeroByteOffset, kMBytes, kRefTime); } for (int i = 0; i < kMaxNumSegmentsAvailable; ++i) { EXPECT_FALSE(SegmentDeleted(GetSegmentName(i))); @@ -1062,7 +1186,7 @@ TEST_P(MediaPlaylistDeleteSegmentsTest, NoSegmentsDeletedInitially) { TEST_P(MediaPlaylistDeleteSegmentsTest, OneSegmentDeleted) { for (int i = 0; i <= kMaxNumSegmentsAvailable; ++i) { media_playlist_->AddSegment(kIgnoredSegmentName, GetTime(i), kDuration, - kZeroByteOffset, kMBytes); + kZeroByteOffset, kMBytes, kRefTime); } EXPECT_FALSE(SegmentDeleted(GetSegmentName(1))); EXPECT_TRUE(SegmentDeleted(GetSegmentName(0))); @@ -1072,7 +1196,7 @@ TEST_P(MediaPlaylistDeleteSegmentsTest, ManySegments) { int many_segments = 50; for (int i = 0; i < many_segments; ++i) { media_playlist_->AddSegment(kIgnoredSegmentName, GetTime(i), kDuration, - kZeroByteOffset, kMBytes); + kZeroByteOffset, kMBytes, kRefTime); } const int last_available_segment_index = many_segments - kMaxNumSegmentsAvailable; diff --git a/packager/hls/base/mock_media_playlist.h b/packager/hls/base/mock_media_playlist.h index 11a350842a6..0194d384f3c 100644 --- a/packager/hls/base/mock_media_playlist.h +++ b/packager/hls/base/mock_media_playlist.h @@ -24,12 +24,13 @@ class MockMediaPlaylist : public MediaPlaylist { ~MockMediaPlaylist() override; MOCK_METHOD1(SetMediaInfo, bool(const MediaInfo& media_info)); - MOCK_METHOD5(AddSegment, + MOCK_METHOD6(AddSegment, void(const std::string& file_name, int64_t start_time, int64_t duration, uint64_t start_byte_offset, - uint64_t size)); + uint64_t size, + const base::Time reference_time)); MOCK_METHOD3(AddKeyFrame, void(int64_t timestamp, uint64_t start_byte_offset, diff --git a/packager/hls/base/simple_hls_notifier.cc b/packager/hls/base/simple_hls_notifier.cc index 0867631e335..9b90c03a6d3 100644 --- a/packager/hls/base/simple_hls_notifier.cc +++ b/packager/hls/base/simple_hls_notifier.cc @@ -373,8 +373,27 @@ bool SimpleHlsNotifier::NotifyNewSegment(uint32_t stream_id, const std::string& segment_url = GenerateSegmentUrl(segment_name, hls_params().base_url, master_playlist_dir_, media_playlist->file_name()); + + // Check if we need to set reference_time_ + // This need to be adjusted on discontinuity + if (hls_params().add_ext_x_program_date_time) { + if (reference_time_.is_null() || start_time < media_playlist->LastSegmentStartTime()) { + base::Time adjusted_time = base::Time::Now() + + base::TimeDelta::FromMilliseconds(hls_params().packaging_time_offset_ms); + if (reference_time_.is_null()) { + LOG(INFO) + << "Init reference time: " << adjusted_time; + } else { + LOG(WARNING) + << "Reset reference time on discontinuity: " << adjusted_time + << " prev: " << media_playlist->LastSegmentStartTime() << " cur: " << start_time; + } + reference_time_ = adjusted_time; + } + } + media_playlist->AddSegment(segment_url, start_time, duration, - start_byte_offset, size); + start_byte_offset, size, reference_time_); // Update target duration. uint32_t longest_segment_duration = diff --git a/packager/hls/base/simple_hls_notifier.h b/packager/hls/base/simple_hls_notifier.h index abfdc081855..5242c6a4833 100644 --- a/packager/hls/base/simple_hls_notifier.h +++ b/packager/hls/base/simple_hls_notifier.h @@ -93,6 +93,9 @@ class SimpleHlsNotifier : public HlsNotifier { base::Lock lock_; + // Reference time to allow computation of segments associated wall time + base::Time reference_time_ = base::Time(); + DISALLOW_COPY_AND_ASSIGN(SimpleHlsNotifier); }; diff --git a/packager/hls/base/simple_hls_notifier_unittest.cc b/packager/hls/base/simple_hls_notifier_unittest.cc index c4981132641..01a859da556 100644 --- a/packager/hls/base/simple_hls_notifier_unittest.cc +++ b/packager/hls/base/simple_hls_notifier_unittest.cc @@ -43,6 +43,7 @@ const char kFairPlayKeyUri[] = "skd://www.license.com/getkey?key_id=testing"; const char kIdentityKeyUri[] = "https://www.license.com/getkey?key_id=testing"; const HlsPlaylistType kVodPlaylist = HlsPlaylistType::kVod; const HlsPlaylistType kLivePlaylist = HlsPlaylistType::kLive; +const base::Time kRefTime = base::Time(); class MockMasterPlaylist : public MasterPlaylist { public: @@ -215,7 +216,7 @@ TEST_F(SimpleHlsNotifierTest, NotifyNewSegment) { const std::string segment_name = "segmentname"; EXPECT_CALL(*mock_media_playlist, AddSegment(StrEq(kTestPrefix + segment_name), kStartTime, - kDuration, 203, kSize)); + kDuration, 203, kSize, kRefTime)); const double kLongestSegmentDuration = 11.3; const uint32_t kTargetDuration = 12; // ceil(kLongestSegmentDuration). @@ -435,7 +436,7 @@ TEST_P(SimpleHlsNotifierRebaseUrlTest, Test) { if (!test_data_.expected_segment_url.empty()) { EXPECT_CALL(*mock_media_playlist, - AddSegment(test_data_.expected_segment_url, _, _, _, _)); + AddSegment(test_data_.expected_segment_url, _, _, _, _, kRefTime)); } EXPECT_CALL(*factory, CreateMock(_, StrEq(test_data_.expected_relative_playlist_path), @@ -544,7 +545,7 @@ TEST_P(LiveOrEventSimpleHlsNotifierTest, NotifyNewSegment) { const std::string segment_name = "segmentname"; EXPECT_CALL(*mock_media_playlist, AddSegment(StrEq(kTestPrefix + segment_name), kStartTime, - kDuration, _, kSize)); + kDuration, _, kSize, kRefTime)); const double kLongestSegmentDuration = 11.3; const uint32_t kTargetDuration = 12; // ceil(kLongestSegmentDuration). @@ -617,7 +618,7 @@ TEST_P(LiveOrEventSimpleHlsNotifierTest, NotifyNewSegmentsWithMultipleStreams) { EXPECT_TRUE(notifier.NotifyNewStream(media_info, "playlist2.m3u8", "name", "groupid", &stream_id2)); - EXPECT_CALL(*mock_media_playlist1, AddSegment(_, _, _, _, _)).Times(1); + EXPECT_CALL(*mock_media_playlist1, AddSegment(_, _, _, _, _, kRefTime)).Times(1); const double kLongestSegmentDuration = 11.3; const uint32_t kTargetDuration = 12; // ceil(kLongestSegmentDuration). EXPECT_CALL(*mock_media_playlist1, GetLongestSegmentDuration()) @@ -648,7 +649,7 @@ TEST_P(LiveOrEventSimpleHlsNotifierTest, NotifyNewSegmentsWithMultipleStreams) { EXPECT_TRUE(notifier.NotifyNewSegment(stream_id1, "segment_name", kStartTime, kDuration, 0, kSize)); - EXPECT_CALL(*mock_media_playlist2, AddSegment(_, _, _, _, _)).Times(1); + EXPECT_CALL(*mock_media_playlist2, AddSegment(_, _, _, _, _, kRefTime)).Times(1); EXPECT_CALL(*mock_media_playlist2, GetLongestSegmentDuration()) .WillOnce(Return(kLongestSegmentDuration)); // Not updating other playlists as target duration does not change. diff --git a/packager/hls/public/hls_params.h b/packager/hls/public/hls_params.h index d1f7200f439..a3b961bc4b2 100644 --- a/packager/hls/public/hls_params.h +++ b/packager/hls/public/hls_params.h @@ -62,6 +62,9 @@ struct HlsParams { /// Custom EXT-X-MEDIA-SEQUENCE value to allow continuous media playback /// across packager restarts. See #691 for details. uint32_t media_sequence_number = 0; + /// Enable generation of EXT-X-PROGRAM-DATE-TIME tag. + bool add_ext_x_program_date_time = false; + uint32_t packaging_time_offset_ms = 0; }; } // namespace shaka