Skip to content

Commit

Permalink
feat: Add support for the EXT-X-START tag (shaka-project#973)
Browse files Browse the repository at this point in the history
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 shaka-project#970

---------

Co-authored-by: Joey Parrish <joeyparrish@google.com>
  • Loading branch information
MarcusWichelmann and joeyparrish committed Feb 15, 2024
1 parent 07f780d commit 76eb2c1
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 2 deletions.
8 changes: 8 additions & 0 deletions docs/source/options/hls_options.rst
Expand Up @@ -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 <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

Optional. Defaults to 0 if not specified. If it is set to 1, indicates the
Expand Down
7 changes: 7 additions & 0 deletions include/packager/hls_params.h
Expand Up @@ -8,6 +8,7 @@
#define PACKAGER_PUBLIC_HLS_PARAMS_H_

#include <cstdint>
#include <optional>
#include <string>

namespace shaka {
Expand Down Expand Up @@ -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<double> start_time_offset;
};

} // namespace shaka
Expand Down
11 changes: 11 additions & 0 deletions packager/app/hls_flags.cc
Expand Up @@ -6,6 +6,8 @@

#include <packager/app/hls_flags.h>

#include <optional>

ABSL_FLAG(std::string,
hls_master_playlist_output,
"",
Expand Down Expand Up @@ -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<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.");
1 change: 1 addition & 0 deletions packager/app/hls_flags.h
Expand Up @@ -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<double>, hls_start_time_offset);

#endif // PACKAGER_APP_HLS_FLAGS_H_
1 change: 1 addition & 0 deletions packager/app/packager_main.cc
Expand Up @@ -532,6 +532,7 @@ std::optional<PackagingParams> 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);
Expand Down
11 changes: 9 additions & 2 deletions packager/hls/base/media_playlist.cc
Expand Up @@ -10,6 +10,7 @@
#include <cinttypes>
#include <cmath>
#include <memory>
#include <optional>

#include <absl/log/check.h>
#include <absl/log/log.h>
Expand Down Expand Up @@ -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<double> start_time_offset) {
const std::string version = GetPackagerVersion();
std::string version_line;
if (!version.empty()) {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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());
Expand Down
88 changes: 88 additions & 0 deletions packager/hls/base/media_playlist_unittest.cc
Expand Up @@ -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_));
}
Expand Down Expand Up @@ -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()
Expand Down

0 comments on commit 76eb2c1

Please sign in to comment.