From 160700b452403897bc3055913912fa8a78f20d15 Mon Sep 17 00:00:00 2001 From: Kongqun Yang Date: Thu, 23 Feb 2017 17:17:47 -0800 Subject: [PATCH] Integrate ChunkingHandler Also moved MediaHandler output validation to Initialize instead. This CL also addresses #122 with consistent chunking. Change-Id: I60c0da6d1b33421d7828bcb827d18899e71884ce --- packager/app/packager_main.cc | 26 +-- packager/app/packager_util.cc | 57 +++---- packager/app/packager_util.h | 12 +- .../testdata/bear-320x240-vorbis-golden.webm | Bin 23999 -> 23999 bytes .../testdata/bear-640x360-a-cbc1-golden.mp4 | Bin 44604 -> 44604 bytes .../testdata/bear-640x360-a-cbcs-golden.mp4 | Bin 43939 -> 43939 bytes .../testdata/bear-640x360-a-cenc-golden.mp4 | Bin 44604 -> 44604 bytes .../testdata/bear-640x360-a-cens-golden.mp4 | Bin 44604 -> 44604 bytes .../testdata/bear-640x360-a-enc-golden-1.ts | Bin 24440 -> 23312 bytes .../testdata/bear-640x360-a-enc-golden-2.ts | Bin 24816 -> 24252 bytes .../testdata/bear-640x360-a-enc-golden-3.ts | Bin 15604 -> 17296 bytes .../testdata/bear-640x360-a-enc-golden.m3u8 | 8 +- .../test/testdata/bear-640x360-a-golden-1.ts | Bin 24440 -> 23312 bytes .../test/testdata/bear-640x360-a-golden-2.ts | Bin 24816 -> 24252 bytes .../test/testdata/bear-640x360-a-golden-3.ts | Bin 15604 -> 17296 bytes .../test/testdata/bear-640x360-a-golden.m3u8 | 6 +- .../test/testdata/bear-640x360-a-golden.mp4 | Bin 43688 -> 43688 bytes .../bear-640x360-a-live-cenc-golden-2.m4s | Bin 17131 -> 16718 bytes .../bear-640x360-a-live-cenc-golden-3.m4s | Bin 9927 -> 10340 bytes ...-640x360-a-live-cenc-rotation-golden-2.m4s | Bin 17255 -> 16842 bytes ...-640x360-a-live-cenc-rotation-golden-3.m4s | Bin 10051 -> 10464 bytes .../testdata/bear-640x360-a-live-golden-2.m4s | Bin 16726 -> 16321 bytes .../testdata/bear-640x360-a-live-golden-3.m4s | Bin 9626 -> 10031 bytes .../testdata/bear-640x360-a-por-BR-golden.mp4 | Bin 43688 -> 43688 bytes .../testdata/bear-640x360-a-por-golden.mp4 | Bin 43688 -> 43688 bytes .../bear-640x360-av-enc-master-golden.m3u8 | 2 +- .../bear-640x360-av-live-cenc-golden.mpd | 7 +- ...ar-640x360-av-live-cenc-non-iop-golden.mpd | 7 +- ...r-640x360-av-live-cenc-rotation-golden.mpd | 7 +- ...0-av-live-cenc-rotation-non-iop-golden.mpd | 7 +- .../testdata/bear-640x360-av-live-golden.mpd | 7 +- .../bear-640x360-av-live-static-golden.mpd | 7 +- .../bear-640x360-av-master-golden.m3u8 | 2 +- packager/media/base/media_handler.cc | 4 +- packager/media/base/muxer.cc | 6 +- packager/media/base/muxer.h | 9 +- packager/media/base/muxer_options.h | 19 --- packager/media/chunking/chunking_handler.cc | 2 + .../chunking/chunking_handler_unittest.cc | 24 +++ .../crypto/encryption_handler_unittest.cc | 3 +- .../mpd_notify_muxer_listener_unittest.cc | 4 - .../media/event/muxer_listener_test_helper.cc | 4 - packager/media/formats/mp2t/ts_muxer.cc | 12 +- packager/media/formats/mp2t/ts_muxer.h | 4 +- packager/media/formats/mp2t/ts_segmenter.cc | 32 +--- packager/media/formats/mp2t/ts_segmenter.h | 24 +-- .../formats/mp2t/ts_segmenter_unittest.cc | 154 ++---------------- packager/media/formats/mp4/fragmenter.h | 2 + packager/media/formats/mp4/mp4_muxer.cc | 13 +- packager/media/formats/mp4/mp4_muxer.h | 4 +- packager/media/formats/mp4/segmenter.cc | 149 +++++++---------- packager/media/formats/mp4/segmenter.h | 14 +- .../webm/encrypted_segmenter_unittest.cc | 6 +- .../formats/webm/multi_segment_segmenter.cc | 78 ++++----- .../formats/webm/multi_segment_segmenter.h | 8 +- .../webm/multi_segment_segmenter_unittest.cc | 70 ++------ packager/media/formats/webm/segmenter.cc | 72 ++------ packager/media/formats/webm/segmenter.h | 55 ++++--- .../media/formats/webm/segmenter_test_base.cc | 4 - .../formats/webm/single_segment_segmenter.cc | 98 ++++++----- .../formats/webm/single_segment_segmenter.h | 6 +- .../webm/single_segment_segmenter_unittest.cc | 41 ++--- .../webm/two_pass_single_segment_segmenter.cc | 3 - packager/media/formats/webm/webm_muxer.cc | 13 +- packager/media/formats/webm/webm_muxer.h | 4 +- packager/media/test/packager_test.cc | 54 +++--- packager/packager.gyp | 2 + 67 files changed, 477 insertions(+), 675 deletions(-) diff --git a/packager/app/packager_main.cc b/packager/app/packager_main.cc index 284dc95a8f..1104820d50 100644 --- a/packager/app/packager_main.cc +++ b/packager/app/packager_main.cc @@ -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 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(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 encryption_key_source; @@ -495,9 +497,9 @@ bool RunPackager(const StreamDescriptorList& stream_descriptors) { std::vector> 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; } diff --git a/packager/app/packager_util.cc b/packager/app/packager_util.cc index 13158ebf2b..d7b02c7dfa 100644 --- a/packager/app/packager_util.cc +++ b/packager/app/packager_util.cc @@ -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 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) - ? 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.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 mpd_options; } } // namespace media diff --git a/packager/app/packager_util.h b/packager/app/packager_util.h index 684ced2ffd..efd3b53359 100644 --- a/packager/app/packager_util.h +++ b/packager/app/packager_util.h @@ -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 CreateEncryptionKeySource(); /// decryption is not required. std::unique_ptr 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 diff --git a/packager/app/test/testdata/bear-320x240-vorbis-golden.webm b/packager/app/test/testdata/bear-320x240-vorbis-golden.webm index b764a53aa8e11caf689582011394f687d065bd07..dee18892f7cd2e6d82c217d0773e5e5464f077cf 100644 GIT binary patch delta 350 zcmXAdO(=tL0Ege-6t%J>^-7asHq^^XDRNke!@TBWY{tYv4n9hu9QU70KFZ;j!o;h0 zk(sQPk4TQ(q+PTFx!@)jlEmaWJWoBB=eRsad??45I+^5dH{NdcZYS2J}-R7f&Mn)hL$tQNz@ zI@N*-_N!JX45=lsGpd%Mnn~4$8h)t?YFXZ*po!j=L-aArwTnp}y24mwbL%>EKDBB% z<6PS&ZfR|g;f2Ze2!1%-5rND?_ZBL7;a*20yE`@b`PsRG2`+cVAXx9QVTOZghq&ai zVx9?)f+aqB>{w&2_Xu%bdn35!Soa7WGzl%`{Y~-2#?Sy|rv%d(u4XlM+sBuZ|T&x!jq?2z_cL#yIrr2`;H# zOfjR%nBlWp2b~3x6Ra^CN#dB}U3NfGj^X?rRJ~-HO0Fh#^0S{BX7JLjvO@#R# dO;?P>L~CBN>Hk^#&rwV?7ZbnxMi33H_y;itgkJyv diff --git a/packager/app/test/testdata/bear-640x360-a-cbc1-golden.mp4 b/packager/app/test/testdata/bear-640x360-a-cbc1-golden.mp4 index 72a64d7631d79100e4644151febec7a912e25e4f..626fab7cc9426aaf10df5f4730ab245a0b1cdc03 100644 GIT binary patch delta 278 zcmdmUhiT6prVVeI1s%B<7}hXMU;qIP1qOzO&HtFi9VKVx=I5sYg#>cbGEzWtK)?h< zjg#eB#3#RL7ZYdL01`V;QdF7;6lVmowSn})$wjW~j2Q%r6Emwo@*EIx1`(h@K1cuv zfNB{XfjFZ$H7^+=#Gt)7+%1BUg@M7YaPw~WTst6(Np$kwc5%kPP#eXiVnAxAAX%sY zR3r;DXyRnajvieg55pQ!psWCrH44Tire@|AmeG?>b^Kyex5 E0OzPYdH?_b delta 278 zcmdmUhiT6prVVeI1)Y{NFl=C$zyJbj%?u1RoBuJ3J4!Ci&CgE*3JK(KR>uxS%*SFBu}lptCvHErOB9*u>P#+`=-Nfx&@k^LqDOyUmsz6B${6hI39< zXcK4r2z7$ESOQ4n1SA*80~HBQZfNUK1+p;g69n4Ffn=ZjWXtwnY${c`DTyVUPjwcl F003ckJ9z*A diff --git a/packager/app/test/testdata/bear-640x360-a-cbcs-golden.mp4 b/packager/app/test/testdata/bear-640x360-a-cbcs-golden.mp4 index 0e8a01563ba34c1738841110cd854953f515f761..d155296c3b0ee0ea48f56605ab03bb0e7a987271 100644 GIT binary patch delta 179 zcmZ2{ooVrPrVZbirR;kd7}hXMU;qKN-wX^5P&T8$WXCq)&B`pl9T;UmOjafa21b_2 zk!`x-3>!eI50n&@<}olZF#_4zKslAk)voJ=7zBz_^O8aGKmZiBE8J}FmTSiX*fUV)VOm~5=%C(X_?5l*|5Dx1pqdIEB*ig delta 175 zcmZ2{ooVrPrVZbi1s#fc_9DGWS-V;P9RIADmNvuWV2y=kqQ9D3@3U3 diff --git a/packager/app/test/testdata/bear-640x360-a-cenc-golden.mp4 b/packager/app/test/testdata/bear-640x360-a-cenc-golden.mp4 index e0b1c3c07e73d67026add0ff447f143aab106d30..a1192dce828bbd2afd0887311c58a5b35727502e 100644 GIT binary patch delta 278 zcmdmUhiT6prVVeI1s%B<7}hXMU;qIP1qOzO&HtFi9VKVx=I5sYg#>cbGEzWtK)?h< zjg#eB#3#RL7ZYdL01`V;QdF7;6lVmowSn})$wjW~j2Q%r6Emwo@*EIx1`(h@K1cuv zfNB{XfjFZ$H7^+=#Gt)7+%1BUg@M7YaPw~WTst6(Np$kwc5%kPP#eXiVnAxAAX%sY zR3r;DXyRnajvieg55pQ!psWCrH44Tire@|AmeG?>b^Kyex5 E0OzPYdH?_b delta 278 zcmdmUhiT6prVVeI1)Y{NFl=C$zyJbj%?u1RoBuJ3J4!Ci&CgE*3JK(KR>uxS%*SFBu}lptCvHErOB9*u>P#+`=-Nfx&@k^LqDOyUmsz6B${6hI39< zXcK4r2z7$ESOQ4n1SA*80~HBQZfNUK1+p;g69n4Ffn=ZjWXtwnY${c`DTyVUPjwcl F003ckJ9z*A diff --git a/packager/app/test/testdata/bear-640x360-a-cens-golden.mp4 b/packager/app/test/testdata/bear-640x360-a-cens-golden.mp4 index bf6c5b590896532349adf5bf185fdbb81cdce3ad..897dbd4b5a72fba76931c7ed68ef7a681c3a414a 100644 GIT binary patch delta 278 zcmdmUhiT6prVVeI1s%B<7}hXMU;qIP1qOzO&HtFi9VKVx=I5sYg#>cbGEzWtK)?h< zjg#eB#3#RL7ZYdL01`V;QdF7;6lVmowSn})$wjW~j2Q%r6Emwo@*EIx1`(h@K1cuv zfNB{XfjFZ$H7^+=#Gt)7+%1BUg@M7YaPw~WTst6(Np$kwc5%kPP#eXiVnAxAAX%sY zR3r;DXyRnajvieg55pQ!psWCrH44Tire@|AmeG?>b^Kyex5 E0OzPYdH?_b delta 278 zcmdmUhiT6prVVeI1)Y{NFl=C$zyJbj%?u1RoBuJ3J4!Ci&CgE*3JK(KR>uxS%*SFBu}lptCvHErOB9*u>P#+`=-Nfx&@k^LqDOyUmsz6B${6hI39< zXcK4r2z7$ESOQ4n1SA*80~HBQZfNUK1+p;g69n4Ffn=ZjWXtwnY${c`DTyVUPjwcl F003ckJ9z*A diff --git a/packager/app/test/testdata/bear-640x360-a-enc-golden-1.ts b/packager/app/test/testdata/bear-640x360-a-enc-golden-1.ts index 50d83fd0650f3dbc1d944dd5bbdbee95e9aa9d22..c492a868c34e16a4cbc5ed6acce03f6f5febcea8 100644 GIT binary patch delta 9 Qcmeydk8#2_#tj0|02nm{8UO$Q delta 868 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

c)lLD0(c()%VmPfaQNbdO} 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 delta 13 UcmbQx&iJKrLp9@OagIuB04azCH~;_u diff --git a/packager/app/test/testdata/bear-640x360-a-enc-golden.m3u8 b/packager/app/test/testdata/bear-640x360-a-enc-golden.m3u8 index ad8b0a8560..d352c7c2d3 100644 --- a/packager/app/test/testdata/bear-640x360-a-enc-golden.m3u8 +++ b/packager/app/test/testdata/bear-640x360-a-enc-golden.m3u8 @@ -3,12 +3,12 @@ ## Generated with https://github.com/google/shaka-packager version -- #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 diff --git a/packager/app/test/testdata/bear-640x360-a-golden-1.ts b/packager/app/test/testdata/bear-640x360-a-golden-1.ts index 50d83fd0650f3dbc1d944dd5bbdbee95e9aa9d22..c492a868c34e16a4cbc5ed6acce03f6f5febcea8 100644 GIT binary patch delta 9 Qcmeydk8#2_#tj0|02nm{8UO$Q delta 868 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 #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 diff --git a/packager/app/test/testdata/bear-640x360-a-golden.mp4 b/packager/app/test/testdata/bear-640x360-a-golden.mp4 index b1cfb2bc35c5015109ca8c19454b01a0a66c624b..e1e27bde3a1edf9e13994e671918cf8efac5f690 100644 GIT binary patch delta 179 zcmZ2+m1)IQrVY}}g7&ox3~LxBFo1yCYX*jf%{t8Dj*>jN`T1!;A%Wbqj1;gW0|OI~ z{xUhQRacy014!sVNl|GYP?!_nlo|orq(((HSXM$#FEVinz3GFo1w+7z0DiW*ug6$H^wlB8*IvJ(-OdH%zW!mYBTQ zWi=b)LY^n;8d`U$a$u?CFGW=v)smHCN}py zej7NFlTIyu{ElsB-#Du#4T7m@0w|!LA`hG3+e86 zvv1Dp`aeT@ZQhLQe^@!>O*7RlF)y+BrZmqXe`3ypnNpQfZiW}5>Z?zEopkxDkzcLu z?{)c$4F1g6b@i=E$9!E44HFT~$eY>1%vLjhxUPK=e`3;gGaieS*1+P|$2eF-t~WMZ zd1^mRfAYJQ6JaMli?;ZhzKWgK{ZUt{Ud%JG>=C22M}g|ql^(Y_b@NJ`B^`VAs4tb~ z-+P|xepgh0?ql7P?dfaNH>LfU`1Fmy#EKB-o%51wGJ@2VUdpC(JhQlKqZ+pDaQ2am zUpp`FEq?89DcmwUX!euC36fi+3?y40Eqi@zS;Ey>?VDUVuhmX{nr6qcpl5wso6qr{ d#cd+n%;L&44t_g(k@<0SYTBGP=M04I0{}d&-|qkb diff --git a/packager/app/test/testdata/bear-640x360-a-live-cenc-golden-3.m4s b/packager/app/test/testdata/bear-640x360-a-live-cenc-golden-3.m4s index cfb7554998eabf741262926feb90da8dd39be4fe..e5295affc5149cbabc20fb83b1190b348a8fe85b 100644 GIT binary patch delta 542 zcmX@^`y^n3o>2?~6fiO{Xecl+G%!p63owc1=I5sYnF6_K87WXT%nS^SeSRy=(y~C=i4&LfFe*$oWE7SZ1#$(7Q}dFcniY&qOwG(KETbofGXAPp zaCelshjr>`ULtuDLHoM_XfzL@b&)?KZ4@hOXbzwKYem2<@YkdUL^$y4{H ze(Y^k73TX|_1=BX*0fWiOmoT`IV>dE{pQ3is9!tpnrWYbckT=6?sv0q&g=R=LwarA zjO%|`Ipj?<)h;nFvG}Gm&mn(e&Vrdzl~Qho7o+N{Pko(q`Kys%t?uu2`HKwx%-D7H ztxCsyT@4Kr5zWY(*}}|LGk>_QeGq?Q(seT)ieZDVw>fq5N}MGfd-kX=mFC}jp6h;BRDkYd z-IMLAxND;tw(W5Ck&ItEFYhgW?QbdE zGCOGYlfwy;TcivmTOTcZeQa66)miPETsg1RPJNnY$FiVjeVbI9&+(qcZQIP^$}|ps YJA0A&adc|hoHyqTgzj$+Whznu0Nsh)-~a#s delta 140 zcmaD7aNKu-o>2k=6fiO{s5LV%)G$l{3ovo!=I5sYnF6_K87WXT%nS^SA15a0i6wvp zCX^JF<}olZ0d+FS18KpDD|%Fc3m=z;*xb@3^K(@=>;IA#i^O;Kng57Ifv0f6{v)0lF9uWoX(utW&fz-$gQmyxgkDY#;rd0yDC96v>N4c*=U%Gflda-1EG^?yus+Z6_2B zchzq#Ip6d2#gfClmFJr^S6sfn>cPdzcdd_)ybk)waw)M-P2xJImI!#c?BjZEd)x>(m!1`U+R@O0E|DPzd}b)Be>xXidN{}tnA)uIP>wM_|P z+jUznckeato|85qVaw{7Q5J%Y%JnkR9r`CYZmeGA%%D2!WAK;FcZ4RCh3OfG^CzD^ l*?(k>%yQ=gH}@)vtg-qdT|C>v&P-9hVY2d_CegS=bpXvF;OqbZ diff --git a/packager/app/test/testdata/bear-640x360-a-live-cenc-rotation-golden-3.m4s b/packager/app/test/testdata/bear-640x360-a-live-cenc-rotation-golden-3.m4s index d924e4bcad2305c5fc3a5441d8fc1622f03bb085..cc56e2ad5158bb45f85fef05db8c08ae8267d835 100644 GIT binary patch delta 9965 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 z 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 delta 9548 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 diff --git a/packager/app/test/testdata/bear-640x360-a-live-golden-2.m4s b/packager/app/test/testdata/bear-640x360-a-live-golden-2.m4s index 1ac70f0d413ce91968a0173f3770ae7275a6994d..8871a6a0d90e02d60f5d4c9c4063b2da5ed6880b 100644 GIT binary patch delta 78 zcmccC#CWiNf|Z(mEd#?Eh6xNHz{r!EpPvR~2;`<^q=1DO7?^~z{4x!?rl`ht7uaP# zRC>5#ZsD?p#q;ZZrB|jd{C47yX6Tg_j#`!n6%&swkhtpn{;txKMK5w?oMS$H*<-y- zQTW4g_Vn7nX&2?!Y8B2sC-Os2XF=Vx1)428yjP}WKGIV<_iV@9-x-?c9waS&`C-!G z)T#9$OC_3jsjBYdyZ73uVA)Q#a>i}^Dk^(@4xR9tB2o2LM>WyRY@Ka%&_j+t>Z#=sGW{Wf#UuRy{O{Ld diff --git a/packager/app/test/testdata/bear-640x360-a-live-golden-3.m4s b/packager/app/test/testdata/bear-640x360-a-live-golden-3.m4s index 8c4542ca9d2f08910f93fbad1c63433f0dc18ce4..76937b2cb921c7ade0355eb5259a7305e4a12888 100644 GIT binary patch delta 506 zcmbQ`z20wvo>2?~6fiO{sJ&)jXkeHC5qOZBpPvR~3go6`q(Id$1L+eJ6ZE9Ox~7yA zmF6)pFadQkC;;&b1_s876PMK0t8wS1B$g;@vMlYAyXa_B9mf6o>Zv(?nFd``RAajf z?6My!JzO!jaM{A*`SrfiD^nMKJ8?)e^vViHEz5(7iN_X5Ty=hbSLw;37r8RdF`vHd zv0kPq{NXrzdhOq|i}Gu=3g?~^`Jtz?pl;d%&6XYBE7LL`=_#Fiwqx$^49#;7l9txL z{4nWo>eP^>63x3*Rrm4Td+k)PY$sbe<2HU3mAyWPPIyg`sCuiTnrLRWQ6wyzb$!W{ zuwP-{ElUz7fXqjd$!|Z)k#Zy*!s{AVGA2k=6fiO{sD?2x)G$nd2;9ld&rbs~1#;6eQlM&>f%K7y33_4)Ab|-b gMWuNR3`{_s4DvwyWa5fCHkGQ}l*E$Fdzp$<0FJyAy8r+H diff --git a/packager/app/test/testdata/bear-640x360-a-por-BR-golden.mp4 b/packager/app/test/testdata/bear-640x360-a-por-BR-golden.mp4 index d50176a494bb6f4f2ccf39e57cae4f942c03a25e..e2b16076c30a3c9e7adfd0a289887f30bc56cbf6 100644 GIT binary patch delta 179 zcmZ2+m1)IQrVY}}g7&ox3~LxBFo1yCYX*jf%{t8Dj*>jN`T1!;A%Wbqj1;gW0|OI~ z{xUhQRacy014!sVNl|GYP?!_nlo|orq(((HSXM$#FEVinz3GFo1w+7z0DiW*ug6$H^wlB8*IvJ(-OdH%zW!mYBTQ zWi=b)LjN`T1!;A%Wbqj1;gW0|OI~ z{xUhQRacy014!sVNl|GYP?!_nlo|orq(((HSXM$#FEVinz3GFo1w+7z0DiW*ug6$H^wlB8*IvJ(-OdH%zW!mYBTQ zWi=b)L-- #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 diff --git a/packager/app/test/testdata/bear-640x360-av-live-cenc-golden.mpd b/packager/app/test/testdata/bear-640x360-av-live-cenc-golden.mpd index 74c7f215b7..7518715304 100644 --- a/packager/app/test/testdata/bear-640x360-av-live-cenc-golden.mpd +++ b/packager/app/test/testdata/bear-640x360-av-live-cenc-golden.mpd @@ -21,12 +21,13 @@ AAAANHBzc2gBAAAAEHfv7MCyTQKs4zweUuL7SwAAAAExMjM0NTY3ODkwMTIzNDU2AAAAAA== - + - - + + + diff --git a/packager/app/test/testdata/bear-640x360-av-live-cenc-non-iop-golden.mpd b/packager/app/test/testdata/bear-640x360-av-live-cenc-non-iop-golden.mpd index d6c6df6147..e3768f912d 100644 --- a/packager/app/test/testdata/bear-640x360-av-live-cenc-non-iop-golden.mpd +++ b/packager/app/test/testdata/bear-640x360-av-live-cenc-non-iop-golden.mpd @@ -17,7 +17,7 @@ - + @@ -25,8 +25,9 @@ - - + + + diff --git a/packager/app/test/testdata/bear-640x360-av-live-cenc-rotation-golden.mpd b/packager/app/test/testdata/bear-640x360-av-live-cenc-rotation-golden.mpd index ee85437b08..07b81749c6 100644 --- a/packager/app/test/testdata/bear-640x360-av-live-cenc-rotation-golden.mpd +++ b/packager/app/test/testdata/bear-640x360-av-live-cenc-rotation-golden.mpd @@ -17,12 +17,13 @@ - + - - + + + diff --git a/packager/app/test/testdata/bear-640x360-av-live-cenc-rotation-non-iop-golden.mpd b/packager/app/test/testdata/bear-640x360-av-live-cenc-rotation-non-iop-golden.mpd index acac1aef23..4adc7ed781 100644 --- a/packager/app/test/testdata/bear-640x360-av-live-cenc-rotation-non-iop-golden.mpd +++ b/packager/app/test/testdata/bear-640x360-av-live-cenc-rotation-non-iop-golden.mpd @@ -15,14 +15,15 @@ - + - - + + + diff --git a/packager/app/test/testdata/bear-640x360-av-live-golden.mpd b/packager/app/test/testdata/bear-640x360-av-live-golden.mpd index 3d2b5a017e..125a7785a5 100644 --- a/packager/app/test/testdata/bear-640x360-av-live-golden.mpd +++ b/packager/app/test/testdata/bear-640x360-av-live-golden.mpd @@ -13,12 +13,13 @@ - + - - + + + diff --git a/packager/app/test/testdata/bear-640x360-av-live-static-golden.mpd b/packager/app/test/testdata/bear-640x360-av-live-static-golden.mpd index d455b643e6..dfe32584f7 100644 --- a/packager/app/test/testdata/bear-640x360-av-live-static-golden.mpd +++ b/packager/app/test/testdata/bear-640x360-av-live-static-golden.mpd @@ -13,12 +13,13 @@ - + - - + + + diff --git a/packager/app/test/testdata/bear-640x360-av-master-golden.m3u8 b/packager/app/test/testdata/bear-640x360-av-master-golden.m3u8 index 165ca15afe..f3be2834f1 100644 --- a/packager/app/test/testdata/bear-640x360-av-master-golden.m3u8 +++ b/packager/app/test/testdata/bear-640x360-av-master-golden.m3u8 @@ -1,5 +1,5 @@ #EXTM3U ## Generated with https://github.com/google/shaka-packager version -- #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 diff --git a/packager/media/base/media_handler.cc b/packager/media/base/media_handler.cc index 0b91c0f8ba..84758de4d3 100644 --- a/packager/media/base/media_handler.cc +++ b/packager/media/base/media_handler.cc @@ -11,7 +11,7 @@ namespace media { Status MediaHandler::SetHandler(int output_stream_index, std::shared_ptr 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; diff --git a/packager/media/base/muxer.cc b/packager/media/base/muxer.cc index d3cda19f4a..87edbf79ee 100644 --- a/packager/media/base/muxer.cc +++ b/packager/media/base/muxer.cc @@ -64,8 +64,12 @@ Status Muxer::Process(std::unique_ptr 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(stream_data->stream_data_type) << " ignored."; diff --git a/packager/media/base/muxer.h b/packager/media/base/muxer.h index 0aa1dcd6a5..2c62cc08e4 100644 --- a/packager/media/base/muxer.h +++ b/packager/media/base/muxer.h @@ -123,8 +123,13 @@ class Muxer : public MediaHandler { // Final clean up. virtual Status Finalize() = 0; - // AddSample implementation. - virtual Status DoAddSample(std::shared_ptr sample) = 0; + // Add a new sample. + virtual Status AddSample(int stream_id, + std::shared_ptr sample) = 0; + + // Finalize the segment or subsegment. + virtual Status FinalizeSegment(int stream_id, + std::shared_ptr segment_info) = 0; MuxerOptions options_; std::vector> streams_; diff --git a/packager/media/base/muxer_options.h b/packager/media/base/muxer_options.h index 327c437c82..14dc73b6f2 100644 --- a/packager/media/base/muxer_options.h +++ b/packager/media/base/muxer_options.h @@ -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 diff --git a/packager/media/chunking/chunking_handler.cc b/packager/media/chunking/chunking_handler.cc index 247a52abb1..562edf164b 100644 --- a/packager/media/chunking/chunking_handler.cc +++ b/packager/media/chunking/chunking_handler.cc @@ -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; } } diff --git a/packager/media/chunking/chunking_handler_unittest.cc b/packager/media/chunking/chunking_handler_unittest.cc index 608b546300..5e1072311f 100644 --- a/packager/media/chunking/chunking_handler_unittest.cc +++ b/packager/media/chunking/chunking_handler_unittest.cc @@ -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; diff --git a/packager/media/crypto/encryption_handler_unittest.cc b/packager/media/crypto/encryption_handler_unittest.cc index fcf3a64ef5..bbb27fce84 100644 --- a/packager/media/crypto/encryption_handler_unittest.cc +++ b/packager/media/crypto/encryption_handler_unittest.cc @@ -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) { diff --git a/packager/media/event/mpd_notify_muxer_listener_unittest.cc b/packager/media/event/mpd_notify_muxer_listener_unittest.cc index 16feac4a2a..ec7819aca4 100644 --- a/packager/media/event/mpd_notify_muxer_listener_unittest.cc +++ b/packager/media/event/mpd_notify_muxer_listener_unittest.cc @@ -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"; diff --git a/packager/media/event/muxer_listener_test_helper.cc b/packager/media/event/muxer_listener_test_helper.cc index f309ad6602..ff23526a26 100644 --- a/packager/media/event/muxer_listener_test_helper.cc +++ b/packager/media/event/muxer_listener_test_helper.cc @@ -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(); diff --git a/packager/media/formats/mp2t/ts_muxer.cc b/packager/media/formats/mp2t/ts_muxer.cc index db688c728c..611fbf4409 100644 --- a/packager/media/formats/mp2t/ts_muxer.cc +++ b/packager/media/formats/mp2t/ts_muxer.cc @@ -34,10 +34,20 @@ Status TsMuxer::Finalize() { return segmenter_->Finalize(); } -Status TsMuxer::DoAddSample(std::shared_ptr sample) { +Status TsMuxer::AddSample(int stream_id, std::shared_ptr sample) { + DCHECK_EQ(stream_id, 0); return segmenter_->AddSample(sample); } +Status TsMuxer::FinalizeSegment(int stream_id, + std::shared_ptr 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; diff --git a/packager/media/formats/mp2t/ts_muxer.h b/packager/media/formats/mp2t/ts_muxer.h index 3308023e23..9271fd7910 100644 --- a/packager/media/formats/mp2t/ts_muxer.h +++ b/packager/media/formats/mp2t/ts_muxer.h @@ -26,7 +26,9 @@ class TsMuxer : public Muxer { // Muxer implementation. Status InitializeMuxer() override; Status Finalize() override; - Status DoAddSample(std::shared_ptr sample) override; + Status AddSample(int stream_id, std::shared_ptr sample) override; + Status FinalizeSegment(int stream_id, + std::shared_ptr sample) override; void FireOnMediaStartEvent(); void FireOnMediaEndEvent(); diff --git a/packager/media/formats/mp2t/ts_segmenter.cc b/packager/media/formats/mp2t/ts_segmenter.cc index e74bd21e2c..b6fe894080 100644 --- a/packager/media/formats/mp2t/ts_segmenter.cc +++ b/packager/media/formats/mp2t/ts_segmenter.cc @@ -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 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 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(); } diff --git a/packager/media/formats/mp2t/ts_segmenter.h b/packager/media/formats/mp2t/ts_segmenter.h index 47ff1e7e9f..347207585f 100644 --- a/packager/media/formats/mp2t/ts_segmenter.h +++ b/packager/media/formats/mp2t/ts_segmenter.h @@ -54,6 +54,18 @@ class TsSegmenter { /// @return OK on success. Status AddSample(std::shared_ptr 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 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 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_; diff --git a/packager/media/formats/mp2t/ts_segmenter_unittest.cc b/packager/media/formats/mp2t/ts_segmenter_unittest.cc index b6a59d95e4..3b5d3bf45a 100644 --- a/packager/media/formats/mp2t/ts_segmenter_unittest.cc +++ b/packager/media/formats/mp2t/ts_segmenter_unittest.cc @@ -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 sample1 = MediaSample::CopyFrom(kAnyData, arraysize(kAnyData), kIsKeyFrame); + sample1->set_duration(kInputTimescale * 11); std::shared_ptr 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 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 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 key_frame_sample1 = - MediaSample::CopyFrom(kAnyData, arraysize(kAnyData), kIsKeyFrame); - std::shared_ptr non_key_frame_sample = - MediaSample::CopyFrom(kAnyData, arraysize(kAnyData), !kIsKeyFrame); - std::shared_ptr 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 sample1 = MediaSample::CopyFrom(kAnyData, arraysize(kAnyData), kIsKeyFrame); + sample1->set_duration(kTimeScale * 2); std::shared_ptr 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)); } diff --git a/packager/media/formats/mp4/fragmenter.h b/packager/media/formats/mp4/fragmenter.h index 2150a1df87..7c38b67a63 100644 --- a/packager/media/formats/mp4/fragmenter.h +++ b/packager/media/formats/mp4/fragmenter.h @@ -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 { diff --git a/packager/media/formats/mp4/mp4_muxer.cc b/packager/media/formats/mp4/mp4_muxer.cc index 67f08f9e3f..9e1b6add7a 100644 --- a/packager/media/formats/mp4/mp4_muxer.cc +++ b/packager/media/formats/mp4/mp4_muxer.cc @@ -161,9 +161,18 @@ Status MP4Muxer::Finalize() { return Status::OK; } -Status MP4Muxer::DoAddSample(std::shared_ptr sample) { +Status MP4Muxer::AddSample(int stream_id, std::shared_ptr sample) { DCHECK(segmenter_); - return segmenter_->AddSample(*streams()[0], sample); + return segmenter_->AddSample(stream_id, sample); +} + +Status MP4Muxer::FinalizeSegment(int stream_id, + std::shared_ptr 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) { diff --git a/packager/media/formats/mp4/mp4_muxer.h b/packager/media/formats/mp4/mp4_muxer.h index 5a16a2ce52..8832ce7182 100644 --- a/packager/media/formats/mp4/mp4_muxer.h +++ b/packager/media/formats/mp4/mp4_muxer.h @@ -37,7 +37,9 @@ class MP4Muxer : public Muxer { // Muxer implementation overrides. Status InitializeMuxer() override; Status Finalize() override; - Status DoAddSample(std::shared_ptr sample) override; + Status AddSample(int stream_id, std::shared_ptr sample) override; + Status FinalizeSegment(int stream_id, + std::shared_ptr segment_info) override; // Generate Audio/Video Track box. void InitializeTrak(const StreamInfo* info, Track* trak); diff --git a/packager/media/formats/mp4/segmenter.cc b/packager/media/formats/mp4/segmenter.cc index 2ce2f417a1..954672a378 100644 --- a/packager/media/formats/mp4/segmenter.cc +++ b/packager/media/formats/mp4/segmenter.cc @@ -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 : 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 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(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(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(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::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(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 : 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(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(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 diff --git a/packager/media/formats/mp4/segmenter.h b/packager/media/formats/mp4/segmenter.h index 6279b32ad4..477ea0f9a3 100644 --- a/packager/media/formats/mp4/segmenter.h +++ b/packager/media/formats/mp4/segmenter.h @@ -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 sample); + Status AddSample(int stream_id, std::shared_ptr 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 ftyp_; std::unique_ptr moov_; @@ -142,7 +145,6 @@ class Segmenter { std::unique_ptr fragment_buffer_; std::unique_ptr sidx_; std::vector> fragmenters_; - std::vector segment_durations_; MuxerListener* muxer_listener_; ProgressListener* progress_listener_; uint64_t progress_target_; diff --git a/packager/media/formats/webm/encrypted_segmenter_unittest.cc b/packager/media/formats/webm/encrypted_segmenter_unittest.cc index ac6a55ceaa..01b2bfaff4 100644 --- a/packager/media/formats/webm/encrypted_segmenter_unittest.cc +++ b/packager/media/formats/webm/encrypted_segmenter_unittest.cc @@ -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 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); diff --git a/packager/media/formats/webm/multi_segment_segmenter.cc b/packager/media/formats/webm/multi_segment_segmenter.cc index d085641f8b..7c4c8d2115 100644 --- a/packager/media/formats/webm/multi_segment_segmenter.cc +++ b/packager/media/formats/webm/multi_segment_segmenter.cc @@ -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,53 +58,23 @@ Status MultiSegmentSegmenter::DoInitialize(std::unique_ptr 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); +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); + writer_.reset(new MkvWriter); + Status status = writer_->Open(segment_name); + if (!status.ok()) + return status; + num_segment_++; } - 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; - } - - // Create a new file for the new segment. - std::string segment_name = - 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()); } diff --git a/packager/media/formats/webm/multi_segment_segmenter.h b/packager/media/formats/webm/multi_segment_segmenter.h index 99da7b1f90..ca4d839fc0 100644 --- a/packager/media/formats/webm/multi_segment_segmenter.h +++ b/packager/media/formats/webm/multi_segment_segmenter.h @@ -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 writer_; uint32_t num_segment_; diff --git a/packager/media/formats/webm/multi_segment_segmenter_unittest.cc b/packager/media/formats/webm/multi_segment_segmenter_unittest.cc index 9140e975b1..eb93f2e983 100644 --- a/packager/media/formats/webm/multi_segment_segmenter_unittest.cc +++ b/packager/media/formats/webm/multi_segment_segmenter_unittest.cc @@ -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 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 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 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 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 diff --git a/packager/media/formats/webm/segmenter.cc b/packager/media/formats/webm/segmenter.cc index 354725fcc9..abec3ad18b 100644 --- a/packager/media/formats/webm/segmenter.cc +++ b/packager/media/formats/webm/segmenter.cc @@ -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 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 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 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 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(segment_info_.duration()) * segment_info_.timecode_scale() / kSecondsToNs; diff --git a/packager/media/formats/webm/segmenter.h b/packager/media/formats/webm/segmenter.h index 9095a4760e..3110cbba33 100644 --- a/packager/media/formats/webm/segmenter.h +++ b/packager/media/formats/webm/segmenter.h @@ -76,6 +76,11 @@ class Segmenter { /// @return OK on success, an error status otherwise. Status AddSample(std::shared_ptr 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 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 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_; - 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 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); }; diff --git a/packager/media/formats/webm/segmenter_test_base.cc b/packager/media/formats/webm/segmenter_test_base.cc index a1deed791a..3452ffc4a4 100644 --- a/packager/media/formats/webm/segmenter_test_base.cc +++ b/packager/media/formats/webm/segmenter_test_base.cc @@ -75,10 +75,6 @@ std::shared_ptr 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/"; diff --git a/packager/media/formats/webm/single_segment_segmenter.cc b/packager/media/formats/webm/single_segment_segmenter.cc index 81688b1bbc..fe190884c9 100644 --- a/packager/media/formats/webm/single_segment_segmenter.cc +++ b/packager/media/formats/webm/single_segment_segmenter.cc @@ -18,55 +18,22 @@ SingleSegmentSegmenter::SingleSegmentSegmenter(const MuxerOptions& options) SingleSegmentSegmenter::~SingleSegmentSegmenter() {} -Status SingleSegmentSegmenter::DoInitialize(std::unique_ptr 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 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 diff --git a/packager/media/formats/webm/single_segment_segmenter.h b/packager/media/formats/webm/single_segment_segmenter.h index ff8efd54f2..fbbf56be00 100644 --- a/packager/media/formats/webm/single_segment_segmenter.h +++ b/packager/media/formats/webm/single_segment_segmenter.h @@ -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 writer_; uint64_t init_end_; diff --git a/packager/media/formats/webm/single_segment_segmenter_unittest.cc b/packager/media/formats/webm/single_segment_segmenter_unittest.cc index 6d5bf2d5fb..d8e2456bd7 100644 --- a/packager/media/formats/webm/single_segment_segmenter_unittest.cc +++ b/packager/media/formats/webm/single_segment_segmenter_unittest.cc @@ -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 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 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 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 diff --git a/packager/media/formats/webm/two_pass_single_segment_segmenter.cc b/packager/media/formats/webm/two_pass_single_segment_segmenter.cc index cc3e1ce885..450d180f9d 100644 --- a/packager/media/formats/webm/two_pass_single_segment_segmenter.cc +++ b/packager/media/formats/webm/two_pass_single_segment_segmenter.cc @@ -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()); diff --git a/packager/media/formats/webm/webm_muxer.cc b/packager/media/formats/webm/webm_muxer.cc index d4ac7bc043..c96d75004f 100644 --- a/packager/media/formats/webm/webm_muxer.cc +++ b/packager/media/formats/webm/webm_muxer.cc @@ -72,11 +72,22 @@ Status WebMMuxer::Finalize() { return Status::OK; } -Status WebMMuxer::DoAddSample(std::shared_ptr sample) { +Status WebMMuxer::AddSample(int stream_id, + std::shared_ptr sample) { DCHECK(segmenter_); + DCHECK_EQ(stream_id, 0); return segmenter_->AddSample(sample); } +Status WebMMuxer::FinalizeSegment(int stream_id, + std::shared_ptr 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; diff --git a/packager/media/formats/webm/webm_muxer.h b/packager/media/formats/webm/webm_muxer.h index ff7e3b13bb..6aba30b565 100644 --- a/packager/media/formats/webm/webm_muxer.h +++ b/packager/media/formats/webm/webm_muxer.h @@ -26,7 +26,9 @@ class WebMMuxer : public Muxer { // Muxer implementation overrides. Status InitializeMuxer() override; Status Finalize() override; - Status DoAddSample(std::shared_ptr sample) override; + Status AddSample(int stream_id, std::shared_ptr sample) override; + Status FinalizeSegment(int stream_id, + std::shared_ptr segment_info) override; void FireOnMediaStartEvent(); void FireOnMediaEndEvent(); diff --git a/packager/media/test/packager_test.cc b/packager/media/test/packager_test.cc index a8ae1be2d9..1a6faa7de8 100644 --- a/packager/media/test/packager_test.cc +++ b/packager/media/test/packager_test.cc @@ -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 { // 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, - bool single_segment) { +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 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_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(SetupChunkingOptions()); + ASSERT_OK(demuxer.SetHandler("video", chunking_handler)); + ASSERT_OK(chunking_handler->SetHandler(0, muxer_video)); } std::shared_ptr 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(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; 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(SetupChunkingOptions()); + ASSERT_OK(demuxer.SetHandler("0", chunking_handler)); + ASSERT_OK(chunking_handler->SetHandler(0, muxer)); + + ASSERT_OK(demuxer.Initialize()); ASSERT_OK(demuxer.Run()); } diff --git a/packager/packager.gyp b/packager/packager.gyp index 859217bf57..2232251ad6 100644 --- a/packager/packager.gyp +++ b/packager/packager.gyp @@ -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',