diff --git a/packager/app/test/packager_test.py b/packager/app/test/packager_test.py index cc264c95a5..4b7e4ed31f 100755 --- a/packager/app/test/packager_test.py +++ b/packager/app/test/packager_test.py @@ -1597,6 +1597,13 @@ class PackagerFunctionalTest(PackagerAppTest): self._GetFlags(encryption=True, output_hls=True)) self._CheckTestResults('ec3-and-hls-single-segment-mp4-encrypted') + def testHlsSingleSegmentTs(self): + self.assertPackageSuccess( + self._GetStreams( + ['audio', 'video'], hls=True, test_files=['bear-640x360.ts']), + self._GetFlags(output_hls=True)) + self._CheckTestResults('hls-single-segment-ts') + def testEc3PackedAudioEncrypted(self): streams = [ self._GetStream( diff --git a/packager/app/test/testdata/hls-single-segment-ts/bear-640x360-audio.m3u8 b/packager/app/test/testdata/hls-single-segment-ts/bear-640x360-audio.m3u8 new file mode 100644 index 0000000000..d7c015a03f --- /dev/null +++ b/packager/app/test/testdata/hls-single-segment-ts/bear-640x360-audio.m3u8 @@ -0,0 +1,15 @@ +#EXTM3U +#EXT-X-VERSION:6 +## Generated with https://github.com/shaka-project/shaka-packager version -- +#EXT-X-TARGETDURATION:2 +#EXT-X-PLAYLIST-TYPE:VOD +#EXTINF:0.975, +#EXT-X-BYTERANGE:23312@0 +bear-640x360-audio.ts +#EXTINF:0.998, +#EXT-X-BYTERANGE:24252 +bear-640x360-audio.ts +#EXTINF:0.789, +#EXT-X-BYTERANGE:17296 +bear-640x360-audio.ts +#EXT-X-ENDLIST diff --git a/packager/app/test/testdata/hls-single-segment-ts/bear-640x360-audio.ts b/packager/app/test/testdata/hls-single-segment-ts/bear-640x360-audio.ts new file mode 100644 index 0000000000..eaed17eef1 Binary files /dev/null and b/packager/app/test/testdata/hls-single-segment-ts/bear-640x360-audio.ts differ diff --git a/packager/app/test/testdata/hls-single-segment-ts/bear-640x360-video-iframe.m3u8 b/packager/app/test/testdata/hls-single-segment-ts/bear-640x360-video-iframe.m3u8 new file mode 100644 index 0000000000..adfc872a9d --- /dev/null +++ b/packager/app/test/testdata/hls-single-segment-ts/bear-640x360-video-iframe.m3u8 @@ -0,0 +1,16 @@ +#EXTM3U +#EXT-X-VERSION:6 +## Generated with https://github.com/shaka-project/shaka-packager version -- +#EXT-X-TARGETDURATION:2 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-I-FRAMES-ONLY +#EXTINF:1.001, +#EXT-X-BYTERANGE:15604@376 +bear-640x360-video.ts +#EXTINF:1.001, +#EXT-X-BYTERANGE:18236@105656 +bear-640x360-video.ts +#EXTINF:0.734, +#EXT-X-BYTERANGE:19928@233684 +bear-640x360-video.ts +#EXT-X-ENDLIST diff --git a/packager/app/test/testdata/hls-single-segment-ts/bear-640x360-video.m3u8 b/packager/app/test/testdata/hls-single-segment-ts/bear-640x360-video.m3u8 new file mode 100644 index 0000000000..a1cd8ce3d8 --- /dev/null +++ b/packager/app/test/testdata/hls-single-segment-ts/bear-640x360-video.m3u8 @@ -0,0 +1,15 @@ +#EXTM3U +#EXT-X-VERSION:6 +## Generated with https://github.com/shaka-project/shaka-packager version -- +#EXT-X-TARGETDURATION:2 +#EXT-X-PLAYLIST-TYPE:VOD +#EXTINF:1.001, +#EXT-X-BYTERANGE:105280@0 +bear-640x360-video.ts +#EXTINF:1.001, +#EXT-X-BYTERANGE:128028 +bear-640x360-video.ts +#EXTINF:0.734, +#EXT-X-BYTERANGE:84600 +bear-640x360-video.ts +#EXT-X-ENDLIST diff --git a/packager/app/test/testdata/hls-single-segment-ts/bear-640x360-video.ts b/packager/app/test/testdata/hls-single-segment-ts/bear-640x360-video.ts new file mode 100644 index 0000000000..3e52b07d4e Binary files /dev/null and b/packager/app/test/testdata/hls-single-segment-ts/bear-640x360-video.ts differ diff --git a/packager/app/test/testdata/hls-single-segment-ts/output.m3u8 b/packager/app/test/testdata/hls-single-segment-ts/output.m3u8 new file mode 100644 index 0000000000..b827ca1dc8 --- /dev/null +++ b/packager/app/test/testdata/hls-single-segment-ts/output.m3u8 @@ -0,0 +1,11 @@ +#EXTM3U +## Generated with https://github.com/shaka-project/shaka-packager version -- + +#EXT-X-INDEPENDENT-SEGMENTS + +#EXT-X-MEDIA:TYPE=AUDIO,URI="bear-640x360-audio.m3u8",GROUP-ID="default-audio-group",NAME="stream_0",DEFAULT=NO,AUTOSELECT=YES,CHANNELS="2" + +#EXT-X-STREAM-INF:BANDWIDTH=1217520,AVERAGE-BANDWIDTH=1117320,CODECS="avc1.64001e,mp4a.40.2",RESOLUTION=640x360,FRAME-RATE=29.970,AUDIO="default-audio-group",CLOSED-CAPTIONS=NONE +bear-640x360-video.m3u8 + +#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=217180,AVERAGE-BANDWIDTH=157213,CODECS="avc1.64001e",RESOLUTION=640x360,CLOSED-CAPTIONS=NONE,URI="bear-640x360-video-iframe.m3u8" diff --git a/packager/media/formats/mp2t/ts_muxer.cc b/packager/media/formats/mp2t/ts_muxer.cc index f9de46b634..b00de7c1e1 100644 --- a/packager/media/formats/mp2t/ts_muxer.cc +++ b/packager/media/formats/mp2t/ts_muxer.cc @@ -8,6 +8,9 @@ #include +#include +#include + namespace shaka { namespace media { namespace mp2t { @@ -23,6 +26,16 @@ Status TsMuxer::InitializeMuxer() { if (streams().size() > 1u) return Status(error::MUXER_FAILURE, "Cannot handle more than one streams."); + if (options().segment_template.empty()) { + const std::string& file_name = options().output_file_name; + DCHECK(!file_name.empty()); + output_file_.reset(File::Open(file_name.c_str(), "w")); + if (!output_file_) { + return Status(error::FILE_FAILURE, + "Cannot open file for write " + file_name); + } + } + segmenter_.reset(new TsSegmenter(options(), muxer_listener())); Status status = segmenter_->Initialize(*streams()[0]); FireOnMediaStartEvent(); @@ -49,10 +62,81 @@ Status TsMuxer::AddMediaSample(size_t stream_id, const MediaSample& sample) { Status TsMuxer::FinalizeSegment(size_t stream_id, const SegmentInfo& segment_info) { DCHECK_EQ(stream_id, 0u); - return segment_info.is_subsegment - ? Status::OK - : segmenter_->FinalizeSegment(segment_info.start_timestamp, - segment_info.duration); + + if (segment_info.is_subsegment) + return Status::OK; + + Status s = segmenter_->FinalizeSegment(segment_info.start_timestamp, + segment_info.duration); + if (!s.ok()) + return s; + if (!segmenter_->segment_started()) + return Status::OK; + + int64_t segment_start_timestamp = segmenter_->segment_start_timestamp(); + + std::string segment_path = + options().segment_template.empty() + ? options().output_file_name + : GetSegmentName(options().segment_template, segment_start_timestamp, + segment_number_++, options().bandwidth); + + const int64_t file_size = segmenter_->segment_buffer()->Size(); + + RETURN_IF_ERROR(WriteSegment(segment_path, segmenter_->segment_buffer())); + + total_duration_ += segment_info.duration; + + if (muxer_listener()) { + muxer_listener()->OnNewSegment( + segment_path, + segment_info.start_timestamp * segmenter_->timescale() + + segmenter_->transport_stream_timestamp_offset(), + segment_info.duration * segmenter_->timescale(), file_size); + } + + segmenter_->set_segment_started(false); + + return Status::OK; +} + +Status TsMuxer::WriteSegment(const std::string& segment_path, + BufferWriter* segment_buffer) { + std::unique_ptr file; + + if (output_file_) { + // This is in single segment mode. + Range range; + range.start = media_ranges_.subsegment_ranges.empty() + ? 0 + : (media_ranges_.subsegment_ranges.back().end + 1); + range.end = range.start + segment_buffer->Size() - 1; + media_ranges_.subsegment_ranges.push_back(range); + } else { + file.reset(File::Open(segment_path.c_str(), "w")); + if (!file) { + return Status(error::FILE_FAILURE, + "Cannot open file for write " + segment_path); + } + } + + RETURN_IF_ERROR(segment_buffer->WriteToFile(output_file_ ? output_file_.get() + : file.get())); + + if (file) + RETURN_IF_ERROR(CloseFile(std::move(file))); + return Status::OK; +} + +Status TsMuxer::CloseFile(std::unique_ptr file) { + std::string file_name = file->file_name(); + if (!file.release()->Close()) { + return Status( + error::FILE_FAILURE, + "Cannot close file " + file_name + + ", possibly file permission issue or running out of disk space."); + } + return Status::OK; } void TsMuxer::FireOnMediaStartEvent() { @@ -66,10 +150,7 @@ void TsMuxer::FireOnMediaEndEvent() { if (!muxer_listener()) return; - // For now, there is no single file TS segmenter. So all the values passed - // here are left empty. - MuxerListener::MediaRanges range; - muxer_listener()->OnMediaEnd(range, 0); + muxer_listener()->OnMediaEnd(media_ranges_, total_duration_); } } // namespace mp2t diff --git a/packager/media/formats/mp2t/ts_muxer.h b/packager/media/formats/mp2t/ts_muxer.h index 9f55c2f0e4..145ed98be4 100644 --- a/packager/media/formats/mp2t/ts_muxer.h +++ b/packager/media/formats/mp2t/ts_muxer.h @@ -30,6 +30,10 @@ class TsMuxer : public Muxer { Status FinalizeSegment(size_t stream_id, const SegmentInfo& sample) override; + Status WriteSegment(const std::string& segment_path, + BufferWriter* segment_buffer); + Status CloseFile(std::unique_ptr file); + void FireOnMediaStartEvent(); void FireOnMediaEndEvent(); @@ -37,6 +41,17 @@ class TsMuxer : public Muxer { int64_t sample_durations_[2]; int64_t num_samples_ = 0; + // Used in multi-segment mode for segment template. + uint64_t segment_number_ = 0; + + // Used in single segment mode. + std::unique_ptr output_file_; + + // Keeps track of segment ranges in single segment mode. + MuxerListener::MediaRanges media_ranges_; + + uint64_t total_duration_ = 0; + DISALLOW_COPY_AND_ASSIGN(TsMuxer); }; diff --git a/packager/media/formats/mp2t/ts_segmenter.cc b/packager/media/formats/mp2t/ts_segmenter.cc index f2daaeafde..c50a35d8d3 100644 --- a/packager/media/formats/mp2t/ts_segmenter.cc +++ b/packager/media/formats/mp2t/ts_segmenter.cc @@ -37,8 +37,7 @@ bool IsVideoCodec(Codec codec) { } // namespace TsSegmenter::TsSegmenter(const MuxerOptions& options, MuxerListener* listener) - : muxer_options_(options), - listener_(listener), + : listener_(listener), transport_stream_timestamp_offset_( options.transport_stream_timestamp_offset_ms * kTsTimescale / 1000), pes_packet_generator_( @@ -47,8 +46,6 @@ TsSegmenter::TsSegmenter(const MuxerOptions& options, MuxerListener* listener) TsSegmenter::~TsSegmenter() {} Status TsSegmenter::Initialize(const StreamInfo& stream_info) { - if (muxer_options_.segment_template.empty()) - return Status(error::MUXER_FAILURE, "Segment template not specified."); if (!pes_packet_generator_->Initialize(stream_info)) { return Status(error::MUXER_FAILURE, "Failed to initialize PesPacketGenerator."); @@ -172,40 +169,6 @@ Status TsSegmenter::FinalizeSegment(int64_t start_timestamp, int64_t duration) { Status status = WritePesPackets(); if (!status.ok()) return status; - - // This method may be called from Finalize() so segment_started_ could - // be false. - if (!segment_started_) - return Status::OK; - std::string segment_path = - GetSegmentName(muxer_options_.segment_template, segment_start_timestamp_, - segment_number_++, muxer_options_.bandwidth); - - const int64_t file_size = segment_buffer_.Size(); - std::unique_ptr segment_file; - segment_file.reset(File::Open(segment_path.c_str(), "w")); - if (!segment_file) { - return Status(error::FILE_FAILURE, - "Cannot open file for write " + segment_path); - } - - RETURN_IF_ERROR(segment_buffer_.WriteToFile(segment_file.get())); - - if (!segment_file.release()->Close()) { - return Status( - error::FILE_FAILURE, - "Cannot close file " + segment_path + - ", possibly file permission issue or running out of disk space."); - } - - if (listener_) { - listener_->OnNewSegment(segment_path, - start_timestamp * timescale_scale_ + - transport_stream_timestamp_offset_, - duration * timescale_scale_, file_size); - } - segment_started_ = false; - return Status::OK; } diff --git a/packager/media/formats/mp2t/ts_segmenter.h b/packager/media/formats/mp2t/ts_segmenter.h index 7c765a6029..e551feea87 100644 --- a/packager/media/formats/mp2t/ts_segmenter.h +++ b/packager/media/formats/mp2t/ts_segmenter.h @@ -24,9 +24,6 @@ class MuxerListener; namespace mp2t { -// TODO(rkuroiwa): For now, this implements multifile segmenter. Like other -// make this an abstract super class and implement multifile and single file -// segmenters. class TsSegmenter { public: // TODO(rkuroiwa): Add progress listener? @@ -72,13 +69,22 @@ class TsSegmenter { /// Only for testing. void SetSegmentStartedForTesting(bool value); + int64_t segment_start_timestamp() const { return segment_start_timestamp_; } + BufferWriter* segment_buffer() { return &segment_buffer_; } + void set_segment_started(bool value) { segment_started_ = value; } + bool segment_started() const { return segment_started_; } + + double timescale() const { return timescale_scale_; } + uint32_t transport_stream_timestamp_offset() const { + return transport_stream_timestamp_offset_; + } + private: Status StartSegmentIfNeeded(int64_t next_pts); // Writes PES packets (carried in TsPackets) to a buffer. Status WritePesPackets(); - const MuxerOptions& muxer_options_; MuxerListener* const listener_; // Codec for the stream. @@ -87,18 +93,16 @@ class TsSegmenter { const int32_t transport_stream_timestamp_offset_ = 0; // Scale used to scale the input stream to TS's timesccale (which is 90000). + // Used for calculating the duration in seconds fo the current segment. double timescale_scale_ = 1.0; - // Used for segment template. - uint64_t segment_number_ = 0; - std::unique_ptr ts_writer_; BufferWriter segment_buffer_; // Set to true if segment_buffer_ is initialized, set to false after - // FinalizeSegment() succeeds. + // FinalizeSegment() succeeds in ts_muxer. bool segment_started_ = false; std::unique_ptr pes_packet_generator_; diff --git a/packager/media/formats/mp2t/ts_segmenter_unittest.cc b/packager/media/formats/mp2t/ts_segmenter_unittest.cc index 30fa07abf4..8e3a45ba4a 100644 --- a/packager/media/formats/mp2t/ts_segmenter_unittest.cc +++ b/packager/media/formats/mp2t/ts_segmenter_unittest.cc @@ -207,11 +207,6 @@ TEST_F(TsSegmenterTest, PassedSegmentDuration) { // Doesn't really matter how long this is. sample2->set_duration(kInputTimescale * 7); - EXPECT_CALL(mock_listener, - OnNewSegment("memory://file1.ts", - kFirstPts * kTimeScale / kInputTimescale, - kTimeScale * 11, _)); - Sequence writer_sequence; EXPECT_CALL(*mock_ts_writer_, NewSegment(_)) .InSequence(writer_sequence) @@ -245,10 +240,6 @@ TEST_F(TsSegmenterTest, PassedSegmentDuration) { EXPECT_CALL(*mock_pes_packet_generator_, Flush()) .WillOnce(Return(true)); - EXPECT_CALL(*mock_ts_writer_, NewSegment(_)) - .InSequence(writer_sequence) - .WillOnce(Return(true)); - EXPECT_CALL(*mock_ts_writer_, AddPesPacketMock(_, _)) .Times(2) .WillRepeatedly(Return(true)); @@ -391,8 +382,6 @@ TEST_F(TsSegmenterTest, EncryptedSample) { .InSequence(pes_packet_sequence) .WillOnce(Return(new PesPacket())); - EXPECT_CALL(mock_listener, OnNewSegment("memory://file1.ts", _, _, _)); - MockTsWriter* mock_ts_writer_raw = mock_ts_writer_.get(); segmenter.InjectPesPacketGeneratorForTesting( diff --git a/packager/packager.cc b/packager/packager.cc index de0993de76..8ebeb47ff4 100644 --- a/packager/packager.cc +++ b/packager/packager.cc @@ -224,33 +224,17 @@ Status ValidateStreamDescriptor(bool dump_stream_info, if (output_format == CONTAINER_UNKNOWN) { return Status(error::INVALID_ARGUMENT, "Unsupported output format."); } - if (output_format == MediaContainerName::CONTAINER_MPEG2TS) { - if (stream.segment_template.empty()) { - return Status( - error::INVALID_ARGUMENT, - "Please specify 'segment_template'. Single file TS output is " - "not supported."); - } - // Right now the init segment is saved in |output| for multi-segment - // content. However, for TS all segments must be self-initializing so - // there cannot be an init segment. - if (stream.output.length()) { - return Status(error::INVALID_ARGUMENT, - "All TS segments must be self-initializing. Stream " - "descriptors 'output' or 'init_segment' are not allowed."); - } - } else if (output_format == CONTAINER_WEBVTT || - output_format == CONTAINER_TTML || - output_format == CONTAINER_AAC || output_format == CONTAINER_MP3 || - output_format == CONTAINER_AC3 || - output_format == CONTAINER_EAC3) { + if (output_format == CONTAINER_WEBVTT || output_format == CONTAINER_TTML || + output_format == CONTAINER_AAC || output_format == CONTAINER_MP3 || + output_format == CONTAINER_AC3 || output_format == CONTAINER_EAC3 || + output_format == CONTAINER_MPEG2TS) { // There is no need for an init segment when outputting because there is no // initialization data. if (stream.segment_template.length() && stream.output.length()) { return Status( error::INVALID_ARGUMENT, - "Segmented subtitles or PackedAudio output cannot have an init " + "Segmented subtitles, PackedAudio or TS output cannot have an init " "segment. Do not specify stream descriptors 'output' or " "'init_segment' when using 'segment_template'."); }