Low latency DASH support (#979)

# LL-DASH Support
These changes add support for LL-DASH streaming. 

**NOTE:** LL-HLS support is still in progress, but it's coming. :) 

## Testing
`./chunking_unittest --gtest_filter="ChunkingHandlerTest.LowLatencyDash"`

`./media_event_unittest --gtest_filter="MpdNotifyMuxerListenerTest.LowLatencyDash"`

`./mpd_unittest --gtest_filter="PeriodTest.LowLatencyDashMpdGetXml"`
`./mpd_unittest --gtest_filter="SimpleMpdNotifierTest.NotifyAvailabilityTimeOffset"`
`./mpd_unittest --gtest_filter="SimpleMpdNotifierTest.NotifySegmentDuration"`
`./mpd_unittest --gtest_filter="LowLatencySegmentTest.LowLatencySegmentTemplate"`

Note, packager_test must be run from the main project directory
`./out/Release/packager_test --gtest_filter="PackagerTest.LowLatencyDashEnabledAndUtcTimingNotSet"`
`./out/Release/packager_test --gtest_filter="PackagerTest.LowLatencyDashEnabledAndUtcTimingNotSet"`
This commit is contained in:
Caitlin O'Callaghan 2021-08-25 08:38:05 -07:00 committed by GitHub
parent ac125564b9
commit cd018a71c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 937 additions and 25 deletions

View File

@ -95,3 +95,8 @@ DASH options
If enabled, allow adaptive switching between different codecs, if they have If enabled, allow adaptive switching between different codecs, if they have
the same language, media type (audio, video etc) and container type. the same language, media type (audio, video etc) and container type.
--low_latency_dash_mode
If enabled, LL-DASH streaming will be used,
reducing overall latency by decoupling latency from segment duration.

View File

@ -0,0 +1,103 @@
####################################
Low Latency DASH (LL-DASH) Streaming
####################################
************
Introduction
************
If ``--low_latency_dash_mode`` is enabled, low latency DASH (LL-DASH) packaging will be used.
This will reduce overall latency by ensuring that the media segments are chunk encoded and delivered via an aggregating response.
The combination of these features will ensure that overall latency can be decoupled from the segment duration.
For low latency to be achieved, the output of Shaka Packager must be combined with a delivery system which can chain together a set of aggregating responses, such as chunked transfer encoding under HTTP/1.1 or a HTTP/2 or HTTP/3 connection.
The output of Shaka Packager must be played with a DASH client that understands the availabilityTimeOffset MPD value.
Furthermore, the player should also understand the throughput estimation and ABR challenges that arise when operating in the low latency regime.
This tutorial covers LL-DASH packaging and uses features from the DASH, HTTP upload, and FFmpeg piping tutorials.
For more information on DASH, see :doc:`dash`; for HTTP upload, see :doc:`http_upload`;
for FFmpeg piping, see :doc:`ffmpeg_piping`;
for full documentation, see :doc:`/documentation`.
*************
Documentation
*************
Getting started
===============
To enable LL-DASH mode, set the ``--low_latency_dash_mode`` flag to ``true``.
All HTTP requests will use chunked transfer encoding:
``Transfer-Encoding: chunked``.
.. note::
Only LL-DASH is supported. LL-HLS support is yet to come.
Synopsis
========
Here is a basic example of the LL-DASH support.
The LL-DASH setup borrows features from "FFmpeg piping" and "HTTP upload",
see :doc:`ffmpeg_piping` and :doc:`http_upload`.
Define UNIX pipe to connect ffmpeg with packager::
export PIPE=/tmp/bigbuckbunny.fifo
mkfifo ${PIPE}
Acquire and transcode RTMP stream::
ffmpeg -fflags nobuffer -threads 0 -y \
-i rtmp://184.72.239.149/vod/mp4:bigbuckbunny_450.mp4 \
-pix_fmt yuv420p -vcodec libx264 -preset:v superfast -acodec aac \
-f mpegts pipe: > ${PIPE}
Configure and run packager::
# Define upload URL
export UPLOAD_URL=http://localhost:6767/ll-dash
# Go
packager \
"input=${PIPE},stream=audio,init_segment=${UPLOAD_URL}_init.m4s,segment_template=${UPLOAD_URL}/bigbuckbunny-audio-aac-\$Number%04d\$.m4s" \
"input=${PIPE},stream=video,init_segment=${UPLOAD_URL}_init.m4s,segment_template=${UPLOAD_URL}/bigbuckbunny-video-h264-450-\$Number%04d\$.m4s" \
--io_block_size 65536 \
--segment_duration 2 \
--low_latency_dash_mode=true \
--utc_timings "urn:mpeg:dash:utc:http-xsdate:2014"="https://time.akamai.com/?iso" \
--mpd_output "${UPLOAD_URL}/bigbuckbunny.mpd" \
*************************
Low Latency Compatibility
*************************
For low latency to be achieved, the processes handling Shaka Packager's output, such as the server and player,
must support LL-DASH streaming.
Delivery Pipeline
=================
Shaka Packager will upload the LL-DASH content to the specified output via HTTP chunked transfer encoding.
The server must have the ability to handle this type of request. If using a proxy or shim for cloud authentication,
these services must also support HTTP chunked transfer encoding.
Examples of supporting content delivery systems:
* `AWS MediaStore <https://aws.amazon.com/mediastore/>`_
* `s3-upload-proxy <https://github.com/fsouza/s3-upload-proxy>`_
* `Streamline Low Latency DASH preview <https://github.com/streamlinevideo/low-latency-preview>`_
* `go-chunked-streaming-server <https://github.com/mjneil/go-chunked-streaming-server>`_
Player
======
The player must support LL-DASH playout.
LL-DASH requires the player to be able to interpret ``availabilityTimeOffset`` values from the DASH MPD.
The player should also recognize the the throughput estimation and ABR challenges that arise with low latency streaming.
Examples of supporting players:
* `Shaka Player <https://github.com/google/shaka-player>`_
* `dash.js <https://github.com/Dash-Industry-Forum/dash.js>`_
* `Streamline Low Latency DASH preview <https://github.com/streamlinevideo/low-latency-preview>`_

View File

@ -13,3 +13,4 @@ Tutorials
ads.rst ads.rst
ffmpeg_piping.rst ffmpeg_piping.rst
http_upload.rst http_upload.rst
low_latency.rst

View File

@ -75,3 +75,11 @@ DEFINE_bool(dash_force_segment_list,
"content is huge and the total number of (sub)segment references " "content is huge and the total number of (sub)segment references "
"is greater than what the sidx atom allows (65535). Currently " "is greater than what the sidx atom allows (65535). Currently "
"this flag is only supported in DASH ondemand profile."); "this flag is only supported in DASH ondemand profile.");
DEFINE_bool(
low_latency_dash_mode,
false,
"If enabled, LL-DASH streaming will be used, "
"reducing overall latency by decoupling latency from segment duration. "
"Please see "
"https://google.github.io/shaka-packager/html/tutorials/low_latency.html "
"for more information.");

View File

@ -24,5 +24,6 @@ DECLARE_bool(allow_approximate_segment_timeline);
DECLARE_bool(allow_codec_switching); DECLARE_bool(allow_codec_switching);
DECLARE_bool(include_mspr_pro_for_playready); DECLARE_bool(include_mspr_pro_for_playready);
DECLARE_bool(dash_force_segment_list); DECLARE_bool(dash_force_segment_list);
DECLARE_bool(low_latency_dash_mode);
#endif // APP_MPD_FLAGS_H_ #endif // APP_MPD_FLAGS_H_

View File

@ -325,6 +325,7 @@ base::Optional<PackagingParams> GetPackagingParams() {
ChunkingParams& chunking_params = packaging_params.chunking_params; ChunkingParams& chunking_params = packaging_params.chunking_params;
chunking_params.segment_duration_in_seconds = FLAGS_segment_duration; chunking_params.segment_duration_in_seconds = FLAGS_segment_duration;
chunking_params.subsegment_duration_in_seconds = FLAGS_fragment_duration; chunking_params.subsegment_duration_in_seconds = FLAGS_fragment_duration;
chunking_params.low_latency_dash_mode = FLAGS_low_latency_dash_mode;
chunking_params.segment_sap_aligned = FLAGS_segment_sap_aligned; chunking_params.segment_sap_aligned = FLAGS_segment_sap_aligned;
chunking_params.subsegment_sap_aligned = FLAGS_fragment_sap_aligned; chunking_params.subsegment_sap_aligned = FLAGS_fragment_sap_aligned;
@ -435,6 +436,7 @@ base::Optional<PackagingParams> GetPackagingParams() {
mp4_params.generate_sidx_in_media_segments = mp4_params.generate_sidx_in_media_segments =
FLAGS_generate_sidx_in_media_segments; FLAGS_generate_sidx_in_media_segments;
mp4_params.include_pssh_in_stream = FLAGS_mp4_include_pssh_in_stream; mp4_params.include_pssh_in_stream = FLAGS_mp4_include_pssh_in_stream;
mp4_params.low_latency_dash_mode = FLAGS_low_latency_dash_mode;
packaging_params.transport_stream_timestamp_offset_ms = packaging_params.transport_stream_timestamp_offset_ms =
FLAGS_transport_stream_timestamp_offset_ms; FLAGS_transport_stream_timestamp_offset_ms;
@ -474,6 +476,7 @@ base::Optional<PackagingParams> GetPackagingParams() {
FLAGS_allow_approximate_segment_timeline; FLAGS_allow_approximate_segment_timeline;
mpd_params.allow_codec_switching = FLAGS_allow_codec_switching; mpd_params.allow_codec_switching = FLAGS_allow_codec_switching;
mpd_params.include_mspr_pro = FLAGS_include_mspr_pro_for_playready; mpd_params.include_mspr_pro = FLAGS_include_mspr_pro_for_playready;
mpd_params.low_latency_dash_mode = FLAGS_low_latency_dash_mode;
HlsParams& hls_params = packaging_params.hls_params; HlsParams& hls_params = packaging_params.hls_params;
if (!GetHlsPlaylistType(FLAGS_hls_playlist_type, &hls_params.playlist_type)) { if (!GetHlsPlaylistType(FLAGS_hls_playlist_type, &hls_params.playlist_type)) {

View File

@ -184,6 +184,7 @@ File* File::CreateInternalFile(const char* file_name, const char* mode) {
base::StringPiece real_file_name; base::StringPiece real_file_name;
const FileTypeInfo* file_type = GetFileTypeInfo(file_name, &real_file_name); const FileTypeInfo* file_type = GetFileTypeInfo(file_name, &real_file_name);
DCHECK(file_type); DCHECK(file_type);
// Calls constructor for the derived File class.
return file_type->factory_function(real_file_name.data(), mode); return file_type->factory_function(real_file_name.data(), mode);
} }

View File

@ -297,7 +297,7 @@ void HttpFile::SetupRequest() {
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &CurlWriteCallback); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &CurlWriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, curl_easy_setopt(curl, CURLOPT_WRITEDATA,
method_ == HttpMethod::kPut ? nullptr : &download_cache_); method_ == HttpMethod::kGet ? &download_cache_ : nullptr);
if (method_ != HttpMethod::kGet) { if (method_ != HttpMethod::kGet) {
curl_easy_setopt(curl, CURLOPT_READFUNCTION, &CurlReadCallback); curl_easy_setopt(curl, CURLOPT_READFUNCTION, &CurlReadCallback);
curl_easy_setopt(curl, CURLOPT_READDATA, &upload_cache_); curl_easy_setopt(curl, CURLOPT_READDATA, &upload_cache_);

View File

@ -54,6 +54,8 @@ struct CueEvent {
struct SegmentInfo { struct SegmentInfo {
bool is_subsegment = false; bool is_subsegment = false;
bool is_chunk = false;
bool is_final_chunk_in_seg = false;
bool is_encrypted = false; bool is_encrypted = false;
int64_t start_timestamp = -1; int64_t start_timestamp = -1;
int64_t duration = 0; int64_t duration = 0;

View File

@ -112,7 +112,23 @@ Status ChunkingHandler::OnMediaSample(
started_new_segment = true; started_new_segment = true;
} }
} }
if (!started_new_segment && IsSubsegmentEnabled()) {
// This handles the LL-DASH case.
// On each media sample, which is the basis for a chunk,
// we must increment the current_subsegment_index_
// in order to hit FinalizeSegment() within Segmenter.
if (!started_new_segment && chunking_params_.low_latency_dash_mode) {
current_subsegment_index_++;
RETURN_IF_ERROR(EndSubsegmentIfStarted());
subsegment_start_time_ = timestamp;
}
// Here, a subsegment refers to a fragment that is within a segment.
// This fragment size can be set with the 'fragment_duration' cmd arg.
// This is NOT for the LL-DASH case.
if (!started_new_segment && IsSubsegmentEnabled() &&
!chunking_params_.low_latency_dash_mode) {
const bool can_start_new_subsegment = const bool can_start_new_subsegment =
sample->is_key_frame() || !chunking_params_.subsegment_sap_aligned; sample->is_key_frame() || !chunking_params_.subsegment_sap_aligned;
if (can_start_new_subsegment) { if (can_start_new_subsegment) {
@ -151,6 +167,10 @@ Status ChunkingHandler::EndSegmentIfStarted() const {
auto segment_info = std::make_shared<SegmentInfo>(); auto segment_info = std::make_shared<SegmentInfo>();
segment_info->start_timestamp = segment_start_time_.value(); segment_info->start_timestamp = segment_start_time_.value();
segment_info->duration = max_segment_time_ - segment_start_time_.value(); segment_info->duration = max_segment_time_ - segment_start_time_.value();
if (chunking_params_.low_latency_dash_mode) {
segment_info->is_chunk = true;
segment_info->is_final_chunk_in_seg = true;
}
return DispatchSegmentInfo(kStreamIndex, std::move(segment_info)); return DispatchSegmentInfo(kStreamIndex, std::move(segment_info));
} }
@ -163,6 +183,8 @@ Status ChunkingHandler::EndSubsegmentIfStarted() const {
subsegment_info->duration = subsegment_info->duration =
max_segment_time_ - subsegment_start_time_.value(); max_segment_time_ - subsegment_start_time_.value();
subsegment_info->is_subsegment = true; subsegment_info->is_subsegment = true;
if (chunking_params_.low_latency_dash_mode)
subsegment_info->is_chunk = true;
return DispatchSegmentInfo(kStreamIndex, std::move(subsegment_info)); return DispatchSegmentInfo(kStreamIndex, std::move(subsegment_info));
} }

View File

@ -207,5 +207,48 @@ TEST_F(ChunkingHandlerTest, CueEvent) {
kDuration, !kEncrypted, _))); kDuration, !kEncrypted, _)));
} }
TEST_F(ChunkingHandlerTest, LowLatencyDash) {
ChunkingParams chunking_params;
chunking_params.low_latency_dash_mode = true;
chunking_params.segment_duration_in_seconds = 1;
SetUpChunkingHandler(1, chunking_params);
// Each completed segment will contain 2 chunks
const int64_t kChunkDurationInMs = 500;
const int64_t kSegmentDurationInMs = 1000;
ASSERT_OK(Process(StreamData::FromStreamInfo(
kStreamIndex, GetVideoStreamInfo(kTimeScale1))));
for (int i = 0; i < 4; ++i) {
ASSERT_OK(Process(StreamData::FromMediaSample(
kStreamIndex, GetMediaSample(i * kChunkDurationInMs, kChunkDurationInMs,
kKeyFrame))));
}
// NOTE: Each MediaSample will create a chunk, dispatching SegmentInfo
EXPECT_THAT(
GetOutputStreamDataVector(),
ElementsAre(
IsStreamInfo(kStreamIndex, kTimeScale1, !kEncrypted, _),
// Chunk 1 for segment 1
IsMediaSample(kStreamIndex, 0, kChunkDurationInMs, !kEncrypted, _),
IsSegmentInfo(kStreamIndex, 0, kChunkDurationInMs, kIsSubsegment,
!kEncrypted),
// Chunk 2 for segment 1
IsMediaSample(kStreamIndex, kChunkDurationInMs, kChunkDurationInMs,
!kEncrypted, _),
IsSegmentInfo(kStreamIndex, 0, 2 * kChunkDurationInMs, !kIsSubsegment,
!kEncrypted),
// Chunk 1 for segment 2
IsMediaSample(kStreamIndex, kSegmentDurationInMs, kChunkDurationInMs,
!kEncrypted, _),
IsSegmentInfo(kStreamIndex, kSegmentDurationInMs, kChunkDurationInMs,
kIsSubsegment, !kEncrypted),
// Chunk 2 for segment 2
IsMediaSample(kStreamIndex, kSegmentDurationInMs + kChunkDurationInMs,
kChunkDurationInMs, !kEncrypted, _)));
}
} // namespace media } // namespace media
} // namespace shaka } // namespace shaka

View File

@ -43,12 +43,24 @@ void CombinedMuxerListener::OnMediaStart(const MuxerOptions& muxer_options,
} }
} }
void CombinedMuxerListener::OnAvailabilityOffsetReady() {
for (auto& listener : muxer_listeners_) {
listener->OnAvailabilityOffsetReady();
}
}
void CombinedMuxerListener::OnSampleDurationReady(int32_t sample_duration) { void CombinedMuxerListener::OnSampleDurationReady(int32_t sample_duration) {
for (auto& listener : muxer_listeners_) { for (auto& listener : muxer_listeners_) {
listener->OnSampleDurationReady(sample_duration); listener->OnSampleDurationReady(sample_duration);
} }
} }
void CombinedMuxerListener::OnSegmentDurationReady() {
for (auto& listener : muxer_listeners_) {
listener->OnSegmentDurationReady();
}
}
void CombinedMuxerListener::OnMediaEnd(const MediaRanges& media_ranges, void CombinedMuxerListener::OnMediaEnd(const MediaRanges& media_ranges,
float duration_seconds) { float duration_seconds) {
for (auto& listener : muxer_listeners_) { for (auto& listener : muxer_listeners_) {

View File

@ -36,7 +36,9 @@ class CombinedMuxerListener : public MuxerListener {
const StreamInfo& stream_info, const StreamInfo& stream_info,
int32_t time_scale, int32_t time_scale,
ContainerType container_type) override; ContainerType container_type) override;
void OnAvailabilityOffsetReady() override;
void OnSampleDurationReady(int32_t sample_duration) override; void OnSampleDurationReady(int32_t sample_duration) override;
void OnSegmentDurationReady() override;
void OnMediaEnd(const MediaRanges& media_ranges, void OnMediaEnd(const MediaRanges& media_ranges,
float duration_seconds) override; float duration_seconds) override;
void OnNewSegment(const std::string& file_name, void OnNewSegment(const std::string& file_name,

View File

@ -105,6 +105,11 @@ void MpdNotifyMuxerListener::OnMediaStart(const MuxerOptions& muxer_options,
} }
} }
// Record the availability time offset for LL-DASH manifests.
void MpdNotifyMuxerListener::OnAvailabilityOffsetReady() {
mpd_notifier_->NotifyAvailabilityTimeOffset(notification_id_.value());
}
// Record the sample duration in the media info for VOD so that OnMediaEnd, all // Record the sample duration in the media info for VOD so that OnMediaEnd, all
// the information is in the media info. // the information is in the media info.
void MpdNotifyMuxerListener::OnSampleDurationReady(int32_t sample_duration) { void MpdNotifyMuxerListener::OnSampleDurationReady(int32_t sample_duration) {
@ -127,6 +132,11 @@ void MpdNotifyMuxerListener::OnSampleDurationReady(int32_t sample_duration) {
media_info_->mutable_video_info()->set_frame_duration(sample_duration); media_info_->mutable_video_info()->set_frame_duration(sample_duration);
} }
// Record the segment duration for LL-DASH manifests.
void MpdNotifyMuxerListener::OnSegmentDurationReady() {
mpd_notifier_->NotifySegmentDuration(notification_id_.value());
}
void MpdNotifyMuxerListener::OnMediaEnd(const MediaRanges& media_ranges, void MpdNotifyMuxerListener::OnMediaEnd(const MediaRanges& media_ranges,
float duration_seconds) { float duration_seconds) {
if (mpd_notifier_->dash_profile() == DashProfile::kLive) { if (mpd_notifier_->dash_profile() == DashProfile::kLive) {

View File

@ -44,7 +44,9 @@ class MpdNotifyMuxerListener : public MuxerListener {
const StreamInfo& stream_info, const StreamInfo& stream_info,
int32_t time_scale, int32_t time_scale,
ContainerType container_type) override; ContainerType container_type) override;
void OnAvailabilityOffsetReady() override;
void OnSampleDurationReady(int32_t sample_duration) override; void OnSampleDurationReady(int32_t sample_duration) override;
void OnSegmentDurationReady() override;
void OnMediaEnd(const MediaRanges& media_ranges, void OnMediaEnd(const MediaRanges& media_ranges,
float duration_seconds) override; float duration_seconds) override;
void OnNewSegment(const std::string& file_name, void OnNewSegment(const std::string& file_name,

View File

@ -96,6 +96,17 @@ class MpdNotifyMuxerListenerTest : public ::testing::TestWithParam<MpdType> {
listener_.reset(new MpdNotifyMuxerListener(notifier_.get())); listener_.reset(new MpdNotifyMuxerListener(notifier_.get()));
} }
void SetupForLowLatencyDash() {
MpdOptions mpd_options;
// Low Latency DASH streaming should be live.
mpd_options.dash_profile = DashProfile::kLive;
// Low Latency DASH live profile should be dynamic.
mpd_options.mpd_type = MpdType::kDynamic;
mpd_options.mpd_params.low_latency_dash_mode = true;
notifier_.reset(new MockMpdNotifier(mpd_options));
listener_.reset(new MpdNotifyMuxerListener(notifier_.get()));
}
void FireOnMediaEndWithParams(const OnMediaEndParameters& params) { void FireOnMediaEndWithParams(const OnMediaEndParameters& params) {
// On success, this writes the result to |temp_file_path_|. // On success, this writes the result to |temp_file_path_|.
listener_->OnMediaEnd(params.media_ranges, params.duration_seconds); listener_->OnMediaEnd(params.media_ranges, params.duration_seconds);
@ -509,7 +520,6 @@ TEST_F(MpdNotifyMuxerListenerTest, VodMultipleFiles) {
FireOnMediaEndWithParams(GetDefaultOnMediaEndParams()); FireOnMediaEndWithParams(GetDefaultOnMediaEndParams());
} }
TEST_F(MpdNotifyMuxerListenerTest, VodMultipleFilesSegmentList) { TEST_F(MpdNotifyMuxerListenerTest, VodMultipleFilesSegmentList) {
SetupForVodSegmentList(); SetupForVodSegmentList();
MuxerOptions muxer_options1; MuxerOptions muxer_options1;
@ -571,6 +581,65 @@ TEST_F(MpdNotifyMuxerListenerTest, VodMultipleFilesSegmentList) {
FireOnMediaEndWithParams(GetDefaultOnMediaEndParams()); FireOnMediaEndWithParams(GetDefaultOnMediaEndParams());
} }
TEST_F(MpdNotifyMuxerListenerTest, LowLatencyDash) {
SetupForLowLatencyDash();
MuxerOptions muxer_options;
SetDefaultLiveMuxerOptions(&muxer_options);
VideoStreamInfoParameters video_params = GetDefaultVideoStreamInfoParams();
std::shared_ptr<StreamInfo> video_stream_info =
CreateVideoStreamInfo(video_params);
const std::string kExpectedMediaInfo =
"video_info {\n"
" codec: \"avc1.010101\"\n"
" width: 720\n"
" height: 480\n"
" time_scale: 10\n"
" pixel_width: 1\n"
" pixel_height: 1\n"
"}\n"
"media_duration_seconds: 20.0\n"
"init_segment_name: \"liveinit.mp4\"\n"
"segment_template: \"live-$NUMBER$.mp4\"\n"
"reference_time_scale: 1000\n"
"container_type: CONTAINER_MP4\n";
const uint64_t kStartTime1 = 0u;
const uint64_t kStartTime2 = 1001u;
const uint64_t kDuration = 1000u;
const uint64_t kSegmentSize1 = 29812u;
const uint64_t kSegmentSize2 = 30128u;
EXPECT_CALL(*notifier_,
NotifyNewContainer(ExpectMediaInfoEq(kExpectedMediaInfo), _))
.WillOnce(Return(true));
EXPECT_CALL(*notifier_, NotifySampleDuration(_, kDuration))
.WillOnce(Return(true));
EXPECT_CALL(*notifier_, NotifyAvailabilityTimeOffset(_))
.WillOnce(Return(true));
EXPECT_CALL(*notifier_, NotifySegmentDuration(_)).WillOnce(Return(true));
EXPECT_CALL(*notifier_,
NotifyNewSegment(_, kStartTime1, kDuration, kSegmentSize1));
EXPECT_CALL(*notifier_, NotifyCueEvent(_, kStartTime2));
EXPECT_CALL(*notifier_,
NotifyNewSegment(_, kStartTime2, kDuration, kSegmentSize2));
EXPECT_CALL(*notifier_, Flush()).Times(2);
listener_->OnMediaStart(muxer_options, *video_stream_info,
kDefaultReferenceTimeScale,
MuxerListener::kContainerMp4);
listener_->OnSampleDurationReady(kDuration);
listener_->OnAvailabilityOffsetReady();
listener_->OnSegmentDurationReady();
listener_->OnNewSegment("", kStartTime1, kDuration, kSegmentSize1);
listener_->OnCueEvent(kStartTime2, "dummy cue data");
listener_->OnNewSegment("", kStartTime2, kDuration, kSegmentSize2);
::testing::Mock::VerifyAndClearExpectations(notifier_.get());
EXPECT_CALL(*notifier_, Flush()).Times(0);
FireOnMediaEndWithParams(GetDefaultOnMediaEndParams());
}
// Live without key rotation. Note that OnEncryptionInfoReady() is called before // Live without key rotation. Note that OnEncryptionInfoReady() is called before
// OnMediaStart() but no more calls. // OnMediaStart() but no more calls.
TEST_P(MpdNotifyMuxerListenerTest, LiveNoKeyRotation) { TEST_P(MpdNotifyMuxerListenerTest, LiveNoKeyRotation) {

View File

@ -100,10 +100,16 @@ class MuxerListener {
int32_t time_scale, int32_t time_scale,
ContainerType container_type) = 0; ContainerType container_type) = 0;
/// Called when LL-DASH streaming starts.
virtual void OnAvailabilityOffsetReady() {}
/// Called when the average sample duration of the media is determined. /// Called when the average sample duration of the media is determined.
/// @param sample_duration in timescale of the media. /// @param sample_duration in timescale of the media.
virtual void OnSampleDurationReady(int32_t sample_duration) = 0; virtual void OnSampleDurationReady(int32_t sample_duration) = 0;
/// Called when LL-DASH streaming starts.
virtual void OnSegmentDurationReady() {}
/// Called when all files are written out and the muxer object does not output /// Called when all files are written out and the muxer object does not output
/// any more files. /// any more files.
/// Note: This event might not be very interesting to MPEG DASH Live profile. /// Note: This event might not be very interesting to MPEG DASH Live profile.

View File

@ -0,0 +1,212 @@
// Copyright 2014 Google Inc. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
#include "packager/media/formats/mp4/low_latency_segment_segmenter.h"
#include <algorithm>
#include "packager/file/file.h"
#include "packager/file/file_closer.h"
#include "packager/media/base/buffer_writer.h"
#include "packager/media/base/media_handler.h"
#include "packager/media/base/muxer_options.h"
#include "packager/media/base/muxer_util.h"
#include "packager/media/event/muxer_listener.h"
#include "packager/media/formats/mp4/box_definitions.h"
#include "packager/media/formats/mp4/fragmenter.h"
#include "packager/media/formats/mp4/key_frame_info.h"
#include "packager/status_macros.h"
namespace shaka {
namespace media {
namespace mp4 {
LowLatencySegmentSegmenter::LowLatencySegmentSegmenter(
const MuxerOptions& options,
std::unique_ptr<FileType> ftyp,
std::unique_ptr<Movie> moov)
: Segmenter(options, std::move(ftyp), std::move(moov)),
styp_(new SegmentType),
num_segments_(0) {
// Use the same brands for styp as ftyp.
styp_->major_brand = Segmenter::ftyp()->major_brand;
styp_->compatible_brands = Segmenter::ftyp()->compatible_brands;
// Replace 'cmfc' with 'cmfs' for CMAF segments compatibility.
std::replace(styp_->compatible_brands.begin(), styp_->compatible_brands.end(),
FOURCC_cmfc, FOURCC_cmfs);
}
LowLatencySegmentSegmenter::~LowLatencySegmentSegmenter() {}
bool LowLatencySegmentSegmenter::GetInitRange(size_t* offset, size_t* size) {
VLOG(1) << "LowLatencySegmentSegmenter outputs init segment: "
<< options().output_file_name;
return false;
}
bool LowLatencySegmentSegmenter::GetIndexRange(size_t* offset, size_t* size) {
VLOG(1) << "LowLatencySegmentSegmenter does not have index range.";
return false;
}
std::vector<Range> LowLatencySegmentSegmenter::GetSegmentRanges() {
VLOG(1) << "LowLatencySegmentSegmenter does not have media segment ranges.";
return std::vector<Range>();
}
Status LowLatencySegmentSegmenter::DoInitialize() {
return WriteInitSegment();
}
Status LowLatencySegmentSegmenter::DoFinalize() {
// Update init segment with media duration set.
RETURN_IF_ERROR(WriteInitSegment());
SetComplete();
return Status::OK;
}
Status LowLatencySegmentSegmenter::DoFinalizeSegment() {
return FinalizeSegment();
}
Status LowLatencySegmentSegmenter::DoFinalizeChunk() {
if (is_initial_chunk_in_seg_) {
return WriteInitialChunk();
}
return WriteChunk();
}
Status LowLatencySegmentSegmenter::WriteInitSegment() {
DCHECK(ftyp());
DCHECK(moov());
// Generate the output file with init segment.
std::unique_ptr<File, FileCloser> file(
File::Open(options().output_file_name.c_str(), "w"));
if (!file) {
return Status(error::FILE_FAILURE,
"Cannot open file for write " + options().output_file_name);
}
std::unique_ptr<BufferWriter> buffer(new BufferWriter);
ftyp()->Write(buffer.get());
moov()->Write(buffer.get());
return buffer->WriteToFile(file.get());
}
Status LowLatencySegmentSegmenter::WriteInitialChunk() {
DCHECK(sidx());
DCHECK(fragment_buffer());
DCHECK(styp_);
DCHECK(!sidx()->references.empty());
// earliest_presentation_time is the earliest presentation time of any access
// unit in the reference stream in the first subsegment.
sidx()->earliest_presentation_time =
sidx()->references[0].earliest_presentation_time;
if (options().segment_template.empty()) {
// Append the segment to output file if segment template is not specified.
file_name_ = options().output_file_name.c_str();
} else {
file_name_ = GetSegmentName(options().segment_template,
sidx()->earliest_presentation_time,
num_segments_, options().bandwidth);
}
// Create the segment file
segment_file_.reset(File::Open(file_name_.c_str(), "a"));
if (!segment_file_) {
return Status(error::FILE_FAILURE,
"Cannot open segment file: " + file_name_);
}
std::unique_ptr<BufferWriter> buffer(new BufferWriter());
// Write the styp header to the beginning of the segment.
styp_->Write(buffer.get());
const size_t segment_header_size = buffer->Size();
const size_t segment_size = segment_header_size + fragment_buffer()->Size();
DCHECK_NE(segment_size, 0u);
RETURN_IF_ERROR(buffer->WriteToFile(segment_file_.get()));
if (muxer_listener()) {
for (const KeyFrameInfo& key_frame_info : key_frame_infos()) {
muxer_listener()->OnKeyFrame(
key_frame_info.timestamp,
segment_header_size + key_frame_info.start_byte_offset,
key_frame_info.size);
}
}
// Write the chunk data to the file
RETURN_IF_ERROR(fragment_buffer()->WriteToFile(segment_file_.get()));
uint64_t segment_duration = GetSegmentDuration();
UpdateProgress(segment_duration);
if (muxer_listener()) {
if (!ll_dash_mpd_values_initialized_) {
// Set necessary values for LL-DASH mpd after the first chunk has been
// processed.
muxer_listener()->OnSampleDurationReady(sample_duration());
muxer_listener()->OnAvailabilityOffsetReady();
muxer_listener()->OnSegmentDurationReady();
ll_dash_mpd_values_initialized_ = true;
}
// Add the current segment in the manifest.
// Following chunks will be appended to the open segment file.
muxer_listener()->OnNewSegment(file_name_,
sidx()->earliest_presentation_time,
segment_duration, segment_size);
is_initial_chunk_in_seg_ = false;
}
return Status::OK;
}
Status LowLatencySegmentSegmenter::WriteChunk() {
DCHECK(fragment_buffer());
// Write the chunk data to the file
RETURN_IF_ERROR(fragment_buffer()->WriteToFile(segment_file_.get()));
UpdateProgress(GetSegmentDuration());
return Status::OK;
}
Status LowLatencySegmentSegmenter::FinalizeSegment() {
// Close the file now that the final chunk has been written
if (!segment_file_.release()->Close()) {
return Status(
error::FILE_FAILURE,
"Cannot close file " + file_name_ +
", possibly file permission issue or running out of disk space.");
}
// Current segment is complete. Reset state in preparation for the next
// segment.
is_initial_chunk_in_seg_ = true;
num_segments_++;
return Status::OK;
}
uint64_t LowLatencySegmentSegmenter::GetSegmentDuration() {
DCHECK(sidx());
uint64_t segment_duration = 0;
// ISO/IEC 23009-1:2012: the value shall be identical to sum of the the
// values of all Subsegment_duration fields in the first sidx box.
for (size_t i = 0; i < sidx()->references.size(); ++i)
segment_duration += sidx()->references[i].subsegment_duration;
return segment_duration;
}
} // namespace mp4
} // namespace media
} // namespace shaka

View File

@ -0,0 +1,72 @@
// Copyright 2014 Google Inc. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
#ifndef PACKAGER_MEDIA_FORMATS_MP4_LOW_LATENCY_SEGMENT_SEGMENTER_H_
#define PACKAGER_MEDIA_FORMATS_MP4_LOW_LATENCY_SEGMENT_SEGMENTER_H_
#include "packager/media/formats/mp4/segmenter.h"
#include "packager/file/file.h"
#include "packager/file/file_closer.h"
namespace shaka {
namespace media {
namespace mp4 {
struct SegmentType;
/// Segmenter for LL-DASH profiles.
/// Each segment constist of many fragments, and each fragment contains one
/// chunk. A chunk is the smallest unit and is constructed of a single moof and
/// mdat atom. A chunk is be generated for each recieved @b MediaSample. The
/// generated chunks are written as they are created to files defined by
/// @b MuxerOptions.segment_template if specified; otherwise, the chunks are
/// appended to the main output file specified by @b
/// MuxerOptions.output_file_name.
class LowLatencySegmentSegmenter : public Segmenter {
public:
LowLatencySegmentSegmenter(const MuxerOptions& options,
std::unique_ptr<FileType> ftyp,
std::unique_ptr<Movie> moov);
~LowLatencySegmentSegmenter() override;
/// @name Segmenter implementation overrides.
/// @{
bool GetInitRange(size_t* offset, size_t* size) override;
bool GetIndexRange(size_t* offset, size_t* size) override;
std::vector<Range> GetSegmentRanges() override;
/// @}
private:
// Segmenter implementation overrides.
Status DoInitialize() override;
Status DoFinalize() override;
Status DoFinalizeSegment() override;
Status DoFinalizeChunk() override;
// Write segment to file.
Status WriteInitSegment();
Status WriteChunk();
Status WriteInitialChunk();
Status FinalizeSegment();
uint64_t GetSegmentDuration();
std::unique_ptr<SegmentType> styp_;
uint32_t num_segments_;
bool is_initial_chunk_in_seg_ = true;
bool ll_dash_mpd_values_initialized_ = false;
std::unique_ptr<File, FileCloser> segment_file_;
std::string file_name_;
DISALLOW_COPY_AND_ASSIGN(LowLatencySegmentSegmenter);
};
} // namespace mp4
} // namespace media
} // namespace shaka
#endif // PACKAGER_MEDIA_FORMATS_MP4_LOW_LATENCY_SEGMENT_SEGMENTER_H_

View File

@ -29,6 +29,8 @@
'fragmenter.cc', 'fragmenter.cc',
'fragmenter.h', 'fragmenter.h',
'key_frame_info.h', 'key_frame_info.h',
'low_latency_segment_segmenter.cc',
'low_latency_segment_segmenter.h',
'mp4_media_parser.cc', 'mp4_media_parser.cc',
'mp4_media_parser.h', 'mp4_media_parser.h',
'mp4_muxer.cc', 'mp4_muxer.cc',

View File

@ -22,6 +22,7 @@
#include "packager/media/codecs/es_descriptor.h" #include "packager/media/codecs/es_descriptor.h"
#include "packager/media/event/muxer_listener.h" #include "packager/media/event/muxer_listener.h"
#include "packager/media/formats/mp4/box_definitions.h" #include "packager/media/formats/mp4/box_definitions.h"
#include "packager/media/formats/mp4/low_latency_segment_segmenter.h"
#include "packager/media/formats/mp4/multi_segment_segmenter.h" #include "packager/media/formats/mp4/multi_segment_segmenter.h"
#include "packager/media/formats/mp4/single_segment_segmenter.h" #include "packager/media/formats/mp4/single_segment_segmenter.h"
#include "packager/media/formats/ttml/ttml_generator.h" #include "packager/media/formats/ttml/ttml_generator.h"
@ -298,6 +299,9 @@ Status MP4Muxer::DelayInitializeMuxer() {
if (options().segment_template.empty()) { if (options().segment_template.empty()) {
segmenter_.reset(new SingleSegmentSegmenter(options(), std::move(ftyp), segmenter_.reset(new SingleSegmentSegmenter(options(), std::move(ftyp),
std::move(moov))); std::move(moov)));
} else if (options().mp4_params.low_latency_dash_mode) {
segmenter_.reset(new LowLatencySegmentSegmenter(options(), std::move(ftyp),
std::move(moov)));
} else { } else {
segmenter_.reset( segmenter_.reset(
new MultiSegmentSegmenter(options(), std::move(ftyp), std::move(moov))); new MultiSegmentSegmenter(options(), std::move(ftyp), std::move(moov)));

View File

@ -225,7 +225,16 @@ Status Segmenter::FinalizeSegment(size_t stream_id,
for (std::unique_ptr<Fragmenter>& fragmenter : fragmenters_) for (std::unique_ptr<Fragmenter>& fragmenter : fragmenters_)
fragmenter->ClearFragmentFinalized(); fragmenter->ClearFragmentFinalized();
if (!segment_info.is_subsegment) {
if (segment_info.is_chunk) {
// Finalize the completed chunk for the LL-DASH case.
Status status = DoFinalizeChunk();
if (!status.ok())
return status;
}
if (!segment_info.is_subsegment || segment_info.is_final_chunk_in_seg) {
// Finalize the segment.
Status status = DoFinalizeSegment(); Status status = DoFinalizeSegment();
// Reset segment information to initial state. // Reset segment information to initial state.
sidx_->references.clear(); sidx_->references.clear();

View File

@ -126,6 +126,8 @@ class Segmenter {
virtual Status DoFinalize() = 0; virtual Status DoFinalize() = 0;
virtual Status DoFinalizeSegment() = 0; virtual Status DoFinalizeSegment() = 0;
virtual Status DoFinalizeChunk() { return Status::OK; }
uint32_t GetReferenceStreamId(); uint32_t GetReferenceStreamId();
void FinalizeFragmentForKeyRotation( void FinalizeFragmentForKeyRotation(

View File

@ -25,6 +25,12 @@ struct ChunkingParams {
/// Setting to subsegment_sap_aligned to true but segment_sap_aligned to false /// Setting to subsegment_sap_aligned to true but segment_sap_aligned to false
/// is not allowed. /// is not allowed.
bool subsegment_sap_aligned = true; bool subsegment_sap_aligned = true;
/// Enable LL-DASH streaming.
/// Each segment constists of many fragments, and each fragment contains one
/// chunk. A chunk is the smallest unit and is constructed of a single moof
/// and mdat atom. Each chunk is uploaded immediately upon creation,
/// decoupling latency from segment duration.
bool low_latency_dash_mode = false;
}; };
} // namespace shaka } // namespace shaka

View File

@ -20,6 +20,12 @@ struct Mp4OutputParams {
/// Note that it is required by spec if segment_template contains $Times$ /// Note that it is required by spec if segment_template contains $Times$
/// specifier. /// specifier.
bool generate_sidx_in_media_segments = true; bool generate_sidx_in_media_segments = true;
/// Enable LL-DASH streaming.
/// Each segment constists of many fragments, and each fragment contains one
/// chunk. A chunk is the smallest unit and is constructed of a single moof
/// and mdat atom. Each chunk is uploaded immediately upon creation,
/// decoupling latency from segment duration.
bool low_latency_dash_mode = false;
}; };
} // namespace shaka } // namespace shaka

View File

@ -204,4 +204,12 @@ message MediaInfo {
// Role value defined in "urn:mpeg:dash:role:2011" scheme or in the format: // Role value defined in "urn:mpeg:dash:role:2011" scheme or in the format:
// scheme_id_uri=value (to be implemented). // scheme_id_uri=value (to be implemented).
repeated string dash_roles = 22; repeated string dash_roles = 22;
// LOW LATENCY DASH only. Defines the availabilityTimeOffset in seconds.
// Equal to the segment time minus the chunk duration.
optional double availability_time_offset = 24;
// LOW LATENCY DASH only. Defines the segment duration
// with respect to the reference time scale.
// Equal to the target segment duration times the reference time scale.
optional uint64 segment_duration = 25;
} }

View File

@ -77,6 +77,8 @@ class MockRepresentation : public Representation {
void(const std::string& drm_uuid, const std::string& pssh)); void(const std::string& drm_uuid, const std::string& pssh));
MOCK_METHOD3(AddNewSegment, MOCK_METHOD3(AddNewSegment,
void(int64_t start_time, int64_t duration, uint64_t size)); void(int64_t start_time, int64_t duration, uint64_t size));
MOCK_METHOD0(SetSegmentDuration, void());
MOCK_METHOD0(SetAvailabilityTimeOffset, void());
MOCK_METHOD1(SetSampleDuration, void(int32_t sample_duration)); MOCK_METHOD1(SetSampleDuration, void(int32_t sample_duration));
MOCK_CONST_METHOD0(GetMediaInfo, const MediaInfo&()); MOCK_CONST_METHOD0(GetMediaInfo, const MediaInfo&());
}; };

View File

@ -31,6 +31,8 @@ class MockMpdNotifier : public MpdNotifier {
int64_t start_time, int64_t start_time,
int64_t duration, int64_t duration,
uint64_t size)); uint64_t size));
MOCK_METHOD1(NotifyAvailabilityTimeOffset, bool(uint32_t container_id));
MOCK_METHOD1(NotifySegmentDuration, bool(uint32_t container_id));
MOCK_METHOD2(NotifyCueEvent, bool(uint32_t container_id, int64_t timestamp)); MOCK_METHOD2(NotifyCueEvent, bool(uint32_t container_id, int64_t timestamp));
MOCK_METHOD4(NotifyEncryptionUpdate, MOCK_METHOD4(NotifyEncryptionUpdate,
bool(uint32_t container_id, bool(uint32_t container_id,

View File

@ -46,6 +46,15 @@ class MpdNotifier {
virtual bool NotifyNewContainer(const MediaInfo& media_info, virtual bool NotifyNewContainer(const MediaInfo& media_info,
uint32_t* container_id) = 0; uint32_t* container_id) = 0;
/// Record the availailityTimeOffset for Low Latency DASH streaming.
/// @param container_id Container ID obtained from calling
/// NotifyNewContainer().
/// @return true on success, false otherwise. This may fail if the container
/// specified by @a container_id does not exist.
virtual bool NotifyAvailabilityTimeOffset(uint32_t container_id) {
return true;
}
/// Change the sample duration of container with @a container_id. /// Change the sample duration of container with @a container_id.
/// @param container_id Container ID obtained from calling /// @param container_id Container ID obtained from calling
/// NotifyNewContainer(). /// NotifyNewContainer().
@ -56,6 +65,13 @@ class MpdNotifier {
virtual bool NotifySampleDuration(uint32_t container_id, virtual bool NotifySampleDuration(uint32_t container_id,
int32_t sample_duration) = 0; int32_t sample_duration) = 0;
/// Record the duration of a segment for Low Latency DASH streaming.
/// @param container_id Container ID obtained from calling
/// NotifyNewContainer().
/// @return true on success, false otherwise. This may fail if the container
/// specified by @a container_id does not exist.
virtual bool NotifySegmentDuration(uint32_t container_id) { return true; }
/// Notifies MpdBuilder that there is a new segment ready. For live, this /// Notifies MpdBuilder that there is a new segment ready. For live, this
/// is usually a new segment, for VOD this is usually a subsegment. /// is usually a new segment, for VOD this is usually a subsegment.
/// @param container_id Container ID obtained from calling /// @param container_id Container ID obtained from calling

View File

@ -136,6 +136,28 @@ base::Optional<xml::XmlNode> Period::GetXml(bool output_period_duration) {
// Required for 'dynamic' MPDs. // Required for 'dynamic' MPDs.
if (!period.SetId(id_)) if (!period.SetId(id_))
return base::nullopt; return base::nullopt;
// Required for LL-DASH MPDs.
if (mpd_options_.mpd_params.low_latency_dash_mode) {
// Create ServiceDescription element.
xml::XmlNode service_description_node("ServiceDescription");
if (!service_description_node.SetIntegerAttribute("id", id_))
return base::nullopt;
// Insert Latency into ServiceDescription element.
xml::XmlNode latency_node("Latency");
uint64_t target_latency_ms =
mpd_options_.mpd_params.target_latency_seconds * 1000;
if (!latency_node.SetIntegerAttribute("target", target_latency_ms))
return base::nullopt;
if (!service_description_node.AddChild(std::move(latency_node)))
return base::nullopt;
// Insert ServiceDescription into Period element.
if (!period.AddChild(std::move(service_description_node)))
return base::nullopt;
}
// Iterate thru AdaptationSets and add them to one big Period element. // Iterate thru AdaptationSets and add them to one big Period element.
for (const auto& adaptation_set : adaptation_sets_) { for (const auto& adaptation_set : adaptation_sets_) {
auto child = adaptation_set->GetXml(); auto child = adaptation_set->GetXml();

View File

@ -173,6 +173,46 @@ TEST_F(PeriodTest, DynamicMpdGetXml) {
XmlNodeEqual(kExpectedXml)); XmlNodeEqual(kExpectedXml));
} }
TEST_F(PeriodTest, LowLatencyDashMpdGetXml) {
const char kVideoMediaInfo[] =
"video_info {\n"
" codec: 'avc1'\n"
" width: 1280\n"
" height: 720\n"
" time_scale: 10\n"
" frame_duration: 10\n"
" pixel_width: 1\n"
" pixel_height: 1\n"
"}\n"
"container_type: 1\n";
mpd_options_.mpd_type = MpdType::kDynamic;
mpd_options_.mpd_params.low_latency_dash_mode = true;
mpd_options_.mpd_params.target_latency_seconds = 1;
EXPECT_CALL(testable_period_, NewAdaptationSet(_, _, _))
.WillOnce(Return(ByMove(std::move(default_adaptation_set_))));
ASSERT_EQ(default_adaptation_set_ptr_,
testable_period_.GetOrCreateAdaptationSet(
ConvertToMediaInfo(kVideoMediaInfo),
content_protection_in_adaptation_set_));
const char kExpectedXml[] =
"<Period id=\"9\" start=\"PT5.6S\">"
// LL-DASH standards require ServiceDescription and Latency elements
" <ServiceDescription id=\"9\" >"
// In LL-DASH MPD, the target latency is in ms, so the expected value is
// 1000.
" <Latency target=\"1000\"/>"
" </ServiceDescription>"
// ContentType and Representation elements are populated after
// Representation::Init() is called.
" <AdaptationSet contentType=\"\"/>"
"</Period>";
EXPECT_THAT(testable_period_.GetXml(!kOutputPeriodDuration),
XmlNodeEqual(kExpectedXml));
}
TEST_F(PeriodTest, SetDurationAndGetXml) { TEST_F(PeriodTest, SetDurationAndGetXml) {
const char kVideoMediaInfo[] = const char kVideoMediaInfo[] =
"video_info {\n" "video_info {\n"

View File

@ -208,6 +208,14 @@ void Representation::SetSampleDuration(int32_t frame_duration) {
} }
} }
void Representation::SetSegmentDuration() {
int64_t sd = mpd_options_.mpd_params.target_segment_duration *
media_info_.reference_time_scale();
if (sd <= 0)
return;
media_info_.set_segment_duration(sd);
}
const MediaInfo& Representation::GetMediaInfo() const { const MediaInfo& Representation::GetMediaInfo() const {
return media_info_; return media_info_;
} }
@ -273,8 +281,9 @@ base::Optional<xml::XmlNode> Representation::GetXml() {
} }
if (HasLiveOnlyFields(media_info_) && if (HasLiveOnlyFields(media_info_) &&
!representation.AddLiveOnlyInfo(media_info_, segment_infos_, !representation.AddLiveOnlyInfo(
start_number_)) { media_info_, segment_infos_, start_number_,
mpd_options_.mpd_params.low_latency_dash_mode)) {
LOG(ERROR) << "Failed to add Live info."; LOG(ERROR) << "Failed to add Live info.";
return base::nullopt; return base::nullopt;
} }
@ -297,6 +306,23 @@ void Representation::SetPresentationTimeOffset(
media_info_.set_presentation_time_offset(pto); media_info_.set_presentation_time_offset(pto);
} }
void Representation::SetAvailabilityTimeOffset() {
// Adjust the frame duration to units of seconds to match target segment
// duration.
const double frame_duration_sec =
(double)frame_duration_ / (double)media_info_.reference_time_scale();
// availabilityTimeOffset = segment duration - chunk duration.
// Here, the frame duration is equivalent to the sample duration,
// see Representation::SetSampleDuration(uint32_t frame_duration).
// By definition, each chunk will contain only one sample;
// thus, chunk_duration = sample_duration = frame_duration.
const double ato =
mpd_options_.mpd_params.target_segment_duration - frame_duration_sec;
if (ato <= 0)
return;
media_info_.set_availability_time_offset(ato);
}
bool Representation::GetStartAndEndTimestamps( bool Representation::GetStartAndEndTimestamps(
double* start_timestamp_seconds, double* start_timestamp_seconds,
double* end_timestamp_seconds) const { double* end_timestamp_seconds) const {

View File

@ -127,6 +127,14 @@ class Representation {
/// Set @presentationTimeOffset in SegmentBase / SegmentTemplate. /// Set @presentationTimeOffset in SegmentBase / SegmentTemplate.
void SetPresentationTimeOffset(double presentation_time_offset); void SetPresentationTimeOffset(double presentation_time_offset);
/// Set @availabilityTimeOffset in SegmentTemplate.
/// This is necessary for Low Latency DASH streaming.
void SetAvailabilityTimeOffset();
/// Set @duration in SegmentTemplate.
/// This is necessary for Low Latency DASH streaming.
void SetSegmentDuration();
/// Gets the start and end timestamps in seconds. /// Gets the start and end timestamps in seconds.
/// @param start_timestamp_seconds contains the returned start timestamp in /// @param start_timestamp_seconds contains the returned start timestamp in
/// seconds on success. It can be nullptr, which means that start /// seconds on success. It can be nullptr, which means that start

View File

@ -71,6 +71,17 @@ bool SimpleMpdNotifier::NotifyNewContainer(const MediaInfo& media_info,
return true; return true;
} }
bool SimpleMpdNotifier::NotifyAvailabilityTimeOffset(uint32_t container_id) {
base::AutoLock auto_lock(lock_);
auto it = representation_map_.find(container_id);
if (it == representation_map_.end()) {
LOG(ERROR) << "Unexpected container_id: " << container_id;
return false;
}
it->second->SetAvailabilityTimeOffset();
return true;
}
bool SimpleMpdNotifier::NotifySampleDuration(uint32_t container_id, bool SimpleMpdNotifier::NotifySampleDuration(uint32_t container_id,
int32_t sample_duration) { int32_t sample_duration) {
base::AutoLock auto_lock(lock_); base::AutoLock auto_lock(lock_);
@ -83,6 +94,17 @@ bool SimpleMpdNotifier::NotifySampleDuration(uint32_t container_id,
return true; return true;
} }
bool SimpleMpdNotifier::NotifySegmentDuration(uint32_t container_id) {
base::AutoLock auto_lock(lock_);
auto it = representation_map_.find(container_id);
if (it == representation_map_.end()) {
LOG(ERROR) << "Unexpected container_id: " << container_id;
return false;
}
it->second->SetSegmentDuration();
return true;
}
bool SimpleMpdNotifier::NotifyNewSegment(uint32_t container_id, bool SimpleMpdNotifier::NotifyNewSegment(uint32_t container_id,
int64_t start_time, int64_t start_time,
int64_t duration, int64_t duration,

View File

@ -36,8 +36,10 @@ class SimpleMpdNotifier : public MpdNotifier {
/// @{ /// @{
bool Init() override; bool Init() override;
bool NotifyNewContainer(const MediaInfo& media_info, uint32_t* id) override; bool NotifyNewContainer(const MediaInfo& media_info, uint32_t* id) override;
bool NotifyAvailabilityTimeOffset(uint32_t container_id) override;
bool NotifySampleDuration(uint32_t container_id, bool NotifySampleDuration(uint32_t container_id,
int32_t sample_duration) override; int32_t sample_duration) override;
bool NotifySegmentDuration(uint32_t container_id) override;
bool NotifyNewSegment(uint32_t container_id, bool NotifyNewSegment(uint32_t container_id,
int64_t start_time, int64_t start_time,
int64_t duration, int64_t duration,

View File

@ -151,6 +151,56 @@ TEST_F(SimpleMpdNotifierTest, NotifySampleDuration) {
notifier.NotifySampleDuration(kRepresentationId, kSampleDuration)); notifier.NotifySampleDuration(kRepresentationId, kSampleDuration));
} }
TEST_F(SimpleMpdNotifierTest, NotifySegmentDuration) {
SimpleMpdNotifier notifier(empty_mpd_option_);
const uint32_t kRepresentationId = 9u;
std::unique_ptr<MockMpdBuilder> mock_mpd_builder(new MockMpdBuilder());
std::unique_ptr<MockRepresentation> mock_representation(
new MockRepresentation(kRepresentationId));
EXPECT_CALL(*mock_mpd_builder, GetOrCreatePeriod(_))
.WillOnce(Return(default_mock_period_.get()));
EXPECT_CALL(*default_mock_period_, GetOrCreateAdaptationSet(_, _))
.WillOnce(Return(default_mock_adaptation_set_.get()));
EXPECT_CALL(*default_mock_adaptation_set_, AddRepresentation(_))
.WillOnce(Return(mock_representation.get()));
uint32_t container_id;
SetMpdBuilder(&notifier, std::move(mock_mpd_builder));
EXPECT_TRUE(notifier.NotifyNewContainer(valid_media_info1_, &container_id));
EXPECT_EQ(kRepresentationId, container_id);
mock_representation->SetSegmentDuration();
EXPECT_TRUE(notifier.NotifySegmentDuration(kRepresentationId));
}
TEST_F(SimpleMpdNotifierTest, NotifyAvailabilityTimeOffset) {
SimpleMpdNotifier notifier(empty_mpd_option_);
const uint32_t kRepresentationId = 10u;
std::unique_ptr<MockMpdBuilder> mock_mpd_builder(new MockMpdBuilder());
std::unique_ptr<MockRepresentation> mock_representation(
new MockRepresentation(kRepresentationId));
EXPECT_CALL(*mock_mpd_builder, GetOrCreatePeriod(_))
.WillOnce(Return(default_mock_period_.get()));
EXPECT_CALL(*default_mock_period_, GetOrCreateAdaptationSet(_, _))
.WillOnce(Return(default_mock_adaptation_set_.get()));
EXPECT_CALL(*default_mock_adaptation_set_, AddRepresentation(_))
.WillOnce(Return(mock_representation.get()));
uint32_t container_id;
SetMpdBuilder(&notifier, std::move(mock_mpd_builder));
EXPECT_TRUE(notifier.NotifyNewContainer(valid_media_info1_, &container_id));
EXPECT_EQ(kRepresentationId, container_id);
mock_representation->SetAvailabilityTimeOffset();
EXPECT_TRUE(notifier.NotifyAvailabilityTimeOffset(kRepresentationId));
}
// This test is mainly for tsan. Using both the notifier and the MpdBuilder. // This test is mainly for tsan. Using both the notifier and the MpdBuilder.
// Although locks in MpdBuilder have been removed, // Although locks in MpdBuilder have been removed,
// https://github.com/google/shaka-packager/issues/45 // https://github.com/google/shaka-packager/issues/45

View File

@ -460,18 +460,29 @@ bool RepresentationXmlNode::AddVODOnlyInfo(const MediaInfo& media_info,
bool RepresentationXmlNode::AddLiveOnlyInfo( bool RepresentationXmlNode::AddLiveOnlyInfo(
const MediaInfo& media_info, const MediaInfo& media_info,
const std::list<SegmentInfo>& segment_infos, const std::list<SegmentInfo>& segment_infos,
uint32_t start_number) { uint32_t start_number,
bool low_latency_dash_mode) {
XmlNode segment_template("SegmentTemplate"); XmlNode segment_template("SegmentTemplate");
if (media_info.has_reference_time_scale()) { if (media_info.has_reference_time_scale()) {
RCHECK(segment_template.SetIntegerAttribute( RCHECK(segment_template.SetIntegerAttribute(
"timescale", media_info.reference_time_scale())); "timescale", media_info.reference_time_scale()));
} }
if (media_info.has_segment_duration()) {
RCHECK(segment_template.SetIntegerAttribute("duration",
media_info.segment_duration()));
}
if (media_info.has_presentation_time_offset()) { if (media_info.has_presentation_time_offset()) {
RCHECK(segment_template.SetIntegerAttribute( RCHECK(segment_template.SetIntegerAttribute(
"presentationTimeOffset", media_info.presentation_time_offset())); "presentationTimeOffset", media_info.presentation_time_offset()));
} }
if (media_info.has_availability_time_offset()) {
RCHECK(segment_template.SetFloatingPointAttribute(
"availabilityTimeOffset", media_info.availability_time_offset()));
}
if (media_info.has_init_segment_url()) { if (media_info.has_init_segment_url()) {
RCHECK(segment_template.SetStringAttribute("initialization", RCHECK(segment_template.SetStringAttribute("initialization",
media_info.init_segment_url())); media_info.init_segment_url()));
@ -499,9 +510,11 @@ bool RepresentationXmlNode::AddLiveOnlyInfo(
std::to_string(last_segment_number))); std::to_string(last_segment_number)));
} }
} else { } else {
XmlNode segment_timeline("SegmentTimeline"); if (!low_latency_dash_mode) {
RCHECK(PopulateSegmentTimeline(segment_infos, &segment_timeline)); XmlNode segment_timeline("SegmentTimeline");
RCHECK(segment_template.AddChild(std::move(segment_timeline))); RCHECK(PopulateSegmentTimeline(segment_infos, &segment_timeline));
RCHECK(segment_template.AddChild(std::move(segment_timeline)));
}
} }
} }
return AddChild(std::move(segment_template)); return AddChild(std::move(segment_template));

View File

@ -219,7 +219,8 @@ class RepresentationXmlNode : public RepresentationBaseXmlNode {
/// SegmentInfos are sorted by its start time. /// SegmentInfos are sorted by its start time.
bool AddLiveOnlyInfo(const MediaInfo& media_info, bool AddLiveOnlyInfo(const MediaInfo& media_info,
const std::list<SegmentInfo>& segment_infos, const std::list<SegmentInfo>& segment_infos,
uint32_t start_number) WARN_UNUSED_RESULT; uint32_t start_number,
bool low_latency_dash_mode) WARN_UNUSED_RESULT;
private: private:
// Add AudioChannelConfiguration element. Note that it is a required element // Add AudioChannelConfiguration element. Note that it is a required element

View File

@ -368,13 +368,14 @@ TEST_F(LiveSegmentTimelineTest, OneSegmentInfo) {
const int64_t kStartTime = 0; const int64_t kStartTime = 0;
const int64_t kDuration = 100; const int64_t kDuration = 100;
const uint64_t kRepeat = 9; const uint64_t kRepeat = 9;
const bool kIsLowLatency = false;
std::list<SegmentInfo> segment_infos = { std::list<SegmentInfo> segment_infos = {
{kStartTime, kDuration, kRepeat}, {kStartTime, kDuration, kRepeat},
}; };
RepresentationXmlNode representation; RepresentationXmlNode representation;
ASSERT_TRUE( ASSERT_TRUE(representation.AddLiveOnlyInfo(media_info_, segment_infos,
representation.AddLiveOnlyInfo(media_info_, segment_infos, kStartNumber)); kStartNumber, kIsLowLatency));
EXPECT_THAT( EXPECT_THAT(
representation, representation,
@ -389,13 +390,14 @@ TEST_F(LiveSegmentTimelineTest, OneSegmentInfoNonZeroStartTime) {
const int64_t kNonZeroStartTime = 500; const int64_t kNonZeroStartTime = 500;
const int64_t kDuration = 100; const int64_t kDuration = 100;
const uint64_t kRepeat = 9; const uint64_t kRepeat = 9;
const bool kIsLowLatency = false;
std::list<SegmentInfo> segment_infos = { std::list<SegmentInfo> segment_infos = {
{kNonZeroStartTime, kDuration, kRepeat}, {kNonZeroStartTime, kDuration, kRepeat},
}; };
RepresentationXmlNode representation; RepresentationXmlNode representation;
ASSERT_TRUE( ASSERT_TRUE(representation.AddLiveOnlyInfo(media_info_, segment_infos,
representation.AddLiveOnlyInfo(media_info_, segment_infos, kStartNumber)); kStartNumber, kIsLowLatency));
EXPECT_THAT(representation, EXPECT_THAT(representation,
XmlNodeEqual( XmlNodeEqual(
@ -413,13 +415,14 @@ TEST_F(LiveSegmentTimelineTest, OneSegmentInfoMatchingStartTimeAndNumber) {
const int64_t kNonZeroStartTime = 500; const int64_t kNonZeroStartTime = 500;
const int64_t kDuration = 100; const int64_t kDuration = 100;
const uint64_t kRepeat = 9; const uint64_t kRepeat = 9;
const bool kIsLowLatency = false;
std::list<SegmentInfo> segment_infos = { std::list<SegmentInfo> segment_infos = {
{kNonZeroStartTime, kDuration, kRepeat}, {kNonZeroStartTime, kDuration, kRepeat},
}; };
RepresentationXmlNode representation; RepresentationXmlNode representation;
ASSERT_TRUE( ASSERT_TRUE(representation.AddLiveOnlyInfo(media_info_, segment_infos,
representation.AddLiveOnlyInfo(media_info_, segment_infos, kStartNumber)); kStartNumber, kIsLowLatency));
EXPECT_THAT( EXPECT_THAT(
representation, representation,
@ -431,6 +434,7 @@ TEST_F(LiveSegmentTimelineTest, OneSegmentInfoMatchingStartTimeAndNumber) {
TEST_F(LiveSegmentTimelineTest, AllSegmentsSameDurationExpectLastOne) { TEST_F(LiveSegmentTimelineTest, AllSegmentsSameDurationExpectLastOne) {
const uint32_t kStartNumber = 1; const uint32_t kStartNumber = 1;
const bool kIsLowLatency = false;
const int64_t kStartTime1 = 0; const int64_t kStartTime1 = 0;
const int64_t kDuration1 = 100; const int64_t kDuration1 = 100;
@ -445,8 +449,8 @@ TEST_F(LiveSegmentTimelineTest, AllSegmentsSameDurationExpectLastOne) {
{kStartTime2, kDuration2, kRepeat2}, {kStartTime2, kDuration2, kRepeat2},
}; };
RepresentationXmlNode representation; RepresentationXmlNode representation;
ASSERT_TRUE( ASSERT_TRUE(representation.AddLiveOnlyInfo(media_info_, segment_infos,
representation.AddLiveOnlyInfo(media_info_, segment_infos, kStartNumber)); kStartNumber, kIsLowLatency));
EXPECT_THAT( EXPECT_THAT(
representation, representation,
@ -458,6 +462,7 @@ TEST_F(LiveSegmentTimelineTest, AllSegmentsSameDurationExpectLastOne) {
TEST_F(LiveSegmentTimelineTest, SecondSegmentInfoNonZeroRepeat) { TEST_F(LiveSegmentTimelineTest, SecondSegmentInfoNonZeroRepeat) {
const uint32_t kStartNumber = 1; const uint32_t kStartNumber = 1;
const bool kIsLowLatency = false;
const int64_t kStartTime1 = 0; const int64_t kStartTime1 = 0;
const int64_t kDuration1 = 100; const int64_t kDuration1 = 100;
@ -472,8 +477,8 @@ TEST_F(LiveSegmentTimelineTest, SecondSegmentInfoNonZeroRepeat) {
{kStartTime2, kDuration2, kRepeat2}, {kStartTime2, kDuration2, kRepeat2},
}; };
RepresentationXmlNode representation; RepresentationXmlNode representation;
ASSERT_TRUE( ASSERT_TRUE(representation.AddLiveOnlyInfo(media_info_, segment_infos,
representation.AddLiveOnlyInfo(media_info_, segment_infos, kStartNumber)); kStartNumber, kIsLowLatency));
EXPECT_THAT(representation, EXPECT_THAT(representation,
XmlNodeEqual( XmlNodeEqual(
@ -489,6 +494,7 @@ TEST_F(LiveSegmentTimelineTest, SecondSegmentInfoNonZeroRepeat) {
TEST_F(LiveSegmentTimelineTest, TwoSegmentInfoWithGap) { TEST_F(LiveSegmentTimelineTest, TwoSegmentInfoWithGap) {
const uint32_t kStartNumber = 1; const uint32_t kStartNumber = 1;
const bool kIsLowLatency = false;
const int64_t kStartTime1 = 0; const int64_t kStartTime1 = 0;
const int64_t kDuration1 = 100; const int64_t kDuration1 = 100;
@ -504,8 +510,8 @@ TEST_F(LiveSegmentTimelineTest, TwoSegmentInfoWithGap) {
{kStartTime2, kDuration2, kRepeat2}, {kStartTime2, kDuration2, kRepeat2},
}; };
RepresentationXmlNode representation; RepresentationXmlNode representation;
ASSERT_TRUE( ASSERT_TRUE(representation.AddLiveOnlyInfo(media_info_, segment_infos,
representation.AddLiveOnlyInfo(media_info_, segment_infos, kStartNumber)); kStartNumber, kIsLowLatency));
EXPECT_THAT(representation, EXPECT_THAT(representation,
XmlNodeEqual( XmlNodeEqual(
@ -524,6 +530,7 @@ TEST_F(LiveSegmentTimelineTest, LastSegmentNumberSupplementalProperty) {
const int64_t kStartTime = 0; const int64_t kStartTime = 0;
const int64_t kDuration = 100; const int64_t kDuration = 100;
const uint64_t kRepeat = 9; const uint64_t kRepeat = 9;
const bool kIsLowLatency = false;
std::list<SegmentInfo> segment_infos = { std::list<SegmentInfo> segment_infos = {
{kStartTime, kDuration, kRepeat}, {kStartTime, kDuration, kRepeat},
@ -531,8 +538,8 @@ TEST_F(LiveSegmentTimelineTest, LastSegmentNumberSupplementalProperty) {
RepresentationXmlNode representation; RepresentationXmlNode representation;
FLAGS_dash_add_last_segment_number_when_needed = true; FLAGS_dash_add_last_segment_number_when_needed = true;
ASSERT_TRUE( ASSERT_TRUE(representation.AddLiveOnlyInfo(media_info_, segment_infos,
representation.AddLiveOnlyInfo(media_info_, segment_infos, kStartNumber)); kStartNumber, kIsLowLatency));
EXPECT_THAT( EXPECT_THAT(
representation, representation,
@ -715,5 +722,41 @@ TEST_F(OnDemandVODSegmentTest, SegmentUrlWithMediaRanges) {
"</Representation>")); "</Representation>"));
} }
class LowLatencySegmentTest : public ::testing::Test {
protected:
void SetUp() override {
media_info_.set_init_segment_url("init.m4s");
media_info_.set_segment_template_url("$Number$.m4s");
media_info_.set_reference_time_scale(90000);
media_info_.set_availability_time_offset(4.9750987314);
media_info_.set_segment_duration(450000);
}
MediaInfo media_info_;
};
TEST_F(LowLatencySegmentTest, LowLatencySegmentTemplate) {
const uint32_t kStartNumber = 1;
const uint64_t kDuration = 100;
const uint64_t kRepeat = 0;
const bool kIsLowLatency = true;
std::list<SegmentInfo> segment_infos = {
{kStartNumber, kDuration, kRepeat},
};
RepresentationXmlNode representation;
ASSERT_TRUE(representation.AddLiveOnlyInfo(media_info_, segment_infos,
kStartNumber, kIsLowLatency));
EXPECT_THAT(
representation,
XmlNodeEqual("<Representation>"
" <SegmentTemplate timescale=\"90000\" duration=\"450000\" "
" availabilityTimeOffset=\"4.9750987314\" "
" initialization=\"init.m4s\" "
" media=\"$Number$.m4s\" "
" startNumber=\"1\"/>"
"</Representation>"));
}
} // namespace xml } // namespace xml
} // namespace shaka } // namespace shaka

View File

@ -91,6 +91,17 @@ struct MpdParams {
/// content is huge and the total number of (sub)segment references /// content is huge and the total number of (sub)segment references
/// is greater than what the sidx atom allows (65535). /// is greater than what the sidx atom allows (65535).
bool use_segment_list = false; bool use_segment_list = false;
/// Enable LL-DASH streaming.
/// Each segment constists of many fragments, and each fragment contains one
/// chunk. A chunk is the smallest unit and is constructed of a single moof
/// and mdat atom. Each chunk is uploaded immediately upon creation,
/// decoupling latency from segment duration.
bool low_latency_dash_mode = false;
/// This is the target latency in seconds requested by the user. The actual
/// latency may be different to the target latency
/// and is greatly influnced by the player.
/// This parameter is required by DASH-IF Low Latency standards.
double target_latency_seconds = 1;
}; };
} // namespace shaka } // namespace shaka

View File

@ -374,6 +374,27 @@ Status ValidateParams(const PackagingParams& packaging_params,
"on-demand profile (not using segment_template or segment list)."); "on-demand profile (not using segment_template or segment list).");
} }
if (packaging_params.chunking_params.low_latency_dash_mode &&
packaging_params.chunking_params.subsegment_duration_in_seconds) {
// Low latency streaming requires data to be shipped as chunks,
// the smallest unit of video. Right now, each chunk contains
// one frame. Therefore, in low latency mode,
// a user specified --fragment_duration is irrelevant.
// TODO(caitlinocallaghan): Add a feature for users to specify the number
// of desired frames per chunk.
return Status(error::INVALID_ARGUMENT,
"--fragment_duration cannot be set "
"if --low_latency_dash_mode is enabled.");
}
if (packaging_params.mpd_params.low_latency_dash_mode &&
packaging_params.mpd_params.utc_timings.empty()) {
// Low latency DASH MPD requires a UTC Timing value
return Status(error::INVALID_ARGUMENT,
"--utc_timings must be be set "
"if --low_latency_dash_mode is enabled.");
}
return Status::OK; return Status::OK;
} }

View File

@ -39,6 +39,7 @@ const uint8_t kKey[]{
0x3a, 0xed, 0xde, 0xc0, 0xbc, 0x42, 0x1f, 0x4d, 0x3a, 0xed, 0xde, 0xc0, 0xbc, 0x42, 0x1f, 0x4d,
}; };
const double kClearLeadInSeconds = 1.0; const double kClearLeadInSeconds = 1.0;
const double kFragmentDurationInSeconds = 5.0;
} // namespace } // namespace
@ -266,6 +267,27 @@ TEST_F(PackagerTest, ReadFromBufferFailed) {
ASSERT_EQ(error::FILE_FAILURE, packager.Run().error_code()); ASSERT_EQ(error::FILE_FAILURE, packager.Run().error_code());
} }
TEST_F(PackagerTest, LowLatencyDashEnabledAndFragmentDurationSet) {
auto packaging_params = SetupPackagingParams();
packaging_params.chunking_params.low_latency_dash_mode = true;
packaging_params.chunking_params.subsegment_duration_in_seconds =
kFragmentDurationInSeconds;
Packager packager;
auto status = packager.Initialize(packaging_params, SetupStreamDescriptors());
ASSERT_EQ(error::INVALID_ARGUMENT, status.error_code());
EXPECT_THAT(status.error_message(),
HasSubstr("--fragment_duration cannot be set"));
}
TEST_F(PackagerTest, LowLatencyDashEnabledAndUtcTimingNotSet) {
auto packaging_params = SetupPackagingParams();
packaging_params.mpd_params.low_latency_dash_mode = true;
Packager packager;
auto status = packager.Initialize(packaging_params, SetupStreamDescriptors());
ASSERT_EQ(error::INVALID_ARGUMENT, status.error_code());
EXPECT_THAT(status.error_message(),
HasSubstr("--utc_timings must be be set"));
}
// TODO(kqyang): Add more tests. // TODO(kqyang): Add more tests.
} // namespace shaka } // namespace shaka