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:
parent
07f780dae1
commit
76eb2c1575
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.");
|
||||||
|
|
|
@ -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_
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in New Issue