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 <joeyparrish@google.com>
This commit is contained in:
Marcus Wichelmann 2024-02-15 21:06:06 +01:00 committed by GitHub
parent 07f780dae1
commit 76eb2c1575
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 125 additions and 2 deletions

View File

@ -76,6 +76,14 @@ HLS options
The EXT-X-MEDIA-SEQUENCE documentation can be read here: The EXT-X-MEDIA-SEQUENCE documentation can be read here:
https://tools.ietf.org/html/rfc8216#section-4.3.3.2. https://tools.ietf.org/html/rfc8216#section-4.3.3.2.
--hls_start_time_offset <seconds>
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 --hls_only=0|1
Optional. Defaults to 0 if not specified. If it is set to 1, indicates the Optional. Defaults to 0 if not specified. If it is set to 1, indicates the

View File

@ -8,6 +8,7 @@
#define PACKAGER_PUBLIC_HLS_PARAMS_H_ #define PACKAGER_PUBLIC_HLS_PARAMS_H_
#include <cstdint> #include <cstdint>
#include <optional>
#include <string> #include <string>
namespace shaka { namespace shaka {
@ -63,6 +64,12 @@ struct HlsParams {
/// Custom EXT-X-MEDIA-SEQUENCE value to allow continuous media playback /// Custom EXT-X-MEDIA-SEQUENCE value to allow continuous media playback
/// across packager restarts. See #691 for details. /// across packager restarts. See #691 for details.
uint32_t media_sequence_number = 0; 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<double> start_time_offset;
}; };
} // namespace shaka } // namespace shaka

View File

@ -6,6 +6,8 @@
#include <packager/app/hls_flags.h> #include <packager/app/hls_flags.h>
#include <optional>
ABSL_FLAG(std::string, ABSL_FLAG(std::string,
hls_master_playlist_output, hls_master_playlist_output,
"", "",
@ -35,3 +37,12 @@ ABSL_FLAG(int32_t,
"EXT-X-MEDIA-SEQUENCE value, which allows continuous media " "EXT-X-MEDIA-SEQUENCE value, which allows continuous media "
"sequence across packager restarts. See #691 for more " "sequence across packager restarts. See #691 for more "
"information about the reasoning of this and its use cases."); "information about the reasoning of this and its use cases.");
ABSL_FLAG(std::optional<double>,
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.");

View File

@ -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_key_uri);
ABSL_DECLARE_FLAG(std::string, hls_playlist_type); ABSL_DECLARE_FLAG(std::string, hls_playlist_type);
ABSL_DECLARE_FLAG(int32_t, hls_media_sequence_number); ABSL_DECLARE_FLAG(int32_t, hls_media_sequence_number);
ABSL_DECLARE_FLAG(std::optional<double>, hls_start_time_offset);
#endif // PACKAGER_APP_HLS_FLAGS_H_ #endif // PACKAGER_APP_HLS_FLAGS_H_

View File

@ -532,6 +532,7 @@ std::optional<PackagingParams> GetPackagingParams() {
hls_params.default_text_language = absl::GetFlag(FLAGS_default_text_language); hls_params.default_text_language = absl::GetFlag(FLAGS_default_text_language);
hls_params.media_sequence_number = hls_params.media_sequence_number =
absl::GetFlag(FLAGS_hls_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; TestParams& test_params = packaging_params.test_params;
test_params.dump_stream_info = absl::GetFlag(FLAGS_dump_stream_info); test_params.dump_stream_info = absl::GetFlag(FLAGS_dump_stream_info);

View File

@ -10,6 +10,7 @@
#include <cinttypes> #include <cinttypes>
#include <cmath> #include <cmath>
#include <memory> #include <memory>
#include <optional>
#include <absl/log/check.h> #include <absl/log/check.h>
#include <absl/log/log.h> #include <absl/log/log.h>
@ -109,7 +110,8 @@ std::string CreatePlaylistHeader(
HlsPlaylistType type, HlsPlaylistType type,
MediaPlaylist::MediaPlaylistStreamType stream_type, MediaPlaylist::MediaPlaylistStreamType stream_type,
uint32_t media_sequence_number, uint32_t media_sequence_number,
int discontinuity_sequence_number) { int discontinuity_sequence_number,
std::optional<double> start_time_offset) {
const std::string version = GetPackagerVersion(); const std::string version = GetPackagerVersion();
std::string version_line; std::string version_line;
if (!version.empty()) { if (!version.empty()) {
@ -151,6 +153,10 @@ std::string CreatePlaylistHeader(
MediaPlaylist::MediaPlaylistStreamType::kVideoIFramesOnly) { MediaPlaylist::MediaPlaylistStreamType::kVideoIFramesOnly) {
absl::StrAppendFormat(&header, "#EXT-X-I-FRAMES-ONLY\n"); 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 // Put EXT-X-MAP at the end since the rest of the playlist is about the
// segment and key info. // segment and key info.
@ -485,7 +491,8 @@ bool MediaPlaylist::WriteToFile(const std::filesystem::path& file_path) {
std::string content = CreatePlaylistHeader( std::string content = CreatePlaylistHeader(
media_info_, target_duration_, hls_params_.playlist_type, stream_type_, 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_) for (const auto& entry : entries_)
absl::StrAppendFormat(&content, "%s\n", entry->ToString().c_str()); absl::StrAppendFormat(&content, "%s\n", entry->ToString().c_str());

View File

@ -51,6 +51,10 @@ class MediaPlaylistTest : public ::testing::Test {
default_group_id_("default_group_id") { default_group_id_("default_group_id") {
hls_params_.playlist_type = type; hls_params_.playlist_type = type;
hls_params_.time_shift_buffer_depth = kTimeShiftBufferDepth; 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_, media_playlist_.reset(new MediaPlaylist(hls_params_, default_file_name_,
default_name_, default_group_id_)); default_name_, default_group_id_));
} }
@ -658,6 +662,90 @@ TEST_F(MediaPlaylistMultiSegmentTest, MultipleEncryptionInfo) {
ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput); 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 { class LiveMediaPlaylistTest : public MediaPlaylistMultiSegmentTest {
protected: protected:
LiveMediaPlaylistTest() LiveMediaPlaylistTest()