Integrate ChunkingHandler
Also moved MediaHandler output validation to Initialize instead. This CL also addresses #122 with consistent chunking. Change-Id: I60c0da6d1b33421d7828bcb827d18899e71884ce
This commit is contained in:
parent
a3ce51785a
commit
160700b452
|
@ -34,6 +34,7 @@
|
|||
#include "packager/media/base/key_source.h"
|
||||
#include "packager/media/base/muxer_options.h"
|
||||
#include "packager/media/base/muxer_util.h"
|
||||
#include "packager/media/chunking/chunking_handler.h"
|
||||
#include "packager/media/event/hls_notify_muxer_listener.h"
|
||||
#include "packager/media/event/mpd_notify_muxer_listener.h"
|
||||
#include "packager/media/event/vod_media_info_dump_muxer_listener.h"
|
||||
|
@ -235,6 +236,7 @@ std::shared_ptr<Muxer> CreateOutputMuxer(const MuxerOptions& options,
|
|||
}
|
||||
|
||||
bool CreateRemuxJobs(const StreamDescriptorList& stream_descriptors,
|
||||
const ChunkingOptions& chunking_options,
|
||||
const MuxerOptions& muxer_options,
|
||||
FakeClock* fake_clock,
|
||||
KeySource* key_source,
|
||||
|
@ -361,11 +363,15 @@ bool CreateRemuxJobs(const StreamDescriptorList& stream_descriptors,
|
|||
if (muxer_listener)
|
||||
muxer->SetMuxerListener(std::move(muxer_listener));
|
||||
|
||||
auto chunking_handler = std::make_shared<ChunkingHandler>(chunking_options);
|
||||
Status status = chunking_handler->SetHandler(0, std::move(muxer));
|
||||
|
||||
auto* demuxer = remux_jobs->back()->demuxer();
|
||||
const std::string& stream_selector = stream_iter->stream_selector;
|
||||
Status status = demuxer->SetHandler(stream_selector, std::move(muxer));
|
||||
status.Update(demuxer->SetHandler(stream_selector, chunking_handler));
|
||||
|
||||
if (!status.ok()) {
|
||||
LOG(ERROR) << "Demuxer::SetHandler failed " << status;
|
||||
LOG(ERROR) << "Failed to setup graph: " << status;
|
||||
return false;
|
||||
}
|
||||
if (!stream_iter->language.empty())
|
||||
|
@ -426,10 +432,8 @@ bool RunPackager(const StreamDescriptorList& stream_descriptors) {
|
|||
return false;
|
||||
}
|
||||
|
||||
// Get basic muxer options.
|
||||
MuxerOptions muxer_options;
|
||||
if (!GetMuxerOptions(&muxer_options))
|
||||
return false;
|
||||
ChunkingOptions chunking_options = GetChunkingOptions();
|
||||
MuxerOptions muxer_options = GetMuxerOptions();
|
||||
|
||||
DCHECK(!stream_descriptors.empty());
|
||||
// On demand profile generates single file segment while live profile
|
||||
|
@ -451,9 +455,7 @@ bool RunPackager(const StreamDescriptorList& stream_descriptors) {
|
|||
return false;
|
||||
}
|
||||
|
||||
MpdOptions mpd_options;
|
||||
if (!GetMpdOptions(on_demand_dash_profile, &mpd_options))
|
||||
return false;
|
||||
MpdOptions mpd_options = GetMpdOptions(on_demand_dash_profile);
|
||||
|
||||
// Create encryption key source if needed.
|
||||
std::unique_ptr<KeySource> encryption_key_source;
|
||||
|
@ -495,9 +497,9 @@ bool RunPackager(const StreamDescriptorList& stream_descriptors) {
|
|||
|
||||
std::vector<std::unique_ptr<RemuxJob>> remux_jobs;
|
||||
FakeClock fake_clock;
|
||||
if (!CreateRemuxJobs(stream_descriptors, muxer_options, &fake_clock,
|
||||
encryption_key_source.get(), mpd_notifier.get(),
|
||||
hls_notifier.get(), &remux_jobs)) {
|
||||
if (!CreateRemuxJobs(stream_descriptors, chunking_options, muxer_options,
|
||||
&fake_clock, encryption_key_source.get(),
|
||||
mpd_notifier.get(), hls_notifier.get(), &remux_jobs)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
#include "packager/media/base/playready_key_source.h"
|
||||
#include "packager/media/base/request_signer.h"
|
||||
#include "packager/media/base/widevine_key_source.h"
|
||||
#include "packager/media/chunking/chunking_handler.h"
|
||||
#include "packager/media/file/file.h"
|
||||
#include "packager/mpd/base/mpd_options.h"
|
||||
|
||||
|
@ -146,45 +147,45 @@ std::unique_ptr<KeySource> CreateDecryptionKeySource() {
|
|||
return decryption_key_source;
|
||||
}
|
||||
|
||||
bool GetMuxerOptions(MuxerOptions* muxer_options) {
|
||||
DCHECK(muxer_options);
|
||||
ChunkingOptions GetChunkingOptions() {
|
||||
ChunkingOptions chunking_options;
|
||||
chunking_options.segment_duration_in_seconds = FLAGS_segment_duration;
|
||||
chunking_options.subsegment_duration_in_seconds = FLAGS_fragment_duration;
|
||||
chunking_options.segment_sap_aligned = FLAGS_segment_sap_aligned;
|
||||
chunking_options.subsegment_sap_aligned = FLAGS_fragment_sap_aligned;
|
||||
return chunking_options;
|
||||
}
|
||||
|
||||
muxer_options->segment_duration = FLAGS_segment_duration;
|
||||
muxer_options->fragment_duration = FLAGS_fragment_duration;
|
||||
muxer_options->segment_sap_aligned = FLAGS_segment_sap_aligned;
|
||||
muxer_options->fragment_sap_aligned = FLAGS_fragment_sap_aligned;
|
||||
muxer_options->num_subsegments_per_sidx = FLAGS_num_subsegments_per_sidx;
|
||||
muxer_options->webm_subsample_encryption = FLAGS_webm_subsample_encryption;
|
||||
MuxerOptions GetMuxerOptions() {
|
||||
MuxerOptions muxer_options;
|
||||
muxer_options.num_subsegments_per_sidx = FLAGS_num_subsegments_per_sidx;
|
||||
muxer_options.webm_subsample_encryption = FLAGS_webm_subsample_encryption;
|
||||
if (FLAGS_mp4_use_decoding_timestamp_in_timeline) {
|
||||
LOG(WARNING) << "Flag --mp4_use_decoding_timestamp_in_timeline is set. "
|
||||
"Note that it is a temporary hack to workaround Chromium "
|
||||
"bug https://crbug.com/398130. The flag may be removed "
|
||||
"when the Chromium bug is fixed.";
|
||||
}
|
||||
muxer_options->mp4_use_decoding_timestamp_in_timeline =
|
||||
muxer_options.mp4_use_decoding_timestamp_in_timeline =
|
||||
FLAGS_mp4_use_decoding_timestamp_in_timeline;
|
||||
|
||||
muxer_options->temp_dir = FLAGS_temp_dir;
|
||||
return true;
|
||||
muxer_options.temp_dir = FLAGS_temp_dir;
|
||||
return muxer_options;
|
||||
}
|
||||
|
||||
bool GetMpdOptions(bool on_demand_profile, MpdOptions* mpd_options) {
|
||||
DCHECK(mpd_options);
|
||||
|
||||
mpd_options->dash_profile =
|
||||
MpdOptions GetMpdOptions(bool on_demand_profile) {
|
||||
MpdOptions mpd_options;
|
||||
mpd_options.dash_profile =
|
||||
on_demand_profile ? DashProfile::kOnDemand : DashProfile::kLive;
|
||||
mpd_options->mpd_type =
|
||||
(on_demand_profile || FLAGS_generate_static_mpd)
|
||||
mpd_options.mpd_type = (on_demand_profile || FLAGS_generate_static_mpd)
|
||||
? MpdType::kStatic
|
||||
: MpdType::kDynamic;
|
||||
mpd_options->availability_time_offset = FLAGS_availability_time_offset;
|
||||
mpd_options->minimum_update_period = FLAGS_minimum_update_period;
|
||||
mpd_options->min_buffer_time = FLAGS_min_buffer_time;
|
||||
mpd_options->time_shift_buffer_depth = FLAGS_time_shift_buffer_depth;
|
||||
mpd_options->suggested_presentation_delay =
|
||||
FLAGS_suggested_presentation_delay;
|
||||
mpd_options->default_language = FLAGS_default_language;
|
||||
return true;
|
||||
mpd_options.availability_time_offset = FLAGS_availability_time_offset;
|
||||
mpd_options.minimum_update_period = FLAGS_minimum_update_period;
|
||||
mpd_options.min_buffer_time = FLAGS_min_buffer_time;
|
||||
mpd_options.time_shift_buffer_depth = FLAGS_time_shift_buffer_depth;
|
||||
mpd_options.suggested_presentation_delay = FLAGS_suggested_presentation_delay;
|
||||
mpd_options.default_language = FLAGS_default_language;
|
||||
return mpd_options;
|
||||
}
|
||||
|
||||
} // namespace media
|
||||
|
|
|
@ -22,6 +22,7 @@ struct MpdOptions;
|
|||
namespace media {
|
||||
|
||||
class KeySource;
|
||||
struct ChunkingOptions;
|
||||
struct MuxerOptions;
|
||||
|
||||
/// Create KeySource based on provided command line options for content
|
||||
|
@ -36,11 +37,14 @@ std::unique_ptr<KeySource> CreateEncryptionKeySource();
|
|||
/// decryption is not required.
|
||||
std::unique_ptr<KeySource> CreateDecryptionKeySource();
|
||||
|
||||
/// Fill MuxerOptions members using provided command line options.
|
||||
bool GetMuxerOptions(MuxerOptions* muxer_options);
|
||||
/// @return ChunkingOptions from provided command line options.
|
||||
ChunkingOptions GetChunkingOptions();
|
||||
|
||||
/// Fill MpdOptions members using provided command line options.
|
||||
bool GetMpdOptions(bool on_demand_profile, MpdOptions* mpd_options);
|
||||
/// @return MuxerOptions from provided command line options.
|
||||
MuxerOptions GetMuxerOptions();
|
||||
|
||||
/// @return MpdOptions from provided command line options.
|
||||
MpdOptions GetMpdOptions(bool on_demand_profile);
|
||||
|
||||
} // namespace media
|
||||
} // namespace shaka
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -3,12 +3,12 @@
|
|||
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
|
||||
#EXT-X-TARGETDURATION:2
|
||||
#EXT-X-PLAYLIST-TYPE:VOD
|
||||
#EXTINF:1.021,
|
||||
#EXTINF:0.975,
|
||||
output_audio-1.ts
|
||||
#EXTINF:0.998,
|
||||
output_audio-2.ts
|
||||
#EXT-X-DISCONTINUITY
|
||||
#EXT-X-KEY:METHOD=SAMPLE-AES,URI="data:text/plain;base64,MTIzNDU2Nzg5MDEyMzQ1Ng==",IV=0x3334353637383930,KEYFORMAT="identity"
|
||||
#EXTINF:1.021,
|
||||
output_audio-2.ts
|
||||
#EXTINF:0.720,
|
||||
#EXTINF:0.789,
|
||||
output_audio-3.ts
|
||||
#EXT-X-ENDLIST
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -3,10 +3,10 @@
|
|||
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
|
||||
#EXT-X-TARGETDURATION:2
|
||||
#EXT-X-PLAYLIST-TYPE:VOD
|
||||
#EXTINF:1.021,
|
||||
#EXTINF:0.975,
|
||||
output_audio-1.ts
|
||||
#EXTINF:1.021,
|
||||
#EXTINF:0.998,
|
||||
output_audio-2.ts
|
||||
#EXTINF:0.720,
|
||||
#EXTINF:0.789,
|
||||
output_audio-3.ts
|
||||
#EXT-X-ENDLIST
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,5 +1,5 @@
|
|||
#EXTM3U
|
||||
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
|
||||
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="stream_0",URI="audio.m3u8"
|
||||
#EXT-X-STREAM-INF:AUDIO="audio",CODECS="avc1.64001e,mp4a.40.2",BANDWIDTH=1217603
|
||||
#EXT-X-STREAM-INF:AUDIO="audio",CODECS="avc1.64001e,mp4a.40.2",BANDWIDTH=1217518
|
||||
video.m3u8
|
||||
|
|
|
@ -21,12 +21,13 @@
|
|||
<ContentProtection schemeIdUri="urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b">
|
||||
<cenc:pssh>AAAANHBzc2gBAAAAEHfv7MCyTQKs4zweUuL7SwAAAAExMjM0NTY3ODkwMTIzNDU2AAAAAA==</cenc:pssh>
|
||||
</ContentProtection>
|
||||
<Representation id="1" bandwidth="124195" codecs="mp4a.40.2" mimeType="audio/mp4" audioSamplingRate="44100">
|
||||
<Representation id="1" bandwidth="124560" codecs="mp4a.40.2" mimeType="audio/mp4" audioSamplingRate="44100">
|
||||
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
|
||||
<SegmentTemplate timescale="44100" initialization="output_audio-init.mp4" media="output_audio-$Number$.m4s" startNumber="1">
|
||||
<SegmentTimeline>
|
||||
<S t="0" d="45056" r="1"/>
|
||||
<S t="90112" d="31744"/>
|
||||
<S t="0" d="45056"/>
|
||||
<S t="45056" d="44032"/>
|
||||
<S t="89088" d="32768"/>
|
||||
</SegmentTimeline>
|
||||
</SegmentTemplate>
|
||||
</Representation>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
</Representation>
|
||||
</AdaptationSet>
|
||||
<AdaptationSet id="1" contentType="audio" segmentAlignment="true">
|
||||
<Representation id="1" bandwidth="124195" codecs="mp4a.40.2" mimeType="audio/mp4" audioSamplingRate="44100">
|
||||
<Representation id="1" bandwidth="124560" codecs="mp4a.40.2" mimeType="audio/mp4" audioSamplingRate="44100">
|
||||
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
|
||||
<ContentProtection value="cenc" schemeIdUri="urn:mpeg:dash:mp4protection:2011" cenc:default_KID="31323334-3536-3738-3930-313233343536"/>
|
||||
<ContentProtection schemeIdUri="urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b">
|
||||
|
@ -25,8 +25,9 @@
|
|||
</ContentProtection>
|
||||
<SegmentTemplate timescale="44100" initialization="output_audio-init.mp4" media="output_audio-$Number$.m4s" startNumber="1">
|
||||
<SegmentTimeline>
|
||||
<S t="0" d="45056" r="1"/>
|
||||
<S t="90112" d="31744"/>
|
||||
<S t="0" d="45056"/>
|
||||
<S t="45056" d="44032"/>
|
||||
<S t="89088" d="32768"/>
|
||||
</SegmentTimeline>
|
||||
</SegmentTemplate>
|
||||
</Representation>
|
||||
|
|
|
@ -17,12 +17,13 @@
|
|||
<AdaptationSet id="1" contentType="audio" segmentAlignment="true">
|
||||
<ContentProtection value="cenc" schemeIdUri="urn:mpeg:dash:mp4protection:2011" cenc:default_KID="00000000-0000-0000-0000-000000000000"/>
|
||||
<ContentProtection schemeIdUri="urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b"/>
|
||||
<Representation id="1" bandwidth="125175" codecs="mp4a.40.2" mimeType="audio/mp4" audioSamplingRate="44100">
|
||||
<Representation id="1" bandwidth="125525" codecs="mp4a.40.2" mimeType="audio/mp4" audioSamplingRate="44100">
|
||||
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
|
||||
<SegmentTemplate timescale="44100" initialization="output_audio-init.mp4" media="output_audio-$Number$.m4s" startNumber="1">
|
||||
<SegmentTimeline>
|
||||
<S t="0" d="45056" r="1"/>
|
||||
<S t="90112" d="31744"/>
|
||||
<S t="0" d="45056"/>
|
||||
<S t="45056" d="44032"/>
|
||||
<S t="89088" d="32768"/>
|
||||
</SegmentTimeline>
|
||||
</SegmentTemplate>
|
||||
</Representation>
|
||||
|
|
|
@ -15,14 +15,15 @@
|
|||
</Representation>
|
||||
</AdaptationSet>
|
||||
<AdaptationSet id="1" contentType="audio" segmentAlignment="true">
|
||||
<Representation id="1" bandwidth="125175" codecs="mp4a.40.2" mimeType="audio/mp4" audioSamplingRate="44100">
|
||||
<Representation id="1" bandwidth="125525" codecs="mp4a.40.2" mimeType="audio/mp4" audioSamplingRate="44100">
|
||||
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
|
||||
<ContentProtection value="cenc" schemeIdUri="urn:mpeg:dash:mp4protection:2011" cenc:default_KID="00000000-0000-0000-0000-000000000000"/>
|
||||
<ContentProtection schemeIdUri="urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b"/>
|
||||
<SegmentTemplate timescale="44100" initialization="output_audio-init.mp4" media="output_audio-$Number$.m4s" startNumber="1">
|
||||
<SegmentTimeline>
|
||||
<S t="0" d="45056" r="1"/>
|
||||
<S t="90112" d="31744"/>
|
||||
<S t="0" d="45056"/>
|
||||
<S t="45056" d="44032"/>
|
||||
<S t="89088" d="32768"/>
|
||||
</SegmentTimeline>
|
||||
</SegmentTemplate>
|
||||
</Representation>
|
||||
|
|
|
@ -13,12 +13,13 @@
|
|||
</Representation>
|
||||
</AdaptationSet>
|
||||
<AdaptationSet id="1" contentType="audio" segmentAlignment="true">
|
||||
<Representation id="1" bandwidth="121855" codecs="mp4a.40.2" mimeType="audio/mp4" audioSamplingRate="44100">
|
||||
<Representation id="1" bandwidth="122235" codecs="mp4a.40.2" mimeType="audio/mp4" audioSamplingRate="44100">
|
||||
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
|
||||
<SegmentTemplate timescale="44100" initialization="output_audio-init.mp4" media="output_audio-$Number$.m4s" startNumber="1">
|
||||
<SegmentTimeline>
|
||||
<S t="0" d="45056" r="1"/>
|
||||
<S t="90112" d="31744"/>
|
||||
<S t="0" d="45056"/>
|
||||
<S t="45056" d="44032"/>
|
||||
<S t="89088" d="32768"/>
|
||||
</SegmentTimeline>
|
||||
</SegmentTemplate>
|
||||
</Representation>
|
||||
|
|
|
@ -13,12 +13,13 @@
|
|||
</Representation>
|
||||
</AdaptationSet>
|
||||
<AdaptationSet id="1" contentType="audio" segmentAlignment="true">
|
||||
<Representation id="1" bandwidth="121855" codecs="mp4a.40.2" mimeType="audio/mp4" audioSamplingRate="44100">
|
||||
<Representation id="1" bandwidth="122235" codecs="mp4a.40.2" mimeType="audio/mp4" audioSamplingRate="44100">
|
||||
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
|
||||
<SegmentTemplate timescale="44100" initialization="output_audio-init.mp4" media="output_audio-$Number$.m4s" startNumber="1">
|
||||
<SegmentTimeline>
|
||||
<S t="0" d="45056" r="1"/>
|
||||
<S t="90112" d="31744"/>
|
||||
<S t="0" d="45056"/>
|
||||
<S t="45056" d="44032"/>
|
||||
<S t="89088" d="32768"/>
|
||||
</SegmentTimeline>
|
||||
</SegmentTemplate>
|
||||
</Representation>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#EXTM3U
|
||||
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
|
||||
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="stream_0",URI="audio.m3u8"
|
||||
#EXT-X-STREAM-INF:AUDIO="audio",CODECS="avc1.64001e,mp4a.40.2",BANDWIDTH=1217603
|
||||
#EXT-X-STREAM-INF:AUDIO="audio",CODECS="avc1.64001e,mp4a.40.2",BANDWIDTH=1217518
|
||||
video.m3u8
|
||||
|
|
|
@ -11,7 +11,7 @@ namespace media {
|
|||
|
||||
Status MediaHandler::SetHandler(int output_stream_index,
|
||||
std::shared_ptr<MediaHandler> handler) {
|
||||
if (!ValidateOutputStreamIndex(output_stream_index))
|
||||
if (output_stream_index < 0)
|
||||
return Status(error::INVALID_ARGUMENT, "Invalid output stream index");
|
||||
if (output_handlers_.find(output_stream_index) != output_handlers_.end()) {
|
||||
return Status(error::ALREADY_EXISTS,
|
||||
|
@ -30,6 +30,8 @@ Status MediaHandler::Initialize() {
|
|||
if (!status.ok())
|
||||
return status;
|
||||
for (auto& pair : output_handlers_) {
|
||||
if (!ValidateOutputStreamIndex(pair.first))
|
||||
return Status(error::INVALID_ARGUMENT, "Invalid output stream index");
|
||||
status = pair.second.first->Initialize();
|
||||
if (!status.ok())
|
||||
return status;
|
||||
|
|
|
@ -64,8 +64,12 @@ Status Muxer::Process(std::unique_ptr<StreamData> stream_data) {
|
|||
case StreamDataType::kStreamInfo:
|
||||
streams_.push_back(std::move(stream_data->stream_info));
|
||||
return InitializeMuxer();
|
||||
case StreamDataType::kSegmentInfo:
|
||||
return FinalizeSegment(stream_data->stream_index,
|
||||
std::move(stream_data->segment_info));
|
||||
case StreamDataType::kMediaSample:
|
||||
return DoAddSample(stream_data->media_sample);
|
||||
return AddSample(stream_data->stream_index,
|
||||
std::move(stream_data->media_sample));
|
||||
default:
|
||||
VLOG(3) << "Stream data type "
|
||||
<< static_cast<int>(stream_data->stream_data_type) << " ignored.";
|
||||
|
|
|
@ -123,8 +123,13 @@ class Muxer : public MediaHandler {
|
|||
// Final clean up.
|
||||
virtual Status Finalize() = 0;
|
||||
|
||||
// AddSample implementation.
|
||||
virtual Status DoAddSample(std::shared_ptr<MediaSample> sample) = 0;
|
||||
// Add a new sample.
|
||||
virtual Status AddSample(int stream_id,
|
||||
std::shared_ptr<MediaSample> sample) = 0;
|
||||
|
||||
// Finalize the segment or subsegment.
|
||||
virtual Status FinalizeSegment(int stream_id,
|
||||
std::shared_ptr<SegmentInfo> segment_info) = 0;
|
||||
|
||||
MuxerOptions options_;
|
||||
std::vector<std::shared_ptr<StreamInfo>> streams_;
|
||||
|
|
|
@ -19,25 +19,6 @@ struct MuxerOptions {
|
|||
MuxerOptions();
|
||||
~MuxerOptions();
|
||||
|
||||
/// Segment duration in seconds. If single_segment is specified, this
|
||||
/// parameter sets the duration of a subsegment; otherwise, this parameter
|
||||
/// sets the duration of a segment. A segment can contain one or many
|
||||
/// fragments.
|
||||
double segment_duration = 0;
|
||||
|
||||
/// Fragment duration in seconds. Should not be larger than the segment
|
||||
/// duration.
|
||||
double fragment_duration = 0;
|
||||
|
||||
/// Force segments to begin with stream access points. Segment duration may
|
||||
/// not be exactly what specified by segment_duration.
|
||||
bool segment_sap_aligned = false;
|
||||
|
||||
/// Force fragments to begin with stream access points. Fragment duration
|
||||
/// may not be exactly what specified by segment_duration. Setting to true
|
||||
/// implies that segment_sap_aligned is true as well.
|
||||
bool fragment_sap_aligned = false;
|
||||
|
||||
/// For ISO BMFF only.
|
||||
/// Set the number of subsegments in each SIDX box. If 0, a single SIDX box
|
||||
/// is used per segment. If -1, no SIDX box is used. Otherwise, the Muxer
|
||||
|
|
|
@ -142,6 +142,8 @@ Status ChunkingHandler::ProcessMediaSample(const MediaSample* sample) {
|
|||
const int64_t segment_index = timestamp / segment_duration_;
|
||||
if (segment_index != current_segment_index_) {
|
||||
current_segment_index_ = segment_index;
|
||||
// Reset subsegment index.
|
||||
current_subsegment_index_ = 0;
|
||||
new_segment = true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -86,6 +86,30 @@ TEST_F(ChunkingHandlerTest, AudioNoSubsegmentsThenFlush) {
|
|||
kDuration1 * 2, !kIsSubsegment)));
|
||||
}
|
||||
|
||||
TEST_F(ChunkingHandlerTest, AudioWithSubsegments) {
|
||||
ChunkingOptions chunking_options;
|
||||
chunking_options.segment_duration_in_seconds = 1;
|
||||
chunking_options.subsegment_duration_in_seconds = 0.5;
|
||||
SetUpChunkingHandler(1, chunking_options);
|
||||
|
||||
ASSERT_OK(Process(GetAudioStreamInfoStreamData(kStreamIndex0, kTimeScale0)));
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
ASSERT_OK(Process(GetMediaSampleStreamData(kStreamIndex0, i * kDuration1,
|
||||
kDuration1, kKeyFrame)));
|
||||
}
|
||||
EXPECT_THAT(
|
||||
GetOutputStreamDataVector(),
|
||||
ElementsAre(
|
||||
IsStreamInfo(kStreamIndex0, kTimeScale0, !kEncrypted),
|
||||
IsMediaSample(kStreamIndex0, 0, kDuration1),
|
||||
IsMediaSample(kStreamIndex0, kDuration1, kDuration1),
|
||||
IsSegmentInfo(kStreamIndex0, 0, kDuration1 * 2, kIsSubsegment),
|
||||
IsMediaSample(kStreamIndex0, 2 * kDuration1, kDuration1),
|
||||
IsSegmentInfo(kStreamIndex0, 0, kDuration1 * 3, !kIsSubsegment),
|
||||
IsMediaSample(kStreamIndex0, 3 * kDuration1, kDuration1),
|
||||
IsMediaSample(kStreamIndex0, 4 * kDuration1, kDuration1)));
|
||||
}
|
||||
|
||||
TEST_F(ChunkingHandlerTest, VideoAndSubsegmentAndNonzeroStart) {
|
||||
ChunkingOptions chunking_options;
|
||||
chunking_options.segment_duration_in_seconds = 1;
|
||||
|
|
|
@ -91,8 +91,9 @@ TEST_F(EncryptionHandlerTest, Initialize) {
|
|||
|
||||
TEST_F(EncryptionHandlerTest, OnlyOneOutput) {
|
||||
// Connecting another handler will fail.
|
||||
ASSERT_OK(encryption_handler_->AddHandler(some_handler()));
|
||||
ASSERT_EQ(error::INVALID_ARGUMENT,
|
||||
encryption_handler_->AddHandler(some_handler()).error_code());
|
||||
encryption_handler_->Initialize().error_code());
|
||||
}
|
||||
|
||||
TEST_F(EncryptionHandlerTest, OnlyOneInput) {
|
||||
|
|
|
@ -44,10 +44,6 @@ MediaInfo ConvertToMediaInfo(const std::string& media_info_string) {
|
|||
}
|
||||
|
||||
void SetDefaultLiveMuxerOptionsValues(media::MuxerOptions* muxer_options) {
|
||||
muxer_options->segment_duration = 10.0;
|
||||
muxer_options->fragment_duration = 10.0;
|
||||
muxer_options->segment_sap_aligned = true;
|
||||
muxer_options->fragment_sap_aligned = true;
|
||||
muxer_options->num_subsegments_per_sidx = 0;
|
||||
muxer_options->output_file_name = "liveinit.mp4";
|
||||
muxer_options->segment_template = "live-$NUMBER$.mp4";
|
||||
|
|
|
@ -73,10 +73,6 @@ OnMediaEndParameters GetDefaultOnMediaEndParams() {
|
|||
}
|
||||
|
||||
void SetDefaultMuxerOptionsValues(MuxerOptions* muxer_options) {
|
||||
muxer_options->segment_duration = 10.0;
|
||||
muxer_options->fragment_duration = 10.0;
|
||||
muxer_options->segment_sap_aligned = true;
|
||||
muxer_options->fragment_sap_aligned = true;
|
||||
muxer_options->num_subsegments_per_sidx = 0;
|
||||
muxer_options->output_file_name = "test_output_file_name.mp4";
|
||||
muxer_options->segment_template.clear();
|
||||
|
|
|
@ -34,10 +34,20 @@ Status TsMuxer::Finalize() {
|
|||
return segmenter_->Finalize();
|
||||
}
|
||||
|
||||
Status TsMuxer::DoAddSample(std::shared_ptr<MediaSample> sample) {
|
||||
Status TsMuxer::AddSample(int stream_id, std::shared_ptr<MediaSample> sample) {
|
||||
DCHECK_EQ(stream_id, 0);
|
||||
return segmenter_->AddSample(sample);
|
||||
}
|
||||
|
||||
Status TsMuxer::FinalizeSegment(int stream_id,
|
||||
std::shared_ptr<SegmentInfo> segment_info) {
|
||||
DCHECK_EQ(stream_id, 0);
|
||||
return segment_info->is_subsegment
|
||||
? Status::OK
|
||||
: segmenter_->FinalizeSegment(segment_info->start_timestamp,
|
||||
segment_info->duration);
|
||||
}
|
||||
|
||||
void TsMuxer::FireOnMediaStartEvent() {
|
||||
if (!muxer_listener())
|
||||
return;
|
||||
|
|
|
@ -26,7 +26,9 @@ class TsMuxer : public Muxer {
|
|||
// Muxer implementation.
|
||||
Status InitializeMuxer() override;
|
||||
Status Finalize() override;
|
||||
Status DoAddSample(std::shared_ptr<MediaSample> sample) override;
|
||||
Status AddSample(int stream_id, std::shared_ptr<MediaSample> sample) override;
|
||||
Status FinalizeSegment(int stream_id,
|
||||
std::shared_ptr<SegmentInfo> sample) override;
|
||||
|
||||
void FireOnMediaStartEvent();
|
||||
void FireOnMediaEndEvent();
|
||||
|
|
|
@ -82,21 +82,10 @@ Status TsSegmenter::Initialize(const StreamInfo& stream_info,
|
|||
}
|
||||
|
||||
Status TsSegmenter::Finalize() {
|
||||
return Flush();
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
// First checks whether the sample is a key frame. If so and the segment has
|
||||
// passed the segment duration, then flush the generator and write all the data
|
||||
// to file.
|
||||
Status TsSegmenter::AddSample(std::shared_ptr<MediaSample> sample) {
|
||||
const bool passed_segment_duration =
|
||||
current_segment_total_sample_duration_ > muxer_options_.segment_duration;
|
||||
if (sample->is_key_frame() && passed_segment_duration) {
|
||||
Status status = Flush();
|
||||
if (!status.ok())
|
||||
return status;
|
||||
}
|
||||
|
||||
if (!ts_writer_file_opened_ && !sample->is_key_frame())
|
||||
LOG(WARNING) << "A segment will start with a non key frame.";
|
||||
|
||||
|
@ -104,11 +93,6 @@ Status TsSegmenter::AddSample(std::shared_ptr<MediaSample> sample) {
|
|||
return Status(error::MUXER_FAILURE,
|
||||
"Failed to add sample to PesPacketGenerator.");
|
||||
}
|
||||
|
||||
const double scaled_sample_duration = sample->duration() * timescale_scale_;
|
||||
current_segment_total_sample_duration_ +=
|
||||
scaled_sample_duration / kTsTimescale;
|
||||
|
||||
return WritePesPacketsToFile();
|
||||
}
|
||||
|
||||
|
@ -133,7 +117,6 @@ Status TsSegmenter::OpenNewSegmentIfClosed(uint32_t next_pts) {
|
|||
segment_number_++, muxer_options_.bandwidth);
|
||||
if (!ts_writer_->NewSegment(segment_name))
|
||||
return Status(error::MUXER_FAILURE, "Failed to initilize TsPacketWriter.");
|
||||
current_segment_start_time_ = next_pts;
|
||||
current_segment_path_ = segment_name;
|
||||
ts_writer_file_opened_ = true;
|
||||
return Status::OK;
|
||||
|
@ -154,7 +137,8 @@ Status TsSegmenter::WritePesPacketsToFile() {
|
|||
return Status::OK;
|
||||
}
|
||||
|
||||
Status TsSegmenter::Flush() {
|
||||
Status TsSegmenter::FinalizeSegment(uint64_t start_timestamp,
|
||||
uint64_t duration) {
|
||||
if (!pes_packet_generator_->Flush()) {
|
||||
return Status(error::MUXER_FAILURE,
|
||||
"Failed to flush PesPacketGenerator.");
|
||||
|
@ -172,15 +156,13 @@ Status TsSegmenter::Flush() {
|
|||
if (listener_) {
|
||||
const int64_t file_size =
|
||||
File::GetFileSize(current_segment_path_.c_str());
|
||||
listener_->OnNewSegment(
|
||||
current_segment_path_, current_segment_start_time_,
|
||||
current_segment_total_sample_duration_ * kTsTimescale, file_size);
|
||||
listener_->OnNewSegment(current_segment_path_,
|
||||
start_timestamp * timescale_scale_,
|
||||
duration * timescale_scale_, file_size);
|
||||
}
|
||||
ts_writer_file_opened_ = false;
|
||||
total_duration_in_seconds_ += current_segment_total_sample_duration_;
|
||||
total_duration_in_seconds_ += duration * timescale_scale_ / kTsTimescale;
|
||||
}
|
||||
current_segment_total_sample_duration_ = 0.0;
|
||||
current_segment_start_time_ = 0;
|
||||
current_segment_path_.clear();
|
||||
return NotifyEncrypted();
|
||||
}
|
||||
|
|
|
@ -54,6 +54,18 @@ class TsSegmenter {
|
|||
/// @return OK on success.
|
||||
Status AddSample(std::shared_ptr<MediaSample> sample);
|
||||
|
||||
/// Flush all the samples that are (possibly) buffered and write them to the
|
||||
/// current segment, this will close the file. If a file is not already opened
|
||||
/// before calling this, this will open one and write them to file.
|
||||
/// @param start_timestamp is the segment's start timestamp in the input
|
||||
/// stream's time scale.
|
||||
/// @param duration is the segment's duration in the input stream's time
|
||||
/// scale.
|
||||
// TODO(kqyang): Remove the usage of segment start timestamp and duration in
|
||||
// xx_segmenter, which could cause confusions on which is the source of truth
|
||||
// as the segment start timestamp and duration could be tracked locally.
|
||||
Status FinalizeSegment(uint64_t start_timestamp, uint64_t duration);
|
||||
|
||||
/// Only for testing.
|
||||
void InjectTsWriterForTesting(std::unique_ptr<TsWriter> writer);
|
||||
|
||||
|
@ -71,11 +83,6 @@ class TsSegmenter {
|
|||
// it will open one. This will not close the file.
|
||||
Status WritePesPacketsToFile();
|
||||
|
||||
// Flush all the samples that are (possibly) buffered and write them to the
|
||||
// current segment, this will close the file. If a file is not already opened
|
||||
// before calling this, this will open one and write them to file.
|
||||
Status Flush();
|
||||
|
||||
// If conditions are met, notify objects that the data is encrypted.
|
||||
Status NotifyEncrypted();
|
||||
|
||||
|
@ -86,12 +93,6 @@ class TsSegmenter {
|
|||
// Used for calculating the duration in seconds fo the current segment.
|
||||
double timescale_scale_ = 1.0;
|
||||
|
||||
// This is the sum of the durations of the samples that were added to
|
||||
// PesPacketGenerator for the current segment (in seconds). Note that this is
|
||||
// not necessarily the same as the length of the PesPackets that have been
|
||||
// written to the current segment in WritePesPacketsToFile().
|
||||
double current_segment_total_sample_duration_ = 0.0;
|
||||
|
||||
// Used for segment template.
|
||||
uint64_t segment_number_ = 0;
|
||||
|
||||
|
@ -102,7 +103,6 @@ class TsSegmenter {
|
|||
std::unique_ptr<PesPacketGenerator> pes_packet_generator_;
|
||||
|
||||
// For OnNewSegment().
|
||||
uint64_t current_segment_start_time_ = 0;
|
||||
// Path of the current segment so that File::GetFileSize() can be used after
|
||||
// the segment has been finalized.
|
||||
std::string current_segment_path_;
|
||||
|
|
|
@ -133,7 +133,6 @@ TEST_F(TsSegmenterTest, AddSample) {
|
|||
arraysize(kExtraData), kWidth, kHeight, kPixelWidth, kPixelHeight,
|
||||
kTrickPlayRate, kNaluLengthSize, kLanguage, kIsEncrypted));
|
||||
MuxerOptions options;
|
||||
options.segment_duration = 10.0;
|
||||
options.segment_template = "file$Number$.ts";
|
||||
TsSegmenter segmenter(options, nullptr);
|
||||
|
||||
|
@ -175,10 +174,7 @@ TEST_F(TsSegmenterTest, AddSample) {
|
|||
EXPECT_OK(segmenter.AddSample(sample));
|
||||
}
|
||||
|
||||
// Verify the case where the segment is long enough and the current segment
|
||||
// should be closed.
|
||||
// This will add 2 samples and verify that the first segment is closed when the
|
||||
// second sample is added.
|
||||
// This will add one sample then finalize segment then add another sample.
|
||||
TEST_F(TsSegmenterTest, PassedSegmentDuration) {
|
||||
// Use something significantly smaller than 90000 to check that the scaling is
|
||||
// done correctly in the segmenter.
|
||||
|
@ -188,7 +184,6 @@ TEST_F(TsSegmenterTest, PassedSegmentDuration) {
|
|||
kExtraData, arraysize(kExtraData), kWidth, kHeight, kPixelWidth,
|
||||
kPixelHeight, kTrickPlayRate, kNaluLengthSize, kLanguage, kIsEncrypted));
|
||||
MuxerOptions options;
|
||||
options.segment_duration = 10.0;
|
||||
options.segment_template = "file$Number$.ts";
|
||||
|
||||
MockMuxerListener mock_listener;
|
||||
|
@ -202,21 +197,18 @@ TEST_F(TsSegmenterTest, PassedSegmentDuration) {
|
|||
|
||||
std::shared_ptr<MediaSample> sample1 =
|
||||
MediaSample::CopyFrom(kAnyData, arraysize(kAnyData), kIsKeyFrame);
|
||||
sample1->set_duration(kInputTimescale * 11);
|
||||
std::shared_ptr<MediaSample> sample2 =
|
||||
MediaSample::CopyFrom(kAnyData, arraysize(kAnyData), kIsKeyFrame);
|
||||
|
||||
// 11 seconds > 10 seconds (segment duration).
|
||||
// Expect the segment to be finalized.
|
||||
sample1->set_duration(kInputTimescale * 11);
|
||||
// Doesn't really matter how long this is.
|
||||
sample2->set_duration(kInputTimescale * 7);
|
||||
|
||||
// (Finalize is not called at the end of this test so) Expect one segment
|
||||
// event. The length should be the same as the above sample that exceeds the
|
||||
// duration.
|
||||
EXPECT_CALL(mock_listener,
|
||||
OnNewSegment("file1.ts", kFirstPts, kTimeScale * 11, _));
|
||||
|
||||
// Doesn't really matter how long this is.
|
||||
sample2->set_duration(kInputTimescale * 7);
|
||||
OnNewSegment("file1.ts", kFirstPts * kTimeScale / kInputTimescale,
|
||||
kTimeScale * 11, _));
|
||||
|
||||
Sequence writer_sequence;
|
||||
EXPECT_CALL(*mock_ts_writer_, NewSegment(StrEq("file1.ts")))
|
||||
|
@ -263,11 +255,9 @@ TEST_F(TsSegmenterTest, PassedSegmentDuration) {
|
|||
|
||||
// The pointers are released inside the segmenter.
|
||||
Sequence pes_packet_sequence;
|
||||
PesPacket* first_pes = new PesPacket();
|
||||
first_pes->set_pts(kFirstPts);
|
||||
EXPECT_CALL(*mock_pes_packet_generator_, GetNextPesPacketMock())
|
||||
.InSequence(pes_packet_sequence)
|
||||
.WillOnce(Return(first_pes));
|
||||
.WillOnce(Return(new PesPacket));
|
||||
EXPECT_CALL(*mock_pes_packet_generator_, GetNextPesPacketMock())
|
||||
.InSequence(pes_packet_sequence)
|
||||
.WillOnce(Return(new PesPacket()));
|
||||
|
@ -277,6 +267,7 @@ TEST_F(TsSegmenterTest, PassedSegmentDuration) {
|
|||
std::move(mock_pes_packet_generator_));
|
||||
EXPECT_OK(segmenter.Initialize(*stream_info, nullptr, 0, 0, 0, 0));
|
||||
EXPECT_OK(segmenter.AddSample(sample1));
|
||||
EXPECT_OK(segmenter.FinalizeSegment(kFirstPts, sample1->duration()));
|
||||
EXPECT_OK(segmenter.AddSample(sample2));
|
||||
}
|
||||
|
||||
|
@ -287,7 +278,6 @@ TEST_F(TsSegmenterTest, InitializeThenFinalize) {
|
|||
arraysize(kExtraData), kWidth, kHeight, kPixelWidth, kPixelHeight,
|
||||
kTrickPlayRate, kNaluLengthSize, kLanguage, kIsEncrypted));
|
||||
MuxerOptions options;
|
||||
options.segment_duration = 10.0;
|
||||
options.segment_template = "file$Number$.ts";
|
||||
TsSegmenter segmenter(options, nullptr);
|
||||
|
||||
|
@ -295,7 +285,7 @@ TEST_F(TsSegmenterTest, InitializeThenFinalize) {
|
|||
EXPECT_CALL(*mock_pes_packet_generator_, Initialize(_))
|
||||
.WillOnce(Return(true));
|
||||
|
||||
EXPECT_CALL(*mock_pes_packet_generator_, Flush()).WillOnce(Return(true));
|
||||
EXPECT_CALL(*mock_pes_packet_generator_, Flush()).Times(0);
|
||||
ON_CALL(*mock_pes_packet_generator_, NumberOfReadyPesPackets())
|
||||
.WillByDefault(Return(0));
|
||||
|
||||
|
@ -310,13 +300,12 @@ TEST_F(TsSegmenterTest, InitializeThenFinalize) {
|
|||
// been initialized.
|
||||
// The test does not really add any samples but instead simulates an initialized
|
||||
// writer with a mock.
|
||||
TEST_F(TsSegmenterTest, Finalize) {
|
||||
TEST_F(TsSegmenterTest, FinalizeSegment) {
|
||||
std::shared_ptr<VideoStreamInfo> stream_info(new VideoStreamInfo(
|
||||
kTrackId, kTimeScale, kDuration, kH264Codec, kCodecString, kExtraData,
|
||||
arraysize(kExtraData), kWidth, kHeight, kPixelWidth, kPixelHeight,
|
||||
kTrickPlayRate, kNaluLengthSize, kLanguage, kIsEncrypted));
|
||||
MuxerOptions options;
|
||||
options.segment_duration = 10.0;
|
||||
options.segment_template = "file$Number$.ts";
|
||||
TsSegmenter segmenter(options, nullptr);
|
||||
|
||||
|
@ -335,114 +324,7 @@ TEST_F(TsSegmenterTest, Finalize) {
|
|||
std::move(mock_pes_packet_generator_));
|
||||
EXPECT_OK(segmenter.Initialize(*stream_info, nullptr, 0, 0, 0, 0));
|
||||
segmenter.SetTsWriterFileOpenedForTesting(true);
|
||||
EXPECT_OK(segmenter.Finalize());
|
||||
}
|
||||
|
||||
// Verify that it won't finish a segment if the sample is not a key frame.
|
||||
TEST_F(TsSegmenterTest, SegmentOnlyBeforeKeyFrame) {
|
||||
std::shared_ptr<VideoStreamInfo> stream_info(new VideoStreamInfo(
|
||||
kTrackId, kTimeScale, kDuration, kH264Codec, kCodecString, kExtraData,
|
||||
arraysize(kExtraData), kWidth, kHeight, kPixelWidth, kPixelHeight,
|
||||
kTrickPlayRate, kNaluLengthSize, kLanguage, kIsEncrypted));
|
||||
MuxerOptions options;
|
||||
options.segment_duration = 10.0;
|
||||
options.segment_template = "file$Number$.ts";
|
||||
TsSegmenter segmenter(options, nullptr);
|
||||
|
||||
EXPECT_CALL(*mock_ts_writer_, Initialize(_)).WillOnce(Return(true));
|
||||
EXPECT_CALL(*mock_pes_packet_generator_, Initialize(_))
|
||||
.WillOnce(Return(true));
|
||||
|
||||
const uint8_t kAnyData[] = {
|
||||
0x01, 0x0F, 0x3C,
|
||||
};
|
||||
std::shared_ptr<MediaSample> key_frame_sample1 =
|
||||
MediaSample::CopyFrom(kAnyData, arraysize(kAnyData), kIsKeyFrame);
|
||||
std::shared_ptr<MediaSample> non_key_frame_sample =
|
||||
MediaSample::CopyFrom(kAnyData, arraysize(kAnyData), !kIsKeyFrame);
|
||||
std::shared_ptr<MediaSample> key_frame_sample2 =
|
||||
MediaSample::CopyFrom(kAnyData, arraysize(kAnyData), kIsKeyFrame);
|
||||
|
||||
// 11 seconds > 10 seconds (segment duration).
|
||||
key_frame_sample1->set_duration(kTimeScale * 11);
|
||||
|
||||
// But since the second sample is not a key frame, it shouldn't be segmented.
|
||||
non_key_frame_sample->set_duration(kTimeScale * 7);
|
||||
|
||||
// Since this is a key frame, it should be segmented when this is added.
|
||||
key_frame_sample2->set_duration(kTimeScale * 3);
|
||||
|
||||
EXPECT_CALL(*mock_pes_packet_generator_, PushSample(_))
|
||||
.Times(3)
|
||||
.WillRepeatedly(Return(true));
|
||||
|
||||
Sequence writer_sequence;
|
||||
EXPECT_CALL(*mock_ts_writer_, NewSegment(StrEq("file1.ts")))
|
||||
.InSequence(writer_sequence)
|
||||
.WillOnce(Return(true));
|
||||
|
||||
Sequence ready_pes_sequence;
|
||||
// First AddSample().
|
||||
EXPECT_CALL(*mock_pes_packet_generator_, NumberOfReadyPesPackets())
|
||||
.InSequence(ready_pes_sequence)
|
||||
.WillOnce(Return(1u));
|
||||
EXPECT_CALL(*mock_pes_packet_generator_, NumberOfReadyPesPackets())
|
||||
.InSequence(ready_pes_sequence)
|
||||
.WillOnce(Return(0u));
|
||||
// Second AddSample().
|
||||
EXPECT_CALL(*mock_pes_packet_generator_, NumberOfReadyPesPackets())
|
||||
.InSequence(ready_pes_sequence)
|
||||
.WillOnce(Return(1u));
|
||||
EXPECT_CALL(*mock_pes_packet_generator_, NumberOfReadyPesPackets())
|
||||
.InSequence(ready_pes_sequence)
|
||||
.WillOnce(Return(0u));
|
||||
// Third AddSample(), in Flush().
|
||||
EXPECT_CALL(*mock_pes_packet_generator_, NumberOfReadyPesPackets())
|
||||
.InSequence(ready_pes_sequence)
|
||||
.WillOnce(Return(0u));
|
||||
// Third AddSample() after Flush().
|
||||
EXPECT_CALL(*mock_pes_packet_generator_, NumberOfReadyPesPackets())
|
||||
.InSequence(ready_pes_sequence)
|
||||
.WillOnce(Return(1u));
|
||||
EXPECT_CALL(*mock_pes_packet_generator_, NumberOfReadyPesPackets())
|
||||
.InSequence(ready_pes_sequence)
|
||||
.WillOnce(Return(0u));
|
||||
|
||||
EXPECT_CALL(*mock_pes_packet_generator_, Flush())
|
||||
.WillOnce(Return(true));
|
||||
|
||||
EXPECT_CALL(*mock_ts_writer_, FinalizeSegment())
|
||||
.InSequence(writer_sequence)
|
||||
.WillOnce(Return(true));
|
||||
|
||||
// Expectations for second AddSample() for the second segment.
|
||||
EXPECT_CALL(*mock_ts_writer_, NewSegment(StrEq("file2.ts")))
|
||||
.InSequence(writer_sequence)
|
||||
.WillOnce(Return(true));
|
||||
|
||||
EXPECT_CALL(*mock_ts_writer_, AddPesPacketMock(_))
|
||||
.Times(3)
|
||||
.WillRepeatedly(Return(true));
|
||||
|
||||
// The pointers are released inside the segmenter.
|
||||
Sequence pes_packet_sequence;
|
||||
EXPECT_CALL(*mock_pes_packet_generator_, GetNextPesPacketMock())
|
||||
.InSequence(pes_packet_sequence)
|
||||
.WillOnce(Return(new PesPacket()));
|
||||
EXPECT_CALL(*mock_pes_packet_generator_, GetNextPesPacketMock())
|
||||
.InSequence(pes_packet_sequence)
|
||||
.WillOnce(Return(new PesPacket()));
|
||||
EXPECT_CALL(*mock_pes_packet_generator_, GetNextPesPacketMock())
|
||||
.InSequence(pes_packet_sequence)
|
||||
.WillOnce(Return(new PesPacket()));
|
||||
|
||||
segmenter.InjectTsWriterForTesting(std::move(mock_ts_writer_));
|
||||
segmenter.InjectPesPacketGeneratorForTesting(
|
||||
std::move(mock_pes_packet_generator_));
|
||||
EXPECT_OK(segmenter.Initialize(*stream_info, nullptr, 0, 0, 0, 0));
|
||||
EXPECT_OK(segmenter.AddSample(key_frame_sample1));
|
||||
EXPECT_OK(segmenter.AddSample(non_key_frame_sample));
|
||||
EXPECT_OK(segmenter.AddSample(key_frame_sample2));
|
||||
EXPECT_OK(segmenter.FinalizeSegment(0, 100 /* arbitrary duration */));
|
||||
}
|
||||
|
||||
TEST_F(TsSegmenterTest, WithEncryptionNoClearLead) {
|
||||
|
@ -451,7 +333,6 @@ TEST_F(TsSegmenterTest, WithEncryptionNoClearLead) {
|
|||
arraysize(kExtraData), kWidth, kHeight, kPixelWidth, kPixelHeight,
|
||||
kTrickPlayRate, kNaluLengthSize, kLanguage, kIsEncrypted));
|
||||
MuxerOptions options;
|
||||
options.segment_duration = 10.0;
|
||||
options.segment_template = "file$Number$.ts";
|
||||
|
||||
MockMuxerListener mock_listener;
|
||||
|
@ -494,7 +375,7 @@ TEST_F(TsSegmenterTest, WithEncryptionNoClearLeadNoMuxerListener) {
|
|||
arraysize(kExtraData), kWidth, kHeight, kPixelWidth, kPixelHeight,
|
||||
kTrickPlayRate, kNaluLengthSize, kLanguage, kIsEncrypted));
|
||||
MuxerOptions options;
|
||||
options.segment_duration = 10.0;
|
||||
// options.segment_duration = 10.0;
|
||||
options.segment_template = "file$Number$.ts";
|
||||
|
||||
TsSegmenter segmenter(options, nullptr);
|
||||
|
@ -535,7 +416,6 @@ TEST_F(TsSegmenterTest, WithEncryptionWithClearLead) {
|
|||
kTrickPlayRate, kNaluLengthSize, kLanguage, kIsEncrypted));
|
||||
MuxerOptions options;
|
||||
|
||||
options.segment_duration = 1.0;
|
||||
const uint32_t k1080pPixels = 1920 * 1080;
|
||||
const uint32_t k2160pPixels = 4096 * 2160;
|
||||
const double kClearLeadSeconds = 1.0;
|
||||
|
@ -557,12 +437,9 @@ TEST_F(TsSegmenterTest, WithEncryptionWithClearLead) {
|
|||
};
|
||||
std::shared_ptr<MediaSample> sample1 =
|
||||
MediaSample::CopyFrom(kAnyData, arraysize(kAnyData), kIsKeyFrame);
|
||||
sample1->set_duration(kTimeScale * 2);
|
||||
std::shared_ptr<MediaSample> sample2 =
|
||||
MediaSample::CopyFrom(kAnyData, arraysize(kAnyData), kIsKeyFrame);
|
||||
|
||||
// Something longer than 1.0 (segment duration and clear lead).
|
||||
sample1->set_duration(kTimeScale * 2);
|
||||
// The length of the second sample doesn't really matter.
|
||||
sample2->set_duration(kTimeScale * 3);
|
||||
|
||||
EXPECT_CALL(*mock_pes_packet_generator_, PushSample(_))
|
||||
|
@ -622,12 +499,13 @@ TEST_F(TsSegmenterTest, WithEncryptionWithClearLead) {
|
|||
kClearLeadSeconds));
|
||||
EXPECT_OK(segmenter.AddSample(sample1));
|
||||
|
||||
// These should be called AFTER the first AddSample(), before the second
|
||||
// segment.
|
||||
// Encryption should be setup after finalizing the first segment.
|
||||
// These should be called when finalize segment is called.
|
||||
EXPECT_CALL(mock_listener, OnEncryptionStart());
|
||||
EXPECT_CALL(*mock_pes_packet_generator_raw, SetEncryptionKeyMock(_))
|
||||
.WillOnce(Return(true));
|
||||
EXPECT_CALL(*mock_ts_writer_raw, SignalEncrypted());
|
||||
EXPECT_OK(segmenter.FinalizeSegment(0, sample1->duration()));
|
||||
EXPECT_OK(segmenter.AddSample(sample2));
|
||||
}
|
||||
|
||||
|
|
|
@ -52,6 +52,8 @@ class Fragmenter {
|
|||
/// Fill @a reference with current fragment information.
|
||||
void GenerateSegmentReference(SegmentReference* reference);
|
||||
|
||||
void ClearFragmentFinalized() { fragment_finalized_ = false; }
|
||||
|
||||
uint64_t fragment_duration() const { return fragment_duration_; }
|
||||
uint64_t first_sap_time() const { return first_sap_time_; }
|
||||
uint64_t earliest_presentation_time() const {
|
||||
|
|
|
@ -161,9 +161,18 @@ Status MP4Muxer::Finalize() {
|
|||
return Status::OK;
|
||||
}
|
||||
|
||||
Status MP4Muxer::DoAddSample(std::shared_ptr<MediaSample> sample) {
|
||||
Status MP4Muxer::AddSample(int stream_id, std::shared_ptr<MediaSample> sample) {
|
||||
DCHECK(segmenter_);
|
||||
return segmenter_->AddSample(*streams()[0], sample);
|
||||
return segmenter_->AddSample(stream_id, sample);
|
||||
}
|
||||
|
||||
Status MP4Muxer::FinalizeSegment(int stream_id,
|
||||
std::shared_ptr<SegmentInfo> segment_info) {
|
||||
DCHECK(segmenter_);
|
||||
VLOG(3) << "Finalize " << (segment_info->is_subsegment ? "sub" : "")
|
||||
<< "segment " << segment_info->start_timestamp << " duration "
|
||||
<< segment_info->duration;
|
||||
return segmenter_->FinalizeSegment(stream_id, segment_info->is_subsegment);
|
||||
}
|
||||
|
||||
void MP4Muxer::InitializeTrak(const StreamInfo* info, Track* trak) {
|
||||
|
|
|
@ -37,7 +37,9 @@ class MP4Muxer : public Muxer {
|
|||
// Muxer implementation overrides.
|
||||
Status InitializeMuxer() override;
|
||||
Status Finalize() override;
|
||||
Status DoAddSample(std::shared_ptr<MediaSample> sample) override;
|
||||
Status AddSample(int stream_id, std::shared_ptr<MediaSample> sample) override;
|
||||
Status FinalizeSegment(int stream_id,
|
||||
std::shared_ptr<SegmentInfo> segment_info) override;
|
||||
|
||||
// Generate Audio/Video Track box.
|
||||
void InitializeTrak(const StreamInfo* info, Track* trak);
|
||||
|
|
|
@ -178,7 +178,6 @@ Status Segmenter::Initialize(
|
|||
moof_->header.sequence_number = 0;
|
||||
|
||||
moof_->tracks.resize(streams.size());
|
||||
segment_durations_.resize(streams.size());
|
||||
fragmenters_.resize(streams.size());
|
||||
const bool key_rotation_enabled = crypto_period_duration_in_seconds != 0;
|
||||
const bool kInitialEncryptionInfo = true;
|
||||
|
@ -292,12 +291,6 @@ Status Segmenter::Initialize(
|
|||
}
|
||||
|
||||
Status Segmenter::Finalize() {
|
||||
for (const std::unique_ptr<Fragmenter>& fragmenter : fragmenters_) {
|
||||
Status status = FinalizeFragment(true, fragmenter.get());
|
||||
if (!status.ok())
|
||||
return status;
|
||||
}
|
||||
|
||||
// Set tracks and moov durations.
|
||||
// Note that the updated moov box will be written to output file for VOD case
|
||||
// only.
|
||||
|
@ -315,111 +308,35 @@ Status Segmenter::Finalize() {
|
|||
return DoFinalize();
|
||||
}
|
||||
|
||||
Status Segmenter::AddSample(const StreamInfo& stream_info,
|
||||
Status Segmenter::AddSample(int stream_id,
|
||||
std::shared_ptr<MediaSample> sample) {
|
||||
// TODO(kqyang): Stream id should be passed in.
|
||||
const uint32_t stream_id = 0;
|
||||
Fragmenter* fragmenter = fragmenters_[stream_id].get();
|
||||
|
||||
// Set default sample duration if it has not been set yet.
|
||||
if (moov_->extends.tracks[stream_id].default_sample_duration == 0) {
|
||||
moov_->extends.tracks[stream_id].default_sample_duration =
|
||||
sample->duration();
|
||||
}
|
||||
|
||||
DCHECK_LT(stream_id, static_cast<int>(fragmenters_.size()));
|
||||
Fragmenter* fragmenter = fragmenters_[stream_id].get();
|
||||
if (fragmenter->fragment_finalized()) {
|
||||
return Status(error::FRAGMENT_FINALIZED,
|
||||
"Current fragment is finalized already.");
|
||||
}
|
||||
|
||||
bool finalize_fragment = false;
|
||||
if (fragmenter->fragment_duration() >=
|
||||
options_.fragment_duration * stream_info.time_scale()) {
|
||||
if (sample->is_key_frame() || !options_.fragment_sap_aligned) {
|
||||
finalize_fragment = true;
|
||||
}
|
||||
}
|
||||
bool finalize_segment = false;
|
||||
if (segment_durations_[stream_id] >=
|
||||
options_.segment_duration * stream_info.time_scale()) {
|
||||
if (sample->is_key_frame() || !options_.segment_sap_aligned) {
|
||||
finalize_segment = true;
|
||||
finalize_fragment = true;
|
||||
}
|
||||
}
|
||||
|
||||
Status status;
|
||||
if (finalize_fragment) {
|
||||
status = FinalizeFragment(finalize_segment, fragmenter);
|
||||
if (!status.ok())
|
||||
return status;
|
||||
}
|
||||
|
||||
status = fragmenter->AddSample(sample);
|
||||
Status status = fragmenter->AddSample(sample);
|
||||
if (!status.ok())
|
||||
return status;
|
||||
|
||||
if (sample_duration_ == 0)
|
||||
sample_duration_ = sample->duration();
|
||||
moov_->tracks[stream_id].media.header.duration += sample->duration();
|
||||
segment_durations_[stream_id] += sample->duration();
|
||||
DCHECK_GE(segment_durations_[stream_id], fragmenter->fragment_duration());
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
uint32_t Segmenter::GetReferenceTimeScale() const {
|
||||
return moov_->header.timescale;
|
||||
}
|
||||
|
||||
double Segmenter::GetDuration() const {
|
||||
if (moov_->header.timescale == 0) {
|
||||
// Handling the case where this is not properly initialized.
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return static_cast<double>(moov_->header.duration) / moov_->header.timescale;
|
||||
}
|
||||
|
||||
void Segmenter::UpdateProgress(uint64_t progress) {
|
||||
accumulated_progress_ += progress;
|
||||
|
||||
if (!progress_listener_) return;
|
||||
if (progress_target_ == 0) return;
|
||||
// It might happen that accumulated progress exceeds progress_target due to
|
||||
// computation errors, e.g. rounding error. Cap it so it never reports > 100%
|
||||
// progress.
|
||||
if (accumulated_progress_ >= progress_target_) {
|
||||
progress_listener_->OnProgress(1.0);
|
||||
} else {
|
||||
progress_listener_->OnProgress(static_cast<double>(accumulated_progress_) /
|
||||
progress_target_);
|
||||
}
|
||||
}
|
||||
|
||||
void Segmenter::SetComplete() {
|
||||
if (!progress_listener_) return;
|
||||
progress_listener_->OnProgress(1.0);
|
||||
}
|
||||
|
||||
Status Segmenter::FinalizeSegment() {
|
||||
Status status = DoFinalizeSegment();
|
||||
|
||||
// Reset segment information to initial state.
|
||||
sidx_->references.clear();
|
||||
std::vector<uint64_t>::iterator it = segment_durations_.begin();
|
||||
for (; it != segment_durations_.end(); ++it)
|
||||
*it = 0;
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
uint32_t Segmenter::GetReferenceStreamId() {
|
||||
DCHECK(sidx_);
|
||||
return sidx_->reference_id - 1;
|
||||
}
|
||||
|
||||
Status Segmenter::FinalizeFragment(bool finalize_segment,
|
||||
Fragmenter* fragmenter) {
|
||||
Status Segmenter::FinalizeSegment(int stream_id, bool is_subsegment) {
|
||||
DCHECK_LT(stream_id, static_cast<int>(fragmenters_.size()));
|
||||
Fragmenter* fragmenter = fragmenters_[stream_id].get();
|
||||
DCHECK(fragmenter);
|
||||
fragmenter->FinalizeFragment();
|
||||
|
||||
// Check if all tracks are ready for fragmentation.
|
||||
|
@ -468,12 +385,56 @@ Status Segmenter::FinalizeFragment(bool finalize_segment,
|
|||
// Increase sequence_number for next fragment.
|
||||
++moof_->header.sequence_number;
|
||||
|
||||
if (finalize_segment)
|
||||
return FinalizeSegment();
|
||||
|
||||
for (std::unique_ptr<Fragmenter>& fragmenter : fragmenters_)
|
||||
fragmenter->ClearFragmentFinalized();
|
||||
if (!is_subsegment) {
|
||||
Status status = DoFinalizeSegment();
|
||||
// Reset segment information to initial state.
|
||||
sidx_->references.clear();
|
||||
return status;
|
||||
}
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
uint32_t Segmenter::GetReferenceTimeScale() const {
|
||||
return moov_->header.timescale;
|
||||
}
|
||||
|
||||
double Segmenter::GetDuration() const {
|
||||
if (moov_->header.timescale == 0) {
|
||||
// Handling the case where this is not properly initialized.
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return static_cast<double>(moov_->header.duration) / moov_->header.timescale;
|
||||
}
|
||||
|
||||
void Segmenter::UpdateProgress(uint64_t progress) {
|
||||
accumulated_progress_ += progress;
|
||||
|
||||
if (!progress_listener_) return;
|
||||
if (progress_target_ == 0) return;
|
||||
// It might happen that accumulated progress exceeds progress_target due to
|
||||
// computation errors, e.g. rounding error. Cap it so it never reports > 100%
|
||||
// progress.
|
||||
if (accumulated_progress_ >= progress_target_) {
|
||||
progress_listener_->OnProgress(1.0);
|
||||
} else {
|
||||
progress_listener_->OnProgress(static_cast<double>(accumulated_progress_) /
|
||||
progress_target_);
|
||||
}
|
||||
}
|
||||
|
||||
void Segmenter::SetComplete() {
|
||||
if (!progress_listener_) return;
|
||||
progress_listener_->OnProgress(1.0);
|
||||
}
|
||||
|
||||
uint32_t Segmenter::GetReferenceStreamId() {
|
||||
DCHECK(sidx_);
|
||||
return sidx_->reference_id - 1;
|
||||
}
|
||||
|
||||
} // namespace mp4
|
||||
} // namespace media
|
||||
} // namespace shaka
|
||||
|
|
|
@ -85,10 +85,16 @@ class Segmenter {
|
|||
Status Finalize();
|
||||
|
||||
/// Add sample to the indicated stream.
|
||||
/// @param stream_id is the zero-based stream index.
|
||||
/// @param sample points to the sample to be added.
|
||||
/// @return OK on success, an error status otherwise.
|
||||
Status AddSample(const StreamInfo& stream_Info,
|
||||
std::shared_ptr<MediaSample> sample);
|
||||
Status AddSample(int stream_id, std::shared_ptr<MediaSample> sample);
|
||||
|
||||
/// Finalize the segment / subsegment.
|
||||
/// @param stream_id is the zero-based stream index.
|
||||
/// @param is_subsegment indicates if it is a subsegment (fragment).
|
||||
/// @return OK on success, an error status otherwise.
|
||||
Status FinalizeSegment(int stream_id, bool is_subsegment);
|
||||
|
||||
/// @return true if there is an initialization range, while setting @a offset
|
||||
/// and @a size; or false if initialization range does not apply.
|
||||
|
@ -130,11 +136,8 @@ class Segmenter {
|
|||
virtual Status DoFinalize() = 0;
|
||||
virtual Status DoFinalizeSegment() = 0;
|
||||
|
||||
Status FinalizeSegment();
|
||||
uint32_t GetReferenceStreamId();
|
||||
|
||||
Status FinalizeFragment(bool finalize_segment, Fragmenter* fragment);
|
||||
|
||||
const MuxerOptions& options_;
|
||||
std::unique_ptr<FileType> ftyp_;
|
||||
std::unique_ptr<Movie> moov_;
|
||||
|
@ -142,7 +145,6 @@ class Segmenter {
|
|||
std::unique_ptr<BufferWriter> fragment_buffer_;
|
||||
std::unique_ptr<SegmentIndex> sidx_;
|
||||
std::vector<std::unique_ptr<Fragmenter>> fragmenters_;
|
||||
std::vector<uint64_t> segment_durations_;
|
||||
MuxerListener* muxer_listener_;
|
||||
ProgressListener* progress_listener_;
|
||||
uint64_t progress_target_;
|
||||
|
|
|
@ -14,6 +14,7 @@ namespace media {
|
|||
namespace {
|
||||
|
||||
const uint64_t kDuration = 1000;
|
||||
const bool kSubsegment = true;
|
||||
const std::string kKeyId = "4c6f72656d20697073756d20646f6c6f";
|
||||
const std::string kIv = "0123456789012345";
|
||||
const std::string kKey = "01234567890123456789012345678901";
|
||||
|
@ -210,17 +211,20 @@ class EncrypedSegmenterTest : public SegmentTestBase {
|
|||
|
||||
TEST_F(EncrypedSegmenterTest, BasicSupport) {
|
||||
MuxerOptions options = CreateMuxerOptions();
|
||||
options.segment_duration = 3.0;
|
||||
ASSERT_NO_FATAL_FAILURE(InitializeSegmenter(options));
|
||||
|
||||
// Write the samples to the Segmenter.
|
||||
// There should be 2 segments with the first segment in clear and the second
|
||||
// segment encrypted.
|
||||
for (int i = 0; i < 5; i++) {
|
||||
if (i == 3)
|
||||
ASSERT_OK(segmenter_->FinalizeSegment(0, 3 * kDuration, !kSubsegment));
|
||||
std::shared_ptr<MediaSample> sample =
|
||||
CreateSample(kKeyFrame, kDuration, kNoSideData);
|
||||
ASSERT_OK(segmenter_->AddSample(sample));
|
||||
}
|
||||
ASSERT_OK(
|
||||
segmenter_->FinalizeSegment(3 * kDuration, 2 * kDuration, !kSubsegment));
|
||||
ASSERT_OK(segmenter_->Finalize());
|
||||
|
||||
ASSERT_FILE_ENDS_WITH(OutputFileName().c_str(), kBasicSupportData);
|
||||
|
|
|
@ -15,11 +15,33 @@
|
|||
namespace shaka {
|
||||
namespace media {
|
||||
namespace webm {
|
||||
|
||||
MultiSegmentSegmenter::MultiSegmentSegmenter(const MuxerOptions& options)
|
||||
: Segmenter(options), num_segment_(0) {}
|
||||
|
||||
MultiSegmentSegmenter::~MultiSegmentSegmenter() {}
|
||||
|
||||
Status MultiSegmentSegmenter::FinalizeSegment(uint64_t start_timescale,
|
||||
uint64_t duration_timescale,
|
||||
bool is_subsegment) {
|
||||
CHECK(cluster());
|
||||
Status status = Segmenter::FinalizeSegment(start_timescale,
|
||||
duration_timescale, is_subsegment);
|
||||
if (!status.ok())
|
||||
return status;
|
||||
if (!cluster()->Finalize())
|
||||
return Status(error::FILE_FAILURE, "Error finalizing segment.");
|
||||
if (!is_subsegment) {
|
||||
if (muxer_listener()) {
|
||||
const uint64_t size = cluster()->Size();
|
||||
muxer_listener()->OnNewSegment(writer_->file()->file_name(),
|
||||
start_timescale, duration_timescale, size);
|
||||
}
|
||||
VLOG(1) << "WEBM file '" << writer_->file()->file_name() << "' finalized.";
|
||||
}
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
bool MultiSegmentSegmenter::GetInitRangeStartAndEnd(uint64_t* start,
|
||||
uint64_t* end) {
|
||||
return false;
|
||||
|
@ -36,52 +58,22 @@ Status MultiSegmentSegmenter::DoInitialize(std::unique_ptr<MkvWriter> writer) {
|
|||
}
|
||||
|
||||
Status MultiSegmentSegmenter::DoFinalize() {
|
||||
Status status = FinalizeSegment();
|
||||
status.Update(writer_->Close());
|
||||
return status;
|
||||
return writer_->Close();
|
||||
}
|
||||
|
||||
Status MultiSegmentSegmenter::FinalizeSegment() {
|
||||
if (!cluster()->Finalize())
|
||||
return Status(error::FILE_FAILURE, "Error finalizing segment.");
|
||||
|
||||
if (muxer_listener()) {
|
||||
const uint64_t size = cluster()->Size();
|
||||
const uint64_t start_webm_timecode = cluster()->timecode();
|
||||
const uint64_t start_timescale = FromWebMTimecode(start_webm_timecode);
|
||||
muxer_listener()->OnNewSegment(writer_->file()->file_name(),
|
||||
start_timescale,
|
||||
cluster_length_in_time_scale(), size);
|
||||
}
|
||||
|
||||
VLOG(1) << "WEBM file '" << writer_->file()->file_name() << "' finalized.";
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
Status MultiSegmentSegmenter::NewSubsegment(uint64_t start_timescale) {
|
||||
if (cluster() && !cluster()->Finalize())
|
||||
return Status(error::FILE_FAILURE, "Error finalizing segment.");
|
||||
|
||||
uint64_t start_webm_timecode = FromBMFFTimescale(start_timescale);
|
||||
return SetCluster(start_webm_timecode, 0, writer_.get());
|
||||
}
|
||||
|
||||
Status MultiSegmentSegmenter::NewSegment(uint64_t start_timescale) {
|
||||
if (cluster()) {
|
||||
Status temp = FinalizeSegment();
|
||||
if (!temp.ok())
|
||||
return temp;
|
||||
}
|
||||
|
||||
Status MultiSegmentSegmenter::NewSegment(uint64_t start_timescale,
|
||||
bool is_subsegment) {
|
||||
if (!is_subsegment) {
|
||||
// Create a new file for the new segment.
|
||||
std::string segment_name =
|
||||
GetSegmentName(options().segment_template, start_timescale, num_segment_,
|
||||
options().bandwidth);
|
||||
GetSegmentName(options().segment_template, start_timescale,
|
||||
num_segment_, options().bandwidth);
|
||||
writer_.reset(new MkvWriter);
|
||||
Status status = writer_->Open(segment_name);
|
||||
if (!status.ok())
|
||||
return status;
|
||||
num_segment_++;
|
||||
}
|
||||
|
||||
uint64_t start_webm_timecode = FromBMFFTimescale(start_timescale);
|
||||
return SetCluster(start_webm_timecode, 0, writer_.get());
|
||||
|
|
|
@ -28,6 +28,9 @@ class MultiSegmentSegmenter : public Segmenter {
|
|||
|
||||
/// @name Segmenter implementation overrides.
|
||||
/// @{
|
||||
Status FinalizeSegment(uint64_t start_timescale,
|
||||
uint64_t duration_timescale,
|
||||
bool is_subsegment) override;
|
||||
bool GetInitRangeStartAndEnd(uint64_t* start, uint64_t* end) override;
|
||||
bool GetIndexRangeStartAndEnd(uint64_t* start, uint64_t* end) override;
|
||||
/// @}
|
||||
|
@ -39,10 +42,7 @@ class MultiSegmentSegmenter : public Segmenter {
|
|||
|
||||
private:
|
||||
// Segmenter implementation overrides.
|
||||
Status NewSubsegment(uint64_t start_timescale) override;
|
||||
Status NewSegment(uint64_t start_timescale) override;
|
||||
|
||||
Status FinalizeSegment();
|
||||
Status NewSegment(uint64_t start_timescale, bool is_subsegment) override;
|
||||
|
||||
std::unique_ptr<MkvWriter> writer_;
|
||||
uint32_t num_segment_;
|
||||
|
|
|
@ -14,6 +14,7 @@ namespace media {
|
|||
namespace {
|
||||
|
||||
const uint64_t kDuration = 1000;
|
||||
const bool kSubsegment = true;
|
||||
|
||||
const uint8_t kBasicSupportDataInit[] = {
|
||||
// ID: EBML Header omitted.
|
||||
|
@ -124,6 +125,7 @@ TEST_F(MultiSegmentSegmenterTest, BasicSupport) {
|
|||
CreateSample(kKeyFrame, kDuration, kNoSideData);
|
||||
ASSERT_OK(segmenter_->AddSample(sample));
|
||||
}
|
||||
ASSERT_OK(segmenter_->FinalizeSegment(0, 8 * kDuration, !kSubsegment));
|
||||
ASSERT_OK(segmenter_->Finalize());
|
||||
|
||||
// Verify the resulting data.
|
||||
|
@ -134,18 +136,21 @@ TEST_F(MultiSegmentSegmenterTest, BasicSupport) {
|
|||
EXPECT_FALSE(File::Open(TemplateFileName(1).c_str(), "r"));
|
||||
}
|
||||
|
||||
TEST_F(MultiSegmentSegmenterTest, SplitsFilesOnSegmentDuration) {
|
||||
TEST_F(MultiSegmentSegmenterTest, SplitsFilesOnSegment) {
|
||||
MuxerOptions options = CreateMuxerOptions();
|
||||
options.segment_template = segment_template_;
|
||||
options.segment_duration = 5; // seconds
|
||||
ASSERT_NO_FATAL_FAILURE(InitializeSegmenter(options));
|
||||
|
||||
// Write the samples to the Segmenter.
|
||||
for (int i = 0; i < 8; i++) {
|
||||
if (i == 5)
|
||||
ASSERT_OK(segmenter_->FinalizeSegment(0, 5 * kDuration, !kSubsegment));
|
||||
std::shared_ptr<MediaSample> sample =
|
||||
CreateSample(kKeyFrame, kDuration, kNoSideData);
|
||||
ASSERT_OK(segmenter_->AddSample(sample));
|
||||
}
|
||||
ASSERT_OK(
|
||||
segmenter_->FinalizeSegment(5 * kDuration, 8 * kDuration, !kSubsegment));
|
||||
ASSERT_OK(segmenter_->Finalize());
|
||||
|
||||
// Verify the resulting data.
|
||||
|
@ -161,47 +166,20 @@ TEST_F(MultiSegmentSegmenterTest, SplitsFilesOnSegmentDuration) {
|
|||
EXPECT_FALSE(File::Open(TemplateFileName(2).c_str(), "r"));
|
||||
}
|
||||
|
||||
TEST_F(MultiSegmentSegmenterTest, RespectsSegmentSAPAlign) {
|
||||
TEST_F(MultiSegmentSegmenterTest, SplitsClustersOnSubsegment) {
|
||||
MuxerOptions options = CreateMuxerOptions();
|
||||
options.segment_template = segment_template_;
|
||||
options.segment_duration = 3; // seconds
|
||||
options.segment_sap_aligned = true;
|
||||
ASSERT_NO_FATAL_FAILURE(InitializeSegmenter(options));
|
||||
|
||||
// Write the samples to the Segmenter.
|
||||
for (int i = 0; i < 10; i++) {
|
||||
const KeyFrameFlag key_frame_flag = i == 6 ? kKeyFrame : kNotKeyFrame;
|
||||
std::shared_ptr<MediaSample> sample =
|
||||
CreateSample(key_frame_flag, kDuration, kNoSideData);
|
||||
ASSERT_OK(segmenter_->AddSample(sample));
|
||||
}
|
||||
ASSERT_OK(segmenter_->Finalize());
|
||||
|
||||
// Verify the resulting data.
|
||||
ClusterParser parser;
|
||||
ASSERT_NO_FATAL_FAILURE(parser.PopulateFromCluster(TemplateFileName(0)));
|
||||
ASSERT_EQ(1u, parser.cluster_count());
|
||||
EXPECT_EQ(6, parser.GetFrameCountForCluster(0));
|
||||
|
||||
ASSERT_NO_FATAL_FAILURE(parser.PopulateFromCluster(TemplateFileName(1)));
|
||||
ASSERT_EQ(1u, parser.cluster_count());
|
||||
EXPECT_EQ(4, parser.GetFrameCountForCluster(0));
|
||||
|
||||
EXPECT_FALSE(File::Open(TemplateFileName(2).c_str(), "r"));
|
||||
}
|
||||
|
||||
TEST_F(MultiSegmentSegmenterTest, SplitsClustersOnFragmentDuration) {
|
||||
MuxerOptions options = CreateMuxerOptions();
|
||||
options.segment_template = segment_template_;
|
||||
options.fragment_duration = 5; // seconds
|
||||
ASSERT_NO_FATAL_FAILURE(InitializeSegmenter(options));
|
||||
|
||||
// Write the samples to the Segmenter.
|
||||
for (int i = 0; i < 8; i++) {
|
||||
if (i == 5)
|
||||
ASSERT_OK(segmenter_->FinalizeSegment(0, 5 * kDuration, kSubsegment));
|
||||
std::shared_ptr<MediaSample> sample =
|
||||
CreateSample(kKeyFrame, kDuration, kNoSideData);
|
||||
ASSERT_OK(segmenter_->AddSample(sample));
|
||||
}
|
||||
ASSERT_OK(segmenter_->FinalizeSegment(0, 8 * kDuration, !kSubsegment));
|
||||
ASSERT_OK(segmenter_->Finalize());
|
||||
|
||||
// Verify the resulting data.
|
||||
|
@ -214,31 +192,5 @@ TEST_F(MultiSegmentSegmenterTest, SplitsClustersOnFragmentDuration) {
|
|||
EXPECT_FALSE(File::Open(TemplateFileName(1).c_str(), "r"));
|
||||
}
|
||||
|
||||
TEST_F(MultiSegmentSegmenterTest, RespectsFragmentSAPAlign) {
|
||||
MuxerOptions options = CreateMuxerOptions();
|
||||
options.segment_template = segment_template_;
|
||||
options.fragment_duration = 3; // seconds
|
||||
options.fragment_sap_aligned = true;
|
||||
ASSERT_NO_FATAL_FAILURE(InitializeSegmenter(options));
|
||||
|
||||
// Write the samples to the Segmenter.
|
||||
for (int i = 0; i < 10; i++) {
|
||||
const KeyFrameFlag key_frame_flag = i == 6 ? kKeyFrame : kNotKeyFrame;
|
||||
std::shared_ptr<MediaSample> sample =
|
||||
CreateSample(key_frame_flag, kDuration, kNoSideData);
|
||||
ASSERT_OK(segmenter_->AddSample(sample));
|
||||
}
|
||||
ASSERT_OK(segmenter_->Finalize());
|
||||
|
||||
// Verify the resulting data.
|
||||
ClusterParser parser;
|
||||
ASSERT_NO_FATAL_FAILURE(parser.PopulateFromCluster(TemplateFileName(0)));
|
||||
ASSERT_EQ(2u, parser.cluster_count());
|
||||
EXPECT_EQ(6, parser.GetFrameCountForCluster(0));
|
||||
EXPECT_EQ(4, parser.GetFrameCountForCluster(1));
|
||||
|
||||
EXPECT_FALSE(File::Open(TemplateFileName(1).c_str(), "r"));
|
||||
}
|
||||
|
||||
} // namespace media
|
||||
} // namespace shaka
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
#include "packager/base/time/time.h"
|
||||
#include "packager/media/base/audio_stream_info.h"
|
||||
#include "packager/media/base/media_handler.h"
|
||||
#include "packager/media/base/media_sample.h"
|
||||
#include "packager/media/base/muxer_options.h"
|
||||
#include "packager/media/base/muxer_util.h"
|
||||
|
@ -28,22 +29,7 @@ int64_t kTimecodeScale = 1000000;
|
|||
int64_t kSecondsToNs = 1000000000L;
|
||||
} // namespace
|
||||
|
||||
Segmenter::Segmenter(const MuxerOptions& options)
|
||||
: reference_frame_timestamp_(0),
|
||||
options_(options),
|
||||
clear_lead_(0),
|
||||
enable_encryption_(false),
|
||||
info_(NULL),
|
||||
muxer_listener_(NULL),
|
||||
progress_listener_(NULL),
|
||||
progress_target_(0),
|
||||
accumulated_progress_(0),
|
||||
first_timestamp_(0),
|
||||
sample_duration_(0),
|
||||
segment_payload_pos_(0),
|
||||
cluster_length_in_time_scale_(0),
|
||||
segment_length_in_time_scale_(0),
|
||||
track_id_(0) {}
|
||||
Segmenter::Segmenter(const MuxerOptions& options) : options_(options) {}
|
||||
|
||||
Segmenter::~Segmenter() {}
|
||||
|
||||
|
@ -110,10 +96,6 @@ Status Segmenter::Initialize(std::unique_ptr<MkvWriter> writer,
|
|||
}
|
||||
|
||||
Status Segmenter::Finalize() {
|
||||
Status status = WriteFrame(true /* write_duration */);
|
||||
if (!status.ok())
|
||||
return status;
|
||||
|
||||
uint64_t duration =
|
||||
prev_sample_->pts() - first_timestamp_ + prev_sample_->duration();
|
||||
segment_info_.set_duration(FromBMFFTimescale(duration));
|
||||
|
@ -137,33 +119,9 @@ Status Segmenter::AddSample(std::shared_ptr<MediaSample> sample) {
|
|||
// previous frame first before creating the new Cluster.
|
||||
|
||||
Status status;
|
||||
bool wrote_frame = false;
|
||||
bool new_segment = false;
|
||||
if (!cluster_) {
|
||||
status = NewSegment(sample->pts());
|
||||
new_segment = true;
|
||||
// First frame, so no previous frame to write.
|
||||
wrote_frame = true;
|
||||
} else if (segment_length_in_time_scale_ >=
|
||||
options_.segment_duration * info_->time_scale()) {
|
||||
if (sample->is_key_frame() || !options_.segment_sap_aligned) {
|
||||
status = WriteFrame(true /* write_duration */);
|
||||
status.Update(NewSegment(sample->pts()));
|
||||
new_segment = true;
|
||||
segment_length_in_time_scale_ = 0;
|
||||
cluster_length_in_time_scale_ = 0;
|
||||
wrote_frame = true;
|
||||
}
|
||||
} else if (cluster_length_in_time_scale_ >=
|
||||
options_.fragment_duration * info_->time_scale()) {
|
||||
if (sample->is_key_frame() || !options_.fragment_sap_aligned) {
|
||||
status = WriteFrame(true /* write_duration */);
|
||||
status.Update(NewSubsegment(sample->pts()));
|
||||
cluster_length_in_time_scale_ = 0;
|
||||
wrote_frame = true;
|
||||
}
|
||||
}
|
||||
if (!wrote_frame) {
|
||||
if (new_segment_ || new_subsegment_) {
|
||||
status = NewSegment(sample->pts(), new_subsegment_);
|
||||
} else {
|
||||
status = WriteFrame(false /* write_duration */);
|
||||
}
|
||||
if (!status.ok())
|
||||
|
@ -173,7 +131,7 @@ Status Segmenter::AddSample(std::shared_ptr<MediaSample> sample) {
|
|||
if (encryptor_) {
|
||||
// Don't enable encryption in the middle of a segment, i.e. only at the
|
||||
// first frame of a segment.
|
||||
if (new_segment && !enable_encryption_) {
|
||||
if (new_segment_ && !enable_encryption_) {
|
||||
if (sample->pts() - first_timestamp_ >=
|
||||
clear_lead_ * info_->time_scale()) {
|
||||
enable_encryption_ = true;
|
||||
|
@ -189,16 +147,22 @@ Status Segmenter::AddSample(std::shared_ptr<MediaSample> sample) {
|
|||
}
|
||||
}
|
||||
|
||||
// Add the sample to the durations even though we have not written the frame
|
||||
// yet. This is needed to make sure we split Clusters at the correct point.
|
||||
// These are only used in this method.
|
||||
cluster_length_in_time_scale_ += sample->duration();
|
||||
segment_length_in_time_scale_ += sample->duration();
|
||||
|
||||
new_subsegment_ = false;
|
||||
new_segment_ = false;
|
||||
prev_sample_ = sample;
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
Status Segmenter::FinalizeSegment(uint64_t start_timescale,
|
||||
uint64_t duration_timescale,
|
||||
bool is_subsegment) {
|
||||
if (is_subsegment)
|
||||
new_subsegment_ = true;
|
||||
else
|
||||
new_segment_ = true;
|
||||
return WriteFrame(true /* write duration */);
|
||||
}
|
||||
|
||||
float Segmenter::GetDuration() const {
|
||||
return static_cast<float>(segment_info_.duration()) *
|
||||
segment_info_.timecode_scale() / kSecondsToNs;
|
||||
|
|
|
@ -76,6 +76,11 @@ class Segmenter {
|
|||
/// @return OK on success, an error status otherwise.
|
||||
Status AddSample(std::shared_ptr<MediaSample> sample);
|
||||
|
||||
/// Finalize the (sub)segment.
|
||||
virtual Status FinalizeSegment(uint64_t start_timescale,
|
||||
uint64_t duration_timescale,
|
||||
bool is_subsegment) = 0;
|
||||
|
||||
/// @return true if there is an initialization range, while setting @a start
|
||||
/// and @a end; or false if initialization range does not apply.
|
||||
virtual bool GetInitRangeStartAndEnd(uint64_t* start, uint64_t* end) = 0;
|
||||
|
@ -113,9 +118,6 @@ class Segmenter {
|
|||
|
||||
int track_id() const { return track_id_; }
|
||||
uint64_t segment_payload_pos() const { return segment_payload_pos_; }
|
||||
uint64_t cluster_length_in_time_scale() const {
|
||||
return cluster_length_in_time_scale_;
|
||||
}
|
||||
|
||||
virtual Status DoInitialize(std::unique_ptr<MkvWriter> writer) = 0;
|
||||
virtual Status DoFinalize() = 0;
|
||||
|
@ -129,25 +131,23 @@ class Segmenter {
|
|||
// Writes the previous frame to the file.
|
||||
Status WriteFrame(bool write_duration);
|
||||
|
||||
// This is called when there needs to be a new subsegment. This does nothing
|
||||
// in single-segment mode. In multi-segment mode this creates a new Cluster
|
||||
// element.
|
||||
virtual Status NewSubsegment(uint64_t start_timescale) = 0;
|
||||
// This is called when there needs to be a new segment. In single-segment
|
||||
// mode, this creates a new Cluster element. In multi-segment mode this
|
||||
// creates a new output file.
|
||||
virtual Status NewSegment(uint64_t start_timescale) = 0;
|
||||
// This is called when there needs to be a new (sub)segment.
|
||||
// In single-segment mode, a Cluster is a segment and there is no subsegment.
|
||||
// In multi-segment mode, a new file is a segment and the clusters in the file
|
||||
// are subsegments.
|
||||
virtual Status NewSegment(uint64_t start_timescale, bool is_subsegment) = 0;
|
||||
|
||||
// Store the previous sample so we know which one is the last frame.
|
||||
std::shared_ptr<MediaSample> prev_sample_;
|
||||
// The reference frame timestamp; used to populate the ReferenceBlock element
|
||||
// when writing non-keyframe BlockGroups.
|
||||
uint64_t reference_frame_timestamp_;
|
||||
uint64_t reference_frame_timestamp_ = 0;
|
||||
|
||||
const MuxerOptions& options_;
|
||||
std::unique_ptr<Encryptor> encryptor_;
|
||||
double clear_lead_;
|
||||
bool enable_encryption_; // Encryption is enabled only after clear_lead_.
|
||||
double clear_lead_ = 0;
|
||||
// Encryption is enabled only after clear_lead_.
|
||||
bool enable_encryption_ = false;
|
||||
|
||||
std::unique_ptr<mkvmuxer::Cluster> cluster_;
|
||||
mkvmuxer::Cues cues_;
|
||||
|
@ -155,22 +155,23 @@ class Segmenter {
|
|||
mkvmuxer::SegmentInfo segment_info_;
|
||||
mkvmuxer::Tracks tracks_;
|
||||
|
||||
StreamInfo* info_;
|
||||
MuxerListener* muxer_listener_;
|
||||
ProgressListener* progress_listener_;
|
||||
uint64_t progress_target_;
|
||||
uint64_t accumulated_progress_;
|
||||
uint64_t first_timestamp_;
|
||||
int64_t sample_duration_;
|
||||
StreamInfo* info_ = nullptr;
|
||||
MuxerListener* muxer_listener_ = nullptr;
|
||||
ProgressListener* progress_listener_ = nullptr;
|
||||
uint64_t progress_target_ = 0;
|
||||
uint64_t accumulated_progress_ = 0;
|
||||
uint64_t first_timestamp_ = 0;
|
||||
int64_t sample_duration_ = 0;
|
||||
// The position (in bytes) of the start of the Segment payload in the init
|
||||
// file. This is also the size of the header before the SeekHead.
|
||||
uint64_t segment_payload_pos_;
|
||||
uint64_t segment_payload_pos_ = 0;
|
||||
|
||||
// Durations in timescale.
|
||||
uint64_t cluster_length_in_time_scale_;
|
||||
uint64_t segment_length_in_time_scale_;
|
||||
|
||||
int track_id_;
|
||||
// Indicate whether a new segment needed to be created, which is always true
|
||||
// in the beginning.
|
||||
bool new_segment_ = true;
|
||||
// Indicate whether a new subsegment needed to be created.
|
||||
bool new_subsegment_ = false;
|
||||
int track_id_ = 0;
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(Segmenter);
|
||||
};
|
||||
|
|
|
@ -75,10 +75,6 @@ std::shared_ptr<MediaSample> SegmentTestBase::CreateSample(
|
|||
MuxerOptions SegmentTestBase::CreateMuxerOptions() const {
|
||||
MuxerOptions ret;
|
||||
ret.output_file_name = output_file_name_;
|
||||
ret.segment_duration = 30; // seconds
|
||||
ret.fragment_duration = 30; // seconds
|
||||
ret.segment_sap_aligned = false;
|
||||
ret.fragment_sap_aligned = false;
|
||||
// Use memory files for temp storage. Normally this would be a bad idea
|
||||
// since it wouldn't support large files, but for tests the files are small.
|
||||
ret.temp_dir = std::string(kMemoryFilePrefix) + "temp/";
|
||||
|
|
|
@ -18,55 +18,22 @@ SingleSegmentSegmenter::SingleSegmentSegmenter(const MuxerOptions& options)
|
|||
|
||||
SingleSegmentSegmenter::~SingleSegmentSegmenter() {}
|
||||
|
||||
Status SingleSegmentSegmenter::DoInitialize(std::unique_ptr<MkvWriter> writer) {
|
||||
writer_ = std::move(writer);
|
||||
Status ret = WriteSegmentHeader(0, writer_.get());
|
||||
init_end_ = writer_->Position() - 1;
|
||||
seek_head()->set_cluster_pos(init_end_ + 1 - segment_payload_pos());
|
||||
return ret;
|
||||
}
|
||||
|
||||
Status SingleSegmentSegmenter::DoFinalize() {
|
||||
Status SingleSegmentSegmenter::FinalizeSegment(uint64_t start_timescale,
|
||||
uint64_t duration_timescale,
|
||||
bool is_subsegment) {
|
||||
Status status = Segmenter::FinalizeSegment(start_timescale,
|
||||
duration_timescale, is_subsegment);
|
||||
if (!status.ok())
|
||||
return status;
|
||||
// No-op for subsegment in single segment mode.
|
||||
if (is_subsegment)
|
||||
return Status::OK;
|
||||
CHECK(cluster());
|
||||
if (!cluster()->Finalize())
|
||||
return Status(error::FILE_FAILURE, "Error finalizing cluster.");
|
||||
|
||||
// Write the Cues to the end of the file.
|
||||
index_start_ = writer_->Position();
|
||||
seek_head()->set_cues_pos(index_start_ - segment_payload_pos());
|
||||
if (!cues()->Write(writer_.get()))
|
||||
return Status(error::FILE_FAILURE, "Error writing Cues data.");
|
||||
|
||||
// The WebM index is at the end of the file.
|
||||
index_end_ = writer_->Position() - 1;
|
||||
writer_->Position(0);
|
||||
|
||||
Status status = WriteSegmentHeader(index_end_ + 1, writer_.get());
|
||||
status.Update(writer_->Close());
|
||||
return status;
|
||||
}
|
||||
|
||||
Status SingleSegmentSegmenter::NewSubsegment(uint64_t start_timescale) {
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
Status SingleSegmentSegmenter::NewSegment(uint64_t start_timescale) {
|
||||
if (cluster() && !cluster()->Finalize())
|
||||
return Status(error::FILE_FAILURE, "Error finalizing cluster.");
|
||||
|
||||
// Create a new Cue point.
|
||||
uint64_t position = writer_->Position();
|
||||
uint64_t start_webm_timecode = FromBMFFTimescale(start_timescale);
|
||||
|
||||
mkvmuxer::CuePoint* cue_point = new mkvmuxer::CuePoint;
|
||||
cue_point->set_time(start_webm_timecode);
|
||||
cue_point->set_track(track_id());
|
||||
cue_point->set_cluster_pos(position - segment_payload_pos());
|
||||
if (!cues()->AddCue(cue_point))
|
||||
return Status(error::INTERNAL_ERROR, "Error adding CuePoint.");
|
||||
|
||||
return SetCluster(start_webm_timecode, position, writer_.get());
|
||||
}
|
||||
|
||||
bool SingleSegmentSegmenter::GetInitRangeStartAndEnd(uint64_t* start,
|
||||
uint64_t* end) {
|
||||
// The init range is the header, from the start of the file to the size of
|
||||
|
@ -85,6 +52,49 @@ bool SingleSegmentSegmenter::GetIndexRangeStartAndEnd(uint64_t* start,
|
|||
return true;
|
||||
}
|
||||
|
||||
Status SingleSegmentSegmenter::DoInitialize(std::unique_ptr<MkvWriter> writer) {
|
||||
writer_ = std::move(writer);
|
||||
Status ret = WriteSegmentHeader(0, writer_.get());
|
||||
init_end_ = writer_->Position() - 1;
|
||||
seek_head()->set_cluster_pos(init_end_ + 1 - segment_payload_pos());
|
||||
return ret;
|
||||
}
|
||||
|
||||
Status SingleSegmentSegmenter::DoFinalize() {
|
||||
// Write the Cues to the end of the file.
|
||||
index_start_ = writer_->Position();
|
||||
seek_head()->set_cues_pos(index_start_ - segment_payload_pos());
|
||||
if (!cues()->Write(writer_.get()))
|
||||
return Status(error::FILE_FAILURE, "Error writing Cues data.");
|
||||
|
||||
// The WebM index is at the end of the file.
|
||||
index_end_ = writer_->Position() - 1;
|
||||
writer_->Position(0);
|
||||
|
||||
Status status = WriteSegmentHeader(index_end_ + 1, writer_.get());
|
||||
status.Update(writer_->Close());
|
||||
return status;
|
||||
}
|
||||
|
||||
Status SingleSegmentSegmenter::NewSegment(uint64_t start_timescale,
|
||||
bool is_subsegment) {
|
||||
// No-op for subsegment in single segment mode.
|
||||
if (is_subsegment)
|
||||
return Status::OK;
|
||||
// Create a new Cue point.
|
||||
uint64_t position = writer_->Position();
|
||||
uint64_t start_webm_timecode = FromBMFFTimescale(start_timescale);
|
||||
|
||||
mkvmuxer::CuePoint* cue_point = new mkvmuxer::CuePoint;
|
||||
cue_point->set_time(start_webm_timecode);
|
||||
cue_point->set_track(track_id());
|
||||
cue_point->set_cluster_pos(position - segment_payload_pos());
|
||||
if (!cues()->AddCue(cue_point))
|
||||
return Status(error::INTERNAL_ERROR, "Error adding CuePoint.");
|
||||
|
||||
return SetCluster(start_webm_timecode, position, writer_.get());
|
||||
}
|
||||
|
||||
} // namespace webm
|
||||
} // namespace media
|
||||
} // namespace shaka
|
||||
|
|
|
@ -30,6 +30,9 @@ class SingleSegmentSegmenter : public Segmenter {
|
|||
|
||||
/// @name Segmenter implementation overrides.
|
||||
/// @{
|
||||
Status FinalizeSegment(uint64_t start_timescale,
|
||||
uint64_t duration_timescale,
|
||||
bool is_subsegment) override;
|
||||
bool GetInitRangeStartAndEnd(uint64_t* start, uint64_t* end) override;
|
||||
bool GetIndexRangeStartAndEnd(uint64_t* start, uint64_t* end) override;
|
||||
/// @}
|
||||
|
@ -50,8 +53,7 @@ class SingleSegmentSegmenter : public Segmenter {
|
|||
|
||||
private:
|
||||
// Segmenter implementation overrides.
|
||||
Status NewSubsegment(uint64_t start_timescale) override;
|
||||
Status NewSegment(uint64_t start_timescale) override;
|
||||
Status NewSegment(uint64_t start_timescale, bool is_subsegment) override;
|
||||
|
||||
std::unique_ptr<MkvWriter> writer_;
|
||||
uint64_t init_end_;
|
||||
|
|
|
@ -13,6 +13,7 @@ namespace media {
|
|||
namespace {
|
||||
|
||||
const uint64_t kDuration = 1000;
|
||||
const bool kSubsegment = true;
|
||||
|
||||
const uint8_t kBasicSupportData[] = {
|
||||
// ID: EBML Header omitted.
|
||||
|
@ -159,22 +160,26 @@ TEST_F(SingleSegmentSegmenterTest, BasicSupport) {
|
|||
CreateSample(kKeyFrame, kDuration, side_data_flag);
|
||||
ASSERT_OK(segmenter_->AddSample(sample));
|
||||
}
|
||||
ASSERT_OK(segmenter_->FinalizeSegment(0, 5 * kDuration, !kSubsegment));
|
||||
ASSERT_OK(segmenter_->Finalize());
|
||||
|
||||
ASSERT_FILE_ENDS_WITH(OutputFileName().c_str(), kBasicSupportData);
|
||||
}
|
||||
|
||||
TEST_F(SingleSegmentSegmenterTest, SplitsClustersOnSegmentDuration) {
|
||||
TEST_F(SingleSegmentSegmenterTest, SplitsClustersOnSegment) {
|
||||
MuxerOptions options = CreateMuxerOptions();
|
||||
options.segment_duration = 4.5; // seconds
|
||||
ASSERT_NO_FATAL_FAILURE(InitializeSegmenter(options));
|
||||
|
||||
// Write the samples to the Segmenter.
|
||||
for (int i = 0; i < 8; i++) {
|
||||
if (i == 5)
|
||||
ASSERT_OK(segmenter_->FinalizeSegment(0, 5 * kDuration, !kSubsegment));
|
||||
std::shared_ptr<MediaSample> sample =
|
||||
CreateSample(kKeyFrame, kDuration, kNoSideData);
|
||||
ASSERT_OK(segmenter_->AddSample(sample));
|
||||
}
|
||||
ASSERT_OK(
|
||||
segmenter_->FinalizeSegment(5 * kDuration, 8 * kDuration, !kSubsegment));
|
||||
ASSERT_OK(segmenter_->Finalize());
|
||||
|
||||
// Verify the resulting data.
|
||||
|
@ -185,17 +190,19 @@ TEST_F(SingleSegmentSegmenterTest, SplitsClustersOnSegmentDuration) {
|
|||
EXPECT_EQ(3, parser.GetFrameCountForCluster(1));
|
||||
}
|
||||
|
||||
TEST_F(SingleSegmentSegmenterTest, IgnoresFragmentDuration) {
|
||||
TEST_F(SingleSegmentSegmenterTest, IgnoresSubsegment) {
|
||||
MuxerOptions options = CreateMuxerOptions();
|
||||
options.fragment_duration = 5; // seconds
|
||||
ASSERT_NO_FATAL_FAILURE(InitializeSegmenter(options));
|
||||
|
||||
// Write the samples to the Segmenter.
|
||||
for (int i = 0; i < 8; i++) {
|
||||
if (i == 5)
|
||||
ASSERT_OK(segmenter_->FinalizeSegment(0, 5 * kDuration, kSubsegment));
|
||||
std::shared_ptr<MediaSample> sample =
|
||||
CreateSample(kKeyFrame, kDuration, kNoSideData);
|
||||
ASSERT_OK(segmenter_->AddSample(sample));
|
||||
}
|
||||
ASSERT_OK(segmenter_->FinalizeSegment(0, 8 * kDuration, !kSubsegment));
|
||||
ASSERT_OK(segmenter_->Finalize());
|
||||
|
||||
// Verify the resulting data.
|
||||
|
@ -205,31 +212,5 @@ TEST_F(SingleSegmentSegmenterTest, IgnoresFragmentDuration) {
|
|||
EXPECT_EQ(8, parser.GetFrameCountForCluster(0));
|
||||
}
|
||||
|
||||
TEST_F(SingleSegmentSegmenterTest, RespectsSAPAlign) {
|
||||
MuxerOptions options = CreateMuxerOptions();
|
||||
options.segment_duration = 3; // seconds
|
||||
options.segment_sap_aligned = true;
|
||||
ASSERT_NO_FATAL_FAILURE(InitializeSegmenter(options));
|
||||
|
||||
// Write the samples to the Segmenter.
|
||||
for (int i = 0; i < 10; i++) {
|
||||
const KeyFrameFlag key_frame_flag = i == 6 ? kKeyFrame : kNotKeyFrame;
|
||||
std::shared_ptr<MediaSample> sample =
|
||||
CreateSample(key_frame_flag, kDuration, kNoSideData);
|
||||
ASSERT_OK(segmenter_->AddSample(sample));
|
||||
}
|
||||
ASSERT_OK(segmenter_->Finalize());
|
||||
|
||||
// Verify the resulting data.
|
||||
ClusterParser parser;
|
||||
ASSERT_NO_FATAL_FAILURE(parser.PopulateFromSegment(OutputFileName()));
|
||||
// Segments are 1 second, so there would normally be 3 frames per cluster,
|
||||
// but since it's SAP aligned and only frame 7 is a key-frame, there are
|
||||
// two clusters with 6 and 4 frames respectively.
|
||||
ASSERT_EQ(2u, parser.cluster_count());
|
||||
EXPECT_EQ(6, parser.GetFrameCountForCluster(0));
|
||||
EXPECT_EQ(4, parser.GetFrameCountForCluster(1));
|
||||
}
|
||||
|
||||
} // namespace media
|
||||
} // namespace shaka
|
||||
|
|
|
@ -87,9 +87,6 @@ Status TwoPassSingleSegmentSegmenter::DoInitialize(
|
|||
}
|
||||
|
||||
Status TwoPassSingleSegmentSegmenter::DoFinalize() {
|
||||
if (!cluster()->Finalize())
|
||||
return Status(error::FILE_FAILURE, "Error finalizing cluster.");
|
||||
|
||||
const uint64_t header_size = init_end() + 1;
|
||||
const uint64_t cues_pos = header_size - segment_payload_pos();
|
||||
const uint64_t cues_size = UpdateCues(cues());
|
||||
|
|
|
@ -72,11 +72,22 @@ Status WebMMuxer::Finalize() {
|
|||
return Status::OK;
|
||||
}
|
||||
|
||||
Status WebMMuxer::DoAddSample(std::shared_ptr<MediaSample> sample) {
|
||||
Status WebMMuxer::AddSample(int stream_id,
|
||||
std::shared_ptr<MediaSample> sample) {
|
||||
DCHECK(segmenter_);
|
||||
DCHECK_EQ(stream_id, 0);
|
||||
return segmenter_->AddSample(sample);
|
||||
}
|
||||
|
||||
Status WebMMuxer::FinalizeSegment(int stream_id,
|
||||
std::shared_ptr<SegmentInfo> segment_info) {
|
||||
DCHECK(segmenter_);
|
||||
DCHECK_EQ(stream_id, 0);
|
||||
return segmenter_->FinalizeSegment(segment_info->start_timestamp,
|
||||
segment_info->duration,
|
||||
segment_info->is_subsegment);
|
||||
}
|
||||
|
||||
void WebMMuxer::FireOnMediaStartEvent() {
|
||||
if (!muxer_listener())
|
||||
return;
|
||||
|
|
|
@ -26,7 +26,9 @@ class WebMMuxer : public Muxer {
|
|||
// Muxer implementation overrides.
|
||||
Status InitializeMuxer() override;
|
||||
Status Finalize() override;
|
||||
Status DoAddSample(std::shared_ptr<MediaSample> sample) override;
|
||||
Status AddSample(int stream_id, std::shared_ptr<MediaSample> sample) override;
|
||||
Status FinalizeSegment(int stream_id,
|
||||
std::shared_ptr<SegmentInfo> segment_info) override;
|
||||
|
||||
void FireOnMediaStartEvent();
|
||||
void FireOnMediaEndEvent();
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
#include "packager/media/base/muxer_util.h"
|
||||
#include "packager/media/base/stream_info.h"
|
||||
#include "packager/media/base/test/status_test_util.h"
|
||||
#include "packager/media/chunking/chunking_handler.h"
|
||||
#include "packager/media/formats/mp4/mp4_muxer.h"
|
||||
#include "packager/media/test/test_data_util.h"
|
||||
|
||||
|
@ -89,7 +90,9 @@ class PackagerTestBasic : public ::testing::TestWithParam<const char*> {
|
|||
// Check if |file1| and |file2| are the same.
|
||||
bool ContentsEqual(const std::string& file1, const std::string file2);
|
||||
|
||||
MuxerOptions SetupOptions(const std::string& output, bool single_segment);
|
||||
ChunkingOptions SetupChunkingOptions();
|
||||
MuxerOptions SetupMuxerOptions(const std::string& output,
|
||||
bool single_segment);
|
||||
void Remux(const std::string& input,
|
||||
const std::string& video_output,
|
||||
const std::string& audio_output,
|
||||
|
@ -116,15 +119,10 @@ bool PackagerTestBasic::ContentsEqual(const std::string& file1,
|
|||
test_directory_.AppendASCII(file2));
|
||||
}
|
||||
|
||||
MuxerOptions PackagerTestBasic::SetupOptions(const std::string& output,
|
||||
MuxerOptions PackagerTestBasic::SetupMuxerOptions(const std::string& output,
|
||||
bool single_segment) {
|
||||
MuxerOptions options;
|
||||
options.segment_duration = kSegmentDurationInSeconds;
|
||||
options.fragment_duration = kFragmentDurationInSecodns;
|
||||
options.segment_sap_aligned = kSegmentSapAligned;
|
||||
options.fragment_sap_aligned = kFragmentSapAligned;
|
||||
options.num_subsegments_per_sidx = kNumSubsegmentsPerSidx;
|
||||
|
||||
options.output_file_name = GetFullPath(output);
|
||||
if (!single_segment)
|
||||
options.segment_template = GetFullPath(kSegmentTemplate);
|
||||
|
@ -132,6 +130,15 @@ MuxerOptions PackagerTestBasic::SetupOptions(const std::string& output,
|
|||
return options;
|
||||
}
|
||||
|
||||
ChunkingOptions PackagerTestBasic::SetupChunkingOptions() {
|
||||
ChunkingOptions options;
|
||||
options.segment_duration_in_seconds = kSegmentDurationInSeconds;
|
||||
options.subsegment_duration_in_seconds = kFragmentDurationInSecodns;
|
||||
options.segment_sap_aligned = kSegmentSapAligned;
|
||||
options.subsegment_sap_aligned = kFragmentSapAligned;
|
||||
return options;
|
||||
}
|
||||
|
||||
void PackagerTestBasic::Remux(const std::string& input,
|
||||
const std::string& video_output,
|
||||
const std::string& audio_output,
|
||||
|
@ -140,7 +147,6 @@ void PackagerTestBasic::Remux(const std::string& input,
|
|||
CHECK(!video_output.empty() || !audio_output.empty());
|
||||
|
||||
Demuxer demuxer(GetFullPath(input));
|
||||
|
||||
std::unique_ptr<KeySource> encryption_key_source(
|
||||
FixedKeySource::CreateFromHexStrings(kKeyIdHex, kKeyHex, "", ""));
|
||||
DCHECK(encryption_key_source);
|
||||
|
@ -148,7 +154,7 @@ void PackagerTestBasic::Remux(const std::string& input,
|
|||
std::shared_ptr<Muxer> muxer_video;
|
||||
if (!video_output.empty()) {
|
||||
muxer_video.reset(
|
||||
new mp4::MP4Muxer(SetupOptions(video_output, single_segment)));
|
||||
new mp4::MP4Muxer(SetupMuxerOptions(video_output, single_segment)));
|
||||
muxer_video->set_clock(&fake_clock_);
|
||||
|
||||
if (enable_encryption) {
|
||||
|
@ -157,13 +163,17 @@ void PackagerTestBasic::Remux(const std::string& input,
|
|||
kMaxUHD1Pixels, kClearLeadInSeconds,
|
||||
kCryptoDurationInSeconds, FOURCC_cenc);
|
||||
}
|
||||
ASSERT_OK(demuxer.SetHandler("video", muxer_video));
|
||||
|
||||
auto chunking_handler =
|
||||
std::make_shared<ChunkingHandler>(SetupChunkingOptions());
|
||||
ASSERT_OK(demuxer.SetHandler("video", chunking_handler));
|
||||
ASSERT_OK(chunking_handler->SetHandler(0, muxer_video));
|
||||
}
|
||||
|
||||
std::shared_ptr<Muxer> muxer_audio;
|
||||
if (!audio_output.empty()) {
|
||||
muxer_audio.reset(
|
||||
new mp4::MP4Muxer(SetupOptions(audio_output, single_segment)));
|
||||
new mp4::MP4Muxer(SetupMuxerOptions(audio_output, single_segment)));
|
||||
muxer_audio->set_clock(&fake_clock_);
|
||||
|
||||
if (enable_encryption) {
|
||||
|
@ -172,7 +182,11 @@ void PackagerTestBasic::Remux(const std::string& input,
|
|||
kMaxUHD1Pixels, kClearLeadInSeconds,
|
||||
kCryptoDurationInSeconds, FOURCC_cenc);
|
||||
}
|
||||
ASSERT_OK(demuxer.SetHandler("audio", muxer_audio));
|
||||
|
||||
auto chunking_handler =
|
||||
std::make_shared<ChunkingHandler>(SetupChunkingOptions());
|
||||
ASSERT_OK(demuxer.SetHandler("audio", chunking_handler));
|
||||
ASSERT_OK(chunking_handler->SetHandler(0, muxer_audio));
|
||||
}
|
||||
|
||||
ASSERT_OK(demuxer.Initialize());
|
||||
|
@ -193,18 +207,20 @@ void PackagerTestBasic::Decrypt(const std::string& input,
|
|||
|
||||
std::shared_ptr<Muxer> muxer;
|
||||
if (!video_output.empty()) {
|
||||
muxer.reset(
|
||||
new mp4::MP4Muxer(SetupOptions(video_output, true)));
|
||||
muxer.reset(new mp4::MP4Muxer(SetupMuxerOptions(video_output, true)));
|
||||
}
|
||||
if (!audio_output.empty()) {
|
||||
muxer.reset(
|
||||
new mp4::MP4Muxer(SetupOptions(audio_output, true)));
|
||||
muxer.reset(new mp4::MP4Muxer(SetupMuxerOptions(audio_output, true)));
|
||||
}
|
||||
ASSERT_TRUE(muxer);
|
||||
muxer->set_clock(&fake_clock_);
|
||||
ASSERT_OK(demuxer.SetHandler("0", muxer));
|
||||
ASSERT_OK(demuxer.Initialize());
|
||||
|
||||
auto chunking_handler =
|
||||
std::make_shared<ChunkingHandler>(SetupChunkingOptions());
|
||||
ASSERT_OK(demuxer.SetHandler("0", chunking_handler));
|
||||
ASSERT_OK(chunking_handler->SetHandler(0, muxer));
|
||||
|
||||
ASSERT_OK(demuxer.Initialize());
|
||||
ASSERT_OK(demuxer.Run());
|
||||
}
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@
|
|||
'dependencies': [
|
||||
'hls/hls.gyp:hls_builder',
|
||||
'media/codecs/codecs.gyp:codecs',
|
||||
'media/chunking/chunking.gyp:chunking',
|
||||
'media/event/media_event.gyp:media_event',
|
||||
'media/file/file.gyp:file',
|
||||
'media/formats/mp2t/mp2t.gyp:mp2t',
|
||||
|
@ -85,6 +86,7 @@
|
|||
],
|
||||
'dependencies': [
|
||||
'media/codecs/codecs.gyp:codecs',
|
||||
'media/chunking/chunking.gyp:chunking',
|
||||
'media/file/file.gyp:file',
|
||||
'media/formats/mp2t/mp2t.gyp:mp2t',
|
||||
'media/formats/mp4/mp4.gyp:mp4',
|
||||
|
|
Loading…
Reference in New Issue