From 39beb99d6cfe8cc1cf2a4f0dd8f8837e94927f73 Mon Sep 17 00:00:00 2001 From: KongQun Yang Date: Thu, 10 May 2018 14:51:24 -0700 Subject: [PATCH] Implemented PackedAudioWriter Issue #342. Change-Id: Ic1379b00e38f818460f24ad57122ca22c5b5285a --- packager/media/event/muxer_listener.h | 3 +- .../formats/packed_audio/packed_audio.gyp | 5 + .../packed_audio/packed_audio_writer.cc | 132 +++++++++ .../packed_audio/packed_audio_writer.h | 61 ++++ .../packed_audio_writer_unittest.cc | 260 ++++++++++++++++++ 5 files changed, 460 insertions(+), 1 deletion(-) create mode 100644 packager/media/formats/packed_audio/packed_audio_writer.cc create mode 100644 packager/media/formats/packed_audio/packed_audio_writer.h create mode 100644 packager/media/formats/packed_audio/packed_audio_writer_unittest.cc diff --git a/packager/media/event/muxer_listener.h b/packager/media/event/muxer_listener.h index 30eb7400bf..e609acf0af 100644 --- a/packager/media/event/muxer_listener.h +++ b/packager/media/event/muxer_listener.h @@ -36,7 +36,8 @@ class MuxerListener { kContainerMp4, kContainerMpeg2ts, kContainerWebM, - kContainerText + kContainerText, + kContainerPackedAudio, }; /// Structure for specifying ranges within a media file. This is mainly for diff --git a/packager/media/formats/packed_audio/packed_audio.gyp b/packager/media/formats/packed_audio/packed_audio.gyp index a5e5f5366a..aa430dc280 100644 --- a/packager/media/formats/packed_audio/packed_audio.gyp +++ b/packager/media/formats/packed_audio/packed_audio.gyp @@ -15,6 +15,8 @@ 'sources': [ 'packed_audio_segmenter.cc', 'packed_audio_segmenter.h', + 'packed_audio_writer.cc', + 'packed_audio_writer.h', ], 'dependencies': [ '../../base/media_base.gyp:media_base', @@ -26,11 +28,14 @@ 'type': '<(gtest_target_type)', 'sources': [ 'packed_audio_segmenter_unittest.cc', + 'packed_audio_writer_unittest.cc', ], 'dependencies': [ '../../../testing/gtest.gyp:gtest', '../../../testing/gmock.gyp:gmock', + '../../base/media_base.gyp:media_handler_test_base', '../../codecs/codecs.gyp:codecs', + '../../event/media_event.gyp:mock_muxer_listener', '../../test/media_test.gyp:media_test_support', 'packed_audio', ], diff --git a/packager/media/formats/packed_audio/packed_audio_writer.cc b/packager/media/formats/packed_audio/packed_audio_writer.cc new file mode 100644 index 0000000000..dc8e703abe --- /dev/null +++ b/packager/media/formats/packed_audio/packed_audio_writer.cc @@ -0,0 +1,132 @@ +// Copyright 2018 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "packager/media/formats/packed_audio/packed_audio_writer.h" + +#include "packager/media/base/muxer_util.h" +#include "packager/media/formats/packed_audio/packed_audio_segmenter.h" +#include "packager/status_macros.h" + +namespace shaka { +namespace media { + +PackedAudioWriter::PackedAudioWriter(const MuxerOptions& muxer_options) + : Muxer(muxer_options), segmenter_(new PackedAudioSegmenter) {} + +PackedAudioWriter::~PackedAudioWriter() = default; + +Status PackedAudioWriter::InitializeMuxer() { + if (streams().size() > 1u) + return Status(error::MUXER_FAILURE, "Cannot handle more than one streams."); + + RETURN_IF_ERROR(segmenter_->Initialize(*streams()[0])); + + 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); + } + } + + if (muxer_listener()) { + muxer_listener()->OnMediaStart(options(), *streams().front(), + kPackedAudioTimescale, + MuxerListener::kContainerPackedAudio); + } + return Status::OK; +} + +Status PackedAudioWriter::Finalize() { + if (output_file_) + RETURN_IF_ERROR(CloseFile(std::move(output_file_))); + + if (muxer_listener()) { + muxer_listener()->OnMediaEnd( + media_ranges_, total_duration_ * segmenter_->TimescaleScale()); + } + return Status::OK; +} + +Status PackedAudioWriter::AddSample(size_t stream_id, + const MediaSample& sample) { + DCHECK_EQ(stream_id, 0u); + return segmenter_->AddSample(sample); +} + +Status PackedAudioWriter::FinalizeSegment(size_t stream_id, + const SegmentInfo& segment_info) { + DCHECK_EQ(stream_id, 0u); + // PackedAudio does not support subsegment. + if (segment_info.is_subsegment) + return Status::OK; + + RETURN_IF_ERROR(segmenter_->FinalizeSegment()); + + const uint64_t segment_timestamp = + segment_info.start_timestamp * segmenter_->TimescaleScale(); + std::string segment_path = + options().segment_template.empty() + ? options().output_file_name + : GetSegmentName(options().segment_template, segment_timestamp, + segment_number_++, options().bandwidth); + + // Save |segment_size| as it will be cleared after writing. + const size_t segment_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_timestamp, + segment_info.duration * segmenter_->TimescaleScale(), segment_size); + } + return Status::OK; +} + +Status PackedAudioWriter::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 PackedAudioWriter::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; +} + +} // namespace media +} // namespace shaka diff --git a/packager/media/formats/packed_audio/packed_audio_writer.h b/packager/media/formats/packed_audio/packed_audio_writer.h new file mode 100644 index 0000000000..36a8b21a3c --- /dev/null +++ b/packager/media/formats/packed_audio/packed_audio_writer.h @@ -0,0 +1,61 @@ +// Copyright 2018 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#ifndef PACKAGER_MEDIA_FORMATS_PACKED_AUDIO_PACKED_AUDIO_WRITER_H_ +#define PACKAGER_MEDIA_FORMATS_PACKED_AUDIO_PACKED_AUDIO_WRITER_H_ + +#include "packager/file/file_closer.h" +#include "packager/media/base/muxer.h" + +namespace shaka { +namespace media { + +class BufferWriter; +class PackedAudioSegmenter; + +/// Implements packed audio writer. +/// https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-3.4 +/// A Packed Audio Segment contains encoded audio samples and ID3 tags that are +/// simply packed together with minimal framing and no per-sample timestamps. +class PackedAudioWriter : public Muxer { + public: + /// Create a MP4Muxer object from MuxerOptions. + explicit PackedAudioWriter(const MuxerOptions& muxer_options); + ~PackedAudioWriter() override; + + private: + friend class PackedAudioWriterTest; + + PackedAudioWriter(const PackedAudioWriter&) = delete; + PackedAudioWriter& operator=(const PackedAudioWriter&) = delete; + + // Muxer implementations. + Status InitializeMuxer() override; + Status Finalize() override; + Status AddSample(size_t stream_id, const MediaSample& sample) override; + 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); + + std::unique_ptr segmenter_; + + // 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; + + // Used in multi-segment mode for segment template. + uint64_t segment_number_ = 0; +}; + +} // namespace media +} // namespace shaka + +#endif // PACKAGER_MEDIA_FORMATS_PACKED_AUDIO_PACKED_AUDIO_WRITER_H_ diff --git a/packager/media/formats/packed_audio/packed_audio_writer_unittest.cc b/packager/media/formats/packed_audio/packed_audio_writer_unittest.cc new file mode 100644 index 0000000000..9ffe6456d2 --- /dev/null +++ b/packager/media/formats/packed_audio/packed_audio_writer_unittest.cc @@ -0,0 +1,260 @@ +// Copyright 2018 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "packager/media/formats/packed_audio/packed_audio_writer.h" + +#include +#include + +#include "packager/file/file_test_util.h" +#include "packager/media/base/media_handler_test_base.h" +#include "packager/media/event/mock_muxer_listener.h" +#include "packager/media/formats/packed_audio/packed_audio_segmenter.h" +#include "packager/status_test_util.h" + +using ::testing::_; +using ::testing::AllOf; +using ::testing::Bool; +using ::testing::ElementsAre; +using ::testing::Eq; +using ::testing::Field; +using ::testing::Invoke; +using ::testing::Ref; +using ::testing::Return; +using ::testing::WithParamInterface; + +namespace shaka { +namespace media { +namespace { + +const size_t kInputs = 1; +const size_t kOutputs = 0; +const size_t kInput = 0; +const size_t kStreamIndex = 0; + +const uint32_t kTimescale = 9000; + +// For single-segment mode. +const char kOutputFile[] = "memory://test.aac"; +// For multi-segment mode. +const char kSegmentTemplate[] = "memory://test_$Number$.aac"; +const char kSegment1Name[] = "memory://test_1.aac"; +const char kSegment2Name[] = "memory://test_2.aac"; + +class MockPackedAudioSegmenter : public PackedAudioSegmenter { + public: + MOCK_METHOD1(Initialize, Status(const StreamInfo& stream_info)); + MOCK_METHOD1(AddSample, Status(const MediaSample& sample)); + MOCK_METHOD0(FinalizeSegment, Status()); + MOCK_CONST_METHOD0(TimescaleScale, double()); +}; + +} // namespace + +class PackedAudioWriterTest : public MediaHandlerTestBase, + public WithParamInterface { + protected: + void SetUp() override { + MediaHandlerTestBase::SetUp(); + + is_single_segment_mode_ = GetParam(); + + if (is_single_segment_mode_) + muxer_options_.output_file_name = kOutputFile; + else + muxer_options_.segment_template = kSegmentTemplate; + + auto packed_audio_writer = + std::make_shared(muxer_options_); + + std::unique_ptr mock_segmenter( + new MockPackedAudioSegmenter); + mock_segmenter_ptr_ = mock_segmenter.get(); + packed_audio_writer->segmenter_ = std::move(mock_segmenter); + + std::unique_ptr mock_muxer_listener( + new MockMuxerListener); + mock_muxer_listener_ptr_ = mock_muxer_listener.get(); + packed_audio_writer->SetMuxerListener(std::move(mock_muxer_listener)); + + ASSERT_OK(SetUpAndInitializeGraph(packed_audio_writer, kInputs, kOutputs)); + } + + MuxerOptions muxer_options_; + MockPackedAudioSegmenter* mock_segmenter_ptr_; + MockMuxerListener* mock_muxer_listener_ptr_; + bool is_single_segment_mode_; +}; + +TEST_P(PackedAudioWriterTest, InitializeWithStreamInfo) { + auto stream_info_data = + StreamData::FromStreamInfo(kStreamIndex, GetAudioStreamInfo(kTimescale)); + EXPECT_CALL(*mock_muxer_listener_ptr_, + OnMediaStart(_, Ref(*stream_info_data->stream_info), + kPackedAudioTimescale, + MuxerListener::kContainerPackedAudio)); + EXPECT_CALL(*mock_segmenter_ptr_, + Initialize(Ref(*stream_info_data->stream_info))); + ASSERT_OK(Input(kInput)->Dispatch(std::move(stream_info_data))); +} + +TEST_P(PackedAudioWriterTest, Sample) { + const int64_t kTimestamp = 12345; + const int64_t kDuration = 100; + const bool kKeyFrame = true; + auto sample_stream_data = StreamData::FromMediaSample( + kStreamIndex, GetMediaSample(kTimestamp, kDuration, kKeyFrame)); + + EXPECT_CALL(*mock_segmenter_ptr_, + AddSample(Ref(*sample_stream_data->media_sample))); + ASSERT_OK(Input(kInput)->Dispatch(std::move(sample_stream_data))); +} + +TEST_P(PackedAudioWriterTest, SubsegmentIgnored) { + const int64_t kTimestamp = 12345; + const int64_t kDuration = 100; + const bool kSubsegment = true; + auto subsegment_stream_data = StreamData::FromSegmentInfo( + kStreamIndex, GetSegmentInfo(kTimestamp, kDuration, kSubsegment)); + + EXPECT_CALL(*mock_muxer_listener_ptr_, OnNewSegment(_, _, _, _)).Times(0); + EXPECT_CALL(*mock_segmenter_ptr_, FinalizeSegment()).Times(0); + ASSERT_OK(Input(kInput)->Dispatch(std::move(subsegment_stream_data))); +} + +TEST_P(PackedAudioWriterTest, OneSegment) { + ASSERT_OK(Input(kInput)->Dispatch(StreamData::FromStreamInfo( + kStreamIndex, GetAudioStreamInfo(kTimescale)))); + + const int64_t kTimestamp = 12345; + const int64_t kDuration = 100; + const bool kSubsegment = true; + auto segment_stream_data = StreamData::FromSegmentInfo( + kStreamIndex, GetSegmentInfo(kTimestamp, kDuration, !kSubsegment)); + + const double kMockTimescaleScale = 10; + const char kMockSegmentData[] = "hello segment 1"; + const size_t kSegmentDataSize = sizeof(kMockSegmentData) - 1; + + EXPECT_CALL( + *mock_muxer_listener_ptr_, + OnNewSegment(is_single_segment_mode_ ? kOutputFile : kSegment1Name, + kTimestamp * kMockTimescaleScale, + kDuration * kMockTimescaleScale, kSegmentDataSize)); + + EXPECT_CALL(*mock_segmenter_ptr_, TimescaleScale()) + .WillRepeatedly(Return(kMockTimescaleScale)); + EXPECT_CALL(*mock_segmenter_ptr_, FinalizeSegment()) + .WillOnce(Invoke([this, &kMockSegmentData]() { + this->mock_segmenter_ptr_->segment_buffer()->AppendString( + kMockSegmentData); + return Status::OK; + })); + ASSERT_OK(Input(kInput)->Dispatch(std::move(segment_stream_data))); + + const bool kHasInitRange = true; + const bool kHasIndexRange = true; + const bool kHasSegmentRange = true; + if (is_single_segment_mode_) { + EXPECT_CALL( + *mock_muxer_listener_ptr_, + OnMediaEndMock( + !kHasInitRange, 0, 0, !kHasIndexRange, 0, 0, kHasSegmentRange, + ElementsAre(AllOf(Field(&Range::start, Eq(0u)), + Field(&Range::end, Eq(kSegmentDataSize - 1)))), + kDuration * kMockTimescaleScale)); + } else { + EXPECT_CALL(*mock_muxer_listener_ptr_, + OnMediaEndMock(!kHasInitRange, 0, 0, !kHasIndexRange, 0, 0, + !kHasSegmentRange, ElementsAre(), + kDuration * kMockTimescaleScale)); + } + ASSERT_OK(Input(kInput)->FlushDownstream(kStreamIndex)); + + ASSERT_FILE_STREQ(is_single_segment_mode_ ? kOutputFile : kSegment1Name, + kMockSegmentData); +} + +TEST_P(PackedAudioWriterTest, TwoSegments) { + ASSERT_OK(Input(kInput)->Dispatch(StreamData::FromStreamInfo( + kStreamIndex, GetAudioStreamInfo(kTimescale)))); + + const int64_t kTimestamp = 12345; + const int64_t kDuration = 100; + const bool kSubsegment = true; + auto segment1_stream_data = StreamData::FromSegmentInfo( + kStreamIndex, GetSegmentInfo(kTimestamp, kDuration, !kSubsegment)); + auto segment2_stream_data = StreamData::FromSegmentInfo( + kStreamIndex, + GetSegmentInfo(kTimestamp + kDuration, kDuration, !kSubsegment)); + + const double kMockTimescaleScale = 10; + const char kMockSegment1Data[] = "hello segment 1"; + const char kMockSegment2Data[] = "hello segment 2"; + const size_t kSegment1DataSize = sizeof(kMockSegment1Data) - 1; + const size_t kSegment2DataSize = sizeof(kMockSegment2Data) - 1; + + EXPECT_CALL( + *mock_muxer_listener_ptr_, + OnNewSegment(is_single_segment_mode_ ? kOutputFile : kSegment1Name, + kTimestamp * kMockTimescaleScale, + kDuration * kMockTimescaleScale, + sizeof(kMockSegment1Data) - 1)); + EXPECT_CALL( + *mock_muxer_listener_ptr_, + OnNewSegment(is_single_segment_mode_ ? kOutputFile : kSegment2Name, + (kTimestamp + kDuration) * kMockTimescaleScale, + kDuration * kMockTimescaleScale, kSegment2DataSize)); + + EXPECT_CALL(*mock_segmenter_ptr_, TimescaleScale()) + .WillRepeatedly(Return(kMockTimescaleScale)); + EXPECT_CALL(*mock_segmenter_ptr_, FinalizeSegment()) + .WillOnce(Invoke([this, &kMockSegment1Data]() { + this->mock_segmenter_ptr_->segment_buffer()->AppendString( + kMockSegment1Data); + return Status::OK; + })) + .WillOnce(Invoke([this, &kMockSegment2Data]() { + this->mock_segmenter_ptr_->segment_buffer()->AppendString( + kMockSegment2Data); + return Status::OK; + })); + ASSERT_OK(Input(kInput)->Dispatch(std::move(segment1_stream_data))); + ASSERT_OK(Input(kInput)->Dispatch(std::move(segment2_stream_data))); + + if (is_single_segment_mode_) { + EXPECT_CALL( + *mock_muxer_listener_ptr_, + OnMediaEndMock( + _, _, _, _, _, _, _, + ElementsAre(AllOf(Field(&Range::start, Eq(0u)), + Field(&Range::end, Eq(kSegment1DataSize - 1))), + AllOf(Field(&Range::start, Eq(kSegment1DataSize)), + Field(&Range::end, Eq(kSegment1DataSize + + kSegment2DataSize - 1)))), + kDuration * 2 * kMockTimescaleScale)); + } else { + EXPECT_CALL(*mock_muxer_listener_ptr_, + OnMediaEndMock(_, _, _, _, _, _, _, _, + kDuration * 2 * kMockTimescaleScale)); + } + ASSERT_OK(Input(kInput)->FlushDownstream(kStreamIndex)); + + if (is_single_segment_mode_) { + ASSERT_FILE_STREQ(kOutputFile, std::string(kMockSegment1Data) + + std::string(kMockSegment2Data)); + } else { + ASSERT_FILE_STREQ(kSegment1Name, kMockSegment1Data); + ASSERT_FILE_STREQ(kSegment2Name, kMockSegment2Data); + } +} + +INSTANTIATE_TEST_CASE_P(SingleSegmentOrMultiSegment, + PackedAudioWriterTest, + Bool()); + +} // namespace media +} // namespace shaka