diff --git a/docs/source/options/hls_options.rst b/docs/source/options/hls_options.rst index f3c2b9830b7..8199195d26e 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 a476beb412e..e8c86aa9063 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 ac301722cd4..653830ca4f7 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 09c5a3f6f89..5bb7ae754ab 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 7f5186c59d9..60937a14212 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 fcfaab4a1ce..3dd79e32657 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 580dee4b836..d92190d0ca4 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()