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:
parent
ac125564b9
commit
cd018a71c3
|
@ -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.
|
|
@ -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>`_
|
|
@ -13,3 +13,4 @@ Tutorials
|
||||||
ads.rst
|
ads.rst
|
||||||
ffmpeg_piping.rst
|
ffmpeg_piping.rst
|
||||||
http_upload.rst
|
http_upload.rst
|
||||||
|
low_latency.rst
|
||||||
|
|
|
@ -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.");
|
||||||
|
|
|
@ -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_
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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_);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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_) {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
|
@ -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_
|
|
@ -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',
|
||||||
|
|
|
@ -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)));
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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&());
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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(¬ifier, 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(¬ifier, 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
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue