From 76eb2c1575d4c8b32782292dc5216c444a6f2b27 Mon Sep 17 00:00:00 2001 From: Marcus Wichelmann Date: Thu, 15 Feb 2024 21:06:06 +0100 Subject: [PATCH] feat: Add support for the EXT-X-START tag (#973) adds an optional `--hls_start_time_offset` parameter, that, when used, adds `EXT-X-START` tags to the HLS media playlists. The EXT-X-START tag allows to specify the location where the player starts playing, either from start (positive value) or from end (negative value). This is especially useful in case of livestreams where this tag can be used to set the target latency of the playback. Reference: https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.5.2 The RFC says, that the `EXT-X-START` tag could also be added to the master playlist, but my tests have shown that most players only respect it when it's in the media playlists. Fixes #970 --------- Co-authored-by: Joey Parrish --- docs/source/options/hls_options.rst | 8 ++ include/packager/hls_params.h | 7 ++ packager/app/hls_flags.cc | 11 +++ packager/app/hls_flags.h | 1 + packager/app/packager_main.cc | 1 + packager/hls/base/media_playlist.cc | 11 ++- packager/hls/base/media_playlist_unittest.cc | 88 ++++++++++++++++++++ 7 files changed, 125 insertions(+), 2 deletions(-) diff --git a/docs/source/options/hls_options.rst b/docs/source/options/hls_options.rst index f3c2b9830b..8199195d26 100644 --- a/docs/source/options/hls_options.rst +++ b/docs/source/options/hls_options.rst @@ -76,6 +76,14 @@ HLS options The EXT-X-MEDIA-SEQUENCE documentation can be read here: https://tools.ietf.org/html/rfc8216#section-4.3.3.2. +--hls_start_time_offset + + Sets EXT-X-START on the media playlists to specify the preferred point + at wich the player should start playing. + A positive number indicates a time offset from the beginning of the playlist. + A negative number indicates a negative time offset from the end of the + last media segment in the playlist. + --hls_only=0|1 Optional. Defaults to 0 if not specified. If it is set to 1, indicates the diff --git a/include/packager/hls_params.h b/include/packager/hls_params.h index a476beb412..e8c86aa906 100644 --- a/include/packager/hls_params.h +++ b/include/packager/hls_params.h @@ -8,6 +8,7 @@ #define PACKAGER_PUBLIC_HLS_PARAMS_H_ #include +#include #include namespace shaka { @@ -63,6 +64,12 @@ 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; + /// Sets EXT-X-START on the media playlists to specify the preferred point + /// at wich the player should start playing. + /// A positive number indicates a time offset from the beginning of the + /// playlist. A negative number indicates a negative time offset from the end + /// of the last media segment in the playlist. + std::optional start_time_offset; }; } // namespace shaka diff --git a/packager/app/hls_flags.cc b/packager/app/hls_flags.cc index ac301722cd..653830ca4f 100644 --- a/packager/app/hls_flags.cc +++ b/packager/app/hls_flags.cc @@ -6,6 +6,8 @@ #include +#include + ABSL_FLAG(std::string, hls_master_playlist_output, "", @@ -35,3 +37,12 @@ ABSL_FLAG(int32_t, "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."); +ABSL_FLAG(std::optional, + hls_start_time_offset, + std::nullopt, + "Floating-point number. Sets EXT-X-START on the media playlists " + "to specify the preferred point at wich the player should start " + "playing. A positive number indicates a time offset from the " + "beginning of the playlist. A negative number indicates a " + "negative time offset from the end of the last media segment " + "in the playlist."); diff --git a/packager/app/hls_flags.h b/packager/app/hls_flags.h index 09c5a3f6f8..5bb7ae754a 100644 --- a/packager/app/hls_flags.h +++ b/packager/app/hls_flags.h @@ -15,5 +15,6 @@ ABSL_DECLARE_FLAG(std::string, hls_base_url); ABSL_DECLARE_FLAG(std::string, hls_key_uri); ABSL_DECLARE_FLAG(std::string, hls_playlist_type); ABSL_DECLARE_FLAG(int32_t, hls_media_sequence_number); +ABSL_DECLARE_FLAG(std::optional, hls_start_time_offset); #endif // PACKAGER_APP_HLS_FLAGS_H_ diff --git a/packager/app/packager_main.cc b/packager/app/packager_main.cc index 7f5186c59d..60937a1421 100644 --- a/packager/app/packager_main.cc +++ b/packager/app/packager_main.cc @@ -532,6 +532,7 @@ std::optional GetPackagingParams() { hls_params.default_text_language = absl::GetFlag(FLAGS_default_text_language); hls_params.media_sequence_number = absl::GetFlag(FLAGS_hls_media_sequence_number); + hls_params.start_time_offset = absl::GetFlag(FLAGS_hls_start_time_offset); TestParams& test_params = packaging_params.test_params; test_params.dump_stream_info = absl::GetFlag(FLAGS_dump_stream_info); diff --git a/packager/hls/base/media_playlist.cc b/packager/hls/base/media_playlist.cc index fcfaab4a1c..3dd79e3265 100644 --- a/packager/hls/base/media_playlist.cc +++ b/packager/hls/base/media_playlist.cc @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -109,7 +110,8 @@ std::string CreatePlaylistHeader( HlsPlaylistType type, MediaPlaylist::MediaPlaylistStreamType stream_type, uint32_t media_sequence_number, - int discontinuity_sequence_number) { + int discontinuity_sequence_number, + std::optional start_time_offset) { const std::string version = GetPackagerVersion(); std::string version_line; if (!version.empty()) { @@ -151,6 +153,10 @@ std::string CreatePlaylistHeader( MediaPlaylist::MediaPlaylistStreamType::kVideoIFramesOnly) { absl::StrAppendFormat(&header, "#EXT-X-I-FRAMES-ONLY\n"); } + if (start_time_offset.has_value()) { + absl::StrAppendFormat(&header, "#EXT-X-START:TIME-OFFSET=%f\n", + start_time_offset.value()); + } // Put EXT-X-MAP at the end since the rest of the playlist is about the // segment and key info. @@ -485,7 +491,8 @@ bool MediaPlaylist::WriteToFile(const std::filesystem::path& file_path) { std::string content = CreatePlaylistHeader( media_info_, target_duration_, hls_params_.playlist_type, stream_type_, - media_sequence_number_, discontinuity_sequence_number_); + media_sequence_number_, discontinuity_sequence_number_, + hls_params_.start_time_offset); for (const auto& entry : entries_) absl::StrAppendFormat(&content, "%s\n", entry->ToString().c_str()); diff --git a/packager/hls/base/media_playlist_unittest.cc b/packager/hls/base/media_playlist_unittest.cc index 580dee4b83..d92190d0ca 100644 --- a/packager/hls/base/media_playlist_unittest.cc +++ b/packager/hls/base/media_playlist_unittest.cc @@ -51,6 +51,10 @@ class MediaPlaylistTest : public ::testing::Test { default_group_id_("default_group_id") { hls_params_.playlist_type = type; hls_params_.time_shift_buffer_depth = kTimeShiftBufferDepth; + + // NOTE: hls_params_ is passed by and stored by reference in MediaPlaylist, + // so changed made to it through mutable_hls_params() after this point + // still affect what the playlist see in its own hls_params_ later. media_playlist_.reset(new MediaPlaylist(hls_params_, default_file_name_, default_name_, default_group_id_)); } @@ -658,6 +662,90 @@ TEST_F(MediaPlaylistMultiSegmentTest, MultipleEncryptionInfo) { ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput); } +TEST_F(MediaPlaylistSingleSegmentTest, StartTimeEmpty) { + const std::string kExpectedOutput = + "#EXTM3U\n" + "#EXT-X-VERSION:6\n" + "## Generated with https://github.com/shaka-project/shaka-packager " + "version test\n" + "#EXT-X-TARGETDURATION:0\n" + "#EXT-X-PLAYLIST-TYPE:VOD\n" + "#EXT-X-ENDLIST\n"; + + // Because this is std::nullopt, the tag isn't in the playlist at all. + mutable_hls_params()->start_time_offset = std::nullopt; + + ASSERT_TRUE(media_playlist_->SetMediaInfo(valid_video_media_info_)); + + const char kMemoryFilePath[] = "memory://media.m3u8"; + EXPECT_TRUE(media_playlist_->WriteToFile(kMemoryFilePath)); + + ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput); +} + +TEST_F(MediaPlaylistSingleSegmentTest, StartTimeZero) { + const std::string kExpectedOutput = + "#EXTM3U\n" + "#EXT-X-VERSION:6\n" + "## Generated with https://github.com/shaka-project/shaka-packager " + "version test\n" + "#EXT-X-TARGETDURATION:0\n" + "#EXT-X-PLAYLIST-TYPE:VOD\n" + "#EXT-X-START:TIME-OFFSET=0.000000\n" + "#EXT-X-ENDLIST\n"; + + mutable_hls_params()->start_time_offset = 0; + + ASSERT_TRUE(media_playlist_->SetMediaInfo(valid_video_media_info_)); + + const char kMemoryFilePath[] = "memory://media.m3u8"; + EXPECT_TRUE(media_playlist_->WriteToFile(kMemoryFilePath)); + + ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput); +} + +TEST_F(MediaPlaylistSingleSegmentTest, StartTimePositive) { + const std::string kExpectedOutput = + "#EXTM3U\n" + "#EXT-X-VERSION:6\n" + "## Generated with https://github.com/shaka-project/shaka-packager " + "version test\n" + "#EXT-X-TARGETDURATION:0\n" + "#EXT-X-PLAYLIST-TYPE:VOD\n" + "#EXT-X-START:TIME-OFFSET=20.000000\n" + "#EXT-X-ENDLIST\n"; + + mutable_hls_params()->start_time_offset = 20; + + ASSERT_TRUE(media_playlist_->SetMediaInfo(valid_video_media_info_)); + + const char kMemoryFilePath[] = "memory://media.m3u8"; + EXPECT_TRUE(media_playlist_->WriteToFile(kMemoryFilePath)); + + ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput); +} + +TEST_F(MediaPlaylistSingleSegmentTest, StartTimeNegative) { + const std::string kExpectedOutput = + "#EXTM3U\n" + "#EXT-X-VERSION:6\n" + "## Generated with https://github.com/shaka-project/shaka-packager " + "version test\n" + "#EXT-X-TARGETDURATION:0\n" + "#EXT-X-PLAYLIST-TYPE:VOD\n" + "#EXT-X-START:TIME-OFFSET=-3.141590\n" + "#EXT-X-ENDLIST\n"; + + mutable_hls_params()->start_time_offset = -3.14159; + + ASSERT_TRUE(media_playlist_->SetMediaInfo(valid_video_media_info_)); + + const char kMemoryFilePath[] = "memory://media.m3u8"; + EXPECT_TRUE(media_playlist_->WriteToFile(kMemoryFilePath)); + + ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput); +} + class LiveMediaPlaylistTest : public MediaPlaylistMultiSegmentTest { protected: LiveMediaPlaylistTest()