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:
Kongqun Yang 2017-02-23 17:17:47 -08:00 committed by KongQun Yang
parent a3ce51785a
commit 160700b452
67 changed files with 477 additions and 675 deletions

View File

@ -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;
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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;

View File

@ -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.";

View File

@ -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_;

View File

@ -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

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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) {

View File

@ -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";

View File

@ -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();

View File

@ -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;

View File

@ -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();

View File

@ -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();
}

View File

@ -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_;

View File

@ -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));
}

View File

@ -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 {

View File

@ -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) {

View File

@ -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);

View File

@ -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

View File

@ -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_;

View File

@ -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);

View File

@ -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());

View File

@ -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_;

View File

@ -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

View File

@ -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;

View File

@ -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);
};

View File

@ -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/";

View File

@ -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

View File

@ -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_;

View File

@ -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

View File

@ -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());

View File

@ -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;

View File

@ -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();

View File

@ -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());
}

View File

@ -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',