diff options
author | mcasas <mcasas@chromium.org> | 2016-02-18 12:11:36 -0800 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2016-02-18 20:12:55 +0000 |
commit | 7db2a519b8121924ae9b86599e493a0fd68ad4ea (patch) | |
tree | 6f72bca7136e63ab3329c2f64aa788af29b91fd5 /media/muxers | |
parent | 724f2baac94ce55ea4d38f5f7efe067bc86590df (diff) | |
download | chromium_src-7db2a519b8121924ae9b86599e493a0fd68ad4ea.zip chromium_src-7db2a519b8121924ae9b86599e493a0fd68ad4ea.tar.gz chromium_src-7db2a519b8121924ae9b86599e493a0fd68ad4ea.tar.bz2 |
Move media/capture/webm_muxer* to media/muxers
WebmMuxer is used to multiplex incoming encoded video
and audio into a webm live stream. It is only used from
content's MediaRecorderHandler.
This CL moves WebmMuxer to media/muxers where it
belongs, since it has nothing to do with capture/; that is
made even more relevant in a parallel CL
(http://crrev.com/1699553002) that isolates the said
media/capture.
This CL doesn't modify code (except a minor style
reformatting caught by the presubmit). Narrowed some
DEPS too.
BUG=584797
Review URL: https://codereview.chromium.org/1710713002
Cr-Commit-Position: refs/heads/master@{#376243}
Diffstat (limited to 'media/muxers')
-rw-r--r-- | media/muxers/DEPS | 3 | ||||
-rw-r--r-- | media/muxers/OWNERS | 2 | ||||
-rw-r--r-- | media/muxers/webm_muxer.cc | 289 | ||||
-rw-r--r-- | media/muxers/webm_muxer.h | 142 | ||||
-rw-r--r-- | media/muxers/webm_muxer_unittest.cc | 250 |
5 files changed, 686 insertions, 0 deletions
diff --git a/media/muxers/DEPS b/media/muxers/DEPS new file mode 100644 index 0000000..dd27068 --- /dev/null +++ b/media/muxers/DEPS @@ -0,0 +1,3 @@ +include_rules = [ + "+third_party/libwebm", +] diff --git a/media/muxers/OWNERS b/media/muxers/OWNERS new file mode 100644 index 0000000..088dd9c --- /dev/null +++ b/media/muxers/OWNERS @@ -0,0 +1,2 @@ +mcasas@chromium.org +miu@chromium.org
\ No newline at end of file diff --git a/media/muxers/webm_muxer.cc b/media/muxers/webm_muxer.cc new file mode 100644 index 0000000..e00982a --- /dev/null +++ b/media/muxers/webm_muxer.cc @@ -0,0 +1,289 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "media/muxers/webm_muxer.h" + +#include "base/bind.h" +#include "media/audio/audio_parameters.h" +#include "media/base/limits.h" +#include "media/base/video_frame.h" +#include "media/filters/opus_constants.h" +#include "ui/gfx/geometry/size.h" + +namespace media { + +namespace { + +void WriteOpusHeader(const media::AudioParameters& params, uint8_t* header) { + // See https://wiki.xiph.org/OggOpus#ID_Header. + // Set magic signature. + std::string label = "OpusHead"; + memcpy(header + OPUS_EXTRADATA_LABEL_OFFSET, label.c_str(), label.size()); + // Set Opus version. + header[OPUS_EXTRADATA_VERSION_OFFSET] = 1; + // Set channel count. + header[OPUS_EXTRADATA_CHANNELS_OFFSET] = params.channels(); + // Set pre-skip + uint16_t skip = 0; + memcpy(header + OPUS_EXTRADATA_SKIP_SAMPLES_OFFSET, &skip, sizeof(uint16_t)); + // Set original input sample rate in Hz. + uint32_t sample_rate = params.sample_rate(); + memcpy(header + OPUS_EXTRADATA_SAMPLE_RATE_OFFSET, &sample_rate, + sizeof(uint32_t)); + // Set output gain in dB. + uint16_t gain = 0; + memcpy(header + OPUS_EXTRADATA_GAIN_OFFSET, &gain, 2); + + // Set channel mapping. + if (params.channels() > 2) { + // Also possible to have a multistream, not supported for now. + DCHECK_LE(params.channels(), OPUS_MAX_VORBIS_CHANNELS); + header[OPUS_EXTRADATA_CHANNEL_MAPPING_OFFSET] = 1; + // Assuming no coupled streams. This should actually be + // channels() - |coupled_streams|. + header[OPUS_EXTRADATA_NUM_STREAMS_OFFSET] = params.channels(); + header[OPUS_EXTRADATA_NUM_COUPLED_OFFSET] = 0; + // Set the actual stream map. + for (int i = 0; i < params.channels(); ++i) { + header[OPUS_EXTRADATA_STREAM_MAP_OFFSET + i] = + kOpusVorbisChannelMap[params.channels() - 1][i]; + } + } else { + header[OPUS_EXTRADATA_CHANNEL_MAPPING_OFFSET] = 0; + } +} + +static double GetFrameRate(const scoped_refptr<VideoFrame>& video_frame) { + const double kZeroFrameRate = 0.0; + const double kDefaultFrameRate = 30.0; + + double frame_rate = kDefaultFrameRate; + if (!video_frame->metadata()->GetDouble(VideoFrameMetadata::FRAME_RATE, + &frame_rate) || + frame_rate <= kZeroFrameRate || + frame_rate > media::limits::kMaxFramesPerSecond) { + frame_rate = kDefaultFrameRate; + } + return frame_rate; +} + +} // anonymous namespace + +WebmMuxer::WebmMuxer(VideoCodec codec, + bool has_video, + bool has_audio, + const WriteDataCB& write_data_callback) + : use_vp9_(codec == kCodecVP9), + video_track_index_(0), + audio_track_index_(0), + has_video_(has_video), + has_audio_(has_audio), + write_data_callback_(write_data_callback), + position_(0) { + DCHECK(has_video_ || has_audio_); + DCHECK(!write_data_callback_.is_null()); + DCHECK(codec == kCodecVP8 || codec == kCodecVP9) + << " Only Vp8 and VP9 are supported in WebmMuxer"; + + segment_.Init(this); + segment_.set_mode(mkvmuxer::Segment::kLive); + segment_.OutputCues(false); + + mkvmuxer::SegmentInfo* const info = segment_.GetSegmentInfo(); + info->set_writing_app("Chrome"); + info->set_muxing_app("Chrome"); + + // Creation is done on a different thread than main activities. + thread_checker_.DetachFromThread(); +} + +WebmMuxer::~WebmMuxer() { + // No need to segment_.Finalize() since is not Seekable(), i.e. a live + // stream, but is a good practice. + DCHECK(thread_checker_.CalledOnValidThread()); + segment_.Finalize(); +} + +void WebmMuxer::OnEncodedVideo(const scoped_refptr<VideoFrame>& video_frame, + scoped_ptr<std::string> encoded_data, + base::TimeTicks timestamp, + bool is_key_frame) { + DVLOG(1) << __FUNCTION__ << " - " << encoded_data->size() << "B"; + DCHECK(thread_checker_.CalledOnValidThread()); + + if (!video_track_index_) { + // |track_index_|, cannot be zero (!), initialize WebmMuxer in that case. + // http://www.matroska.org/technical/specs/index.html#Tracks + AddVideoTrack(video_frame->visible_rect().size(), + GetFrameRate(video_frame)); + if (first_frame_timestamp_.is_null()) + first_frame_timestamp_ = timestamp; + } + + // TODO(ajose): Support multiple tracks: http://crbug.com/528523 + if (has_audio_ && !audio_track_index_) { + DVLOG(1) << __FUNCTION__ << ": delaying until audio track ready."; + if (is_key_frame) // Upon Key frame reception, empty the encoded queue. + encoded_frames_queue_.clear(); + + encoded_frames_queue_.push_back(make_scoped_ptr(new EncodedVideoFrame( + std::move(encoded_data), timestamp, is_key_frame))); + return; + } + + // Dump all saved encoded video frames if any. + while (!encoded_frames_queue_.empty()) { + AddFrame(std::move(encoded_frames_queue_.front()->data), video_track_index_, + encoded_frames_queue_.front()->timestamp, + encoded_frames_queue_.front()->is_keyframe); + encoded_frames_queue_.pop_front(); + } + + AddFrame(std::move(encoded_data), video_track_index_, timestamp, + is_key_frame); +} + +void WebmMuxer::OnEncodedAudio(const media::AudioParameters& params, + scoped_ptr<std::string> encoded_data, + base::TimeTicks timestamp) { + DVLOG(1) << __FUNCTION__ << " - " << encoded_data->size() << "B"; + DCHECK(thread_checker_.CalledOnValidThread()); + + if (!audio_track_index_) { + AddAudioTrack(params); + if (first_frame_timestamp_.is_null()) + first_frame_timestamp_ = timestamp; + } + + // TODO(ajose): Don't drop audio data: http://crbug.com/547948 + // TODO(ajose): Support multiple tracks: http://crbug.com/528523 + if (has_video_ && !video_track_index_) { + DVLOG(1) << __FUNCTION__ << ": delaying until video track ready."; + return; + } + + // Dump all saved encoded video frames if any. + while (!encoded_frames_queue_.empty()) { + AddFrame(std::move(encoded_frames_queue_.front()->data), video_track_index_, + encoded_frames_queue_.front()->timestamp, + encoded_frames_queue_.front()->is_keyframe); + encoded_frames_queue_.pop_front(); + } + + AddFrame(std::move(encoded_data), audio_track_index_, timestamp, + true /* is_key_frame -- always true for audio */); +} + +void WebmMuxer::AddVideoTrack(const gfx::Size& frame_size, double frame_rate) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK_EQ(0u, video_track_index_) + << "WebmMuxer can only be initialized once."; + + video_track_index_ = + segment_.AddVideoTrack(frame_size.width(), frame_size.height(), 0); + DCHECK_GT(video_track_index_, 0u); + + mkvmuxer::VideoTrack* const video_track = + reinterpret_cast<mkvmuxer::VideoTrack*>( + segment_.GetTrackByNumber(video_track_index_)); + DCHECK(video_track); + video_track->set_codec_id(use_vp9_ ? mkvmuxer::Tracks::kVp9CodecId + : mkvmuxer::Tracks::kVp8CodecId); + DCHECK_EQ(0ull, video_track->crop_right()); + DCHECK_EQ(0ull, video_track->crop_left()); + DCHECK_EQ(0ull, video_track->crop_top()); + DCHECK_EQ(0ull, video_track->crop_bottom()); + + video_track->set_frame_rate(frame_rate); + video_track->set_default_duration(base::Time::kNanosecondsPerSecond / + frame_rate); + // Segment's timestamps should be in milliseconds, DCHECK it. See + // http://www.webmproject.org/docs/container/#muxer-guidelines + DCHECK_EQ(1000000ull, segment_.GetSegmentInfo()->timecode_scale()); +} + +void WebmMuxer::AddAudioTrack(const media::AudioParameters& params) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK_EQ(0u, audio_track_index_) + << "WebmMuxer audio can only be initialised once."; + + audio_track_index_ = + segment_.AddAudioTrack(params.sample_rate(), params.channels(), 0); + DCHECK_GT(audio_track_index_, 0u); + + mkvmuxer::AudioTrack* const audio_track = + reinterpret_cast<mkvmuxer::AudioTrack*>( + segment_.GetTrackByNumber(audio_track_index_)); + DCHECK(audio_track); + audio_track->set_codec_id(mkvmuxer::Tracks::kOpusCodecId); + + DCHECK_EQ(params.sample_rate(), audio_track->sample_rate()); + DCHECK_EQ(params.channels(), static_cast<int>(audio_track->channels())); + + uint8_t opus_header[OPUS_EXTRADATA_SIZE]; + WriteOpusHeader(params, opus_header); + + if (!audio_track->SetCodecPrivate(opus_header, OPUS_EXTRADATA_SIZE)) + LOG(ERROR) << __FUNCTION__ << ": failed to set opus header."; + + // Segment's timestamps should be in milliseconds, DCHECK it. See + // http://www.webmproject.org/docs/container/#muxer-guidelines + DCHECK_EQ(1000000ull, segment_.GetSegmentInfo()->timecode_scale()); +} + +mkvmuxer::int32 WebmMuxer::Write(const void* buf, mkvmuxer::uint32 len) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(buf); + write_data_callback_.Run( + base::StringPiece(reinterpret_cast<const char*>(buf), len)); + position_ += len; + return 0; +} + +mkvmuxer::int64 WebmMuxer::Position() const { + return position_.ValueOrDie(); +} + +mkvmuxer::int32 WebmMuxer::Position(mkvmuxer::int64 position) { + // The stream is not Seekable() so indicate we cannot set the position. + return -1; +} + +bool WebmMuxer::Seekable() const { + return false; +} + +void WebmMuxer::ElementStartNotify(mkvmuxer::uint64 element_id, + mkvmuxer::int64 position) { + // This method gets pinged before items are sent to |write_data_callback_|. + DCHECK_GE(position, position_.ValueOrDefault(0)) + << "Can't go back in a live WebM stream."; +} + +void WebmMuxer::AddFrame(scoped_ptr<std::string> encoded_data, + uint8_t track_index, + base::TimeTicks timestamp, + bool is_key_frame) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!has_video_ || video_track_index_); + DCHECK(!has_audio_ || audio_track_index_); + + most_recent_timestamp_ = + std::max(most_recent_timestamp_, timestamp - first_frame_timestamp_); + + segment_.AddFrame(reinterpret_cast<const uint8_t*>(encoded_data->data()), + encoded_data->size(), track_index, + most_recent_timestamp_.InMicroseconds() * + base::Time::kNanosecondsPerMicrosecond, + is_key_frame); +} + +WebmMuxer::EncodedVideoFrame::EncodedVideoFrame(scoped_ptr<std::string> data, + base::TimeTicks timestamp, + bool is_keyframe) + : data(std::move(data)), timestamp(timestamp), is_keyframe(is_keyframe) {} + +WebmMuxer::EncodedVideoFrame::~EncodedVideoFrame() {} + +} // namespace media diff --git a/media/muxers/webm_muxer.h b/media/muxers/webm_muxer.h new file mode 100644 index 0000000..227ce56 --- /dev/null +++ b/media/muxers/webm_muxer.h @@ -0,0 +1,142 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef MEDIA_MUXERS_WEBM_MUXER_H_ +#define MEDIA_MUXERS_WEBM_MUXER_H_ + +#include <stdint.h> + +#include <deque> + +#include "base/callback.h" +#include "base/macros.h" +#include "base/numerics/safe_math.h" +#include "base/strings/string_piece.h" +#include "base/threading/thread_checker.h" +#include "base/time/time.h" +#include "media/base/media_export.h" +#include "media/base/video_codecs.h" +#include "third_party/libwebm/source/mkvmuxer.hpp" + +namespace gfx { +class Size; +} // namespace gfx + +namespace media { + +class VideoFrame; +class AudioParameters; + +// Adapter class to manage a WebM container [1], a simplified version of a +// Matroska container [2], composed of an EBML header, and a single Segment +// including at least a Track Section and a number of SimpleBlocks each +// containing a single encoded video or audio frame. WebM container has no +// Trailer. +// Clients will push encoded VPx video frames and Opus audio frames one by one +// via OnEncoded{Video|Audio}(). libwebm will eventually ping the WriteDataCB +// passed on contructor with the wrapped encoded data. +// WebmMuxer is designed for use on a single thread. +// [1] http://www.webmproject.org/docs/container/ +// [2] http://www.matroska.org/technical/specs/index.html +class MEDIA_EXPORT WebmMuxer : public NON_EXPORTED_BASE(mkvmuxer::IMkvWriter) { + public: + // Callback to be called when WebmMuxer is ready to write a chunk of data, + // either any file header or a SingleBlock. + using WriteDataCB = base::Callback<void(base::StringPiece)>; + + // |codec| can be VP8 or VP9 and should coincide with whatever is sent in + // OnEncodedVideo(). + WebmMuxer(VideoCodec codec, + bool has_video_, + bool has_audio_, + const WriteDataCB& write_data_callback); + ~WebmMuxer() override; + + // Functions to add video and audio frames with |encoded_data.data()| + // to WebM Segment. + void OnEncodedVideo(const scoped_refptr<VideoFrame>& video_frame, + scoped_ptr<std::string> encoded_data, + base::TimeTicks timestamp, + bool is_key_frame); + void OnEncodedAudio(const media::AudioParameters& params, + scoped_ptr<std::string> encoded_data, + base::TimeTicks timestamp); + + private: + friend class WebmMuxerTest; + + // Methods for creating and adding video and audio tracks, called upon + // receiving the first frame of a given Track. + // AddVideoTrack adds |frame_size| and |frame_rate| to the Segment + // info, although individual frames passed to OnEncodedVideo() can have any + // frame size. + void AddVideoTrack(const gfx::Size& frame_size, double frame_rate); + void AddAudioTrack(const media::AudioParameters& params); + + // IMkvWriter interface. + mkvmuxer::int32 Write(const void* buf, mkvmuxer::uint32 len) override; + mkvmuxer::int64 Position() const override; + mkvmuxer::int32 Position(mkvmuxer::int64 position) override; + bool Seekable() const override; + void ElementStartNotify(mkvmuxer::uint64 element_id, + mkvmuxer::int64 position) override; + + // Helper to simplify saving frames. + void AddFrame(scoped_ptr<std::string> encoded_data, + uint8_t track_index, + base::TimeTicks timestamp, + bool is_key_frame); + + // Used to DCHECK that we are called on the correct thread. + base::ThreadChecker thread_checker_; + + // Video Codec configured: VP9 if true, otherwise VP8 is used by default. + const bool use_vp9_; + + // Caller-side identifiers to interact with |segment_|, initialised upon + // first frame arrival to Add{Video, Audio}Track(). + uint8_t video_track_index_; + uint8_t audio_track_index_; + + // Origin of times for frame timestamps. + base::TimeTicks first_frame_timestamp_; + base::TimeDelta most_recent_timestamp_; + + // TODO(ajose): Change these when support is added for multiple tracks. + // http://crbug.com/528523 + const bool has_video_; + const bool has_audio_; + + // Callback to dump written data as being called by libwebm. + const WriteDataCB write_data_callback_; + + // Rolling counter of the position in bytes of the written goo. + base::CheckedNumeric<mkvmuxer::int64> position_; + + // The MkvMuxer active element. + mkvmuxer::Segment segment_; + + // Hold on to all encoded video frames to dump them with and when audio is + // received, if expected, since WebM headers can only be written once. + struct EncodedVideoFrame { + EncodedVideoFrame(scoped_ptr<std::string> data, + base::TimeTicks timestamp, + bool is_keyframe); + ~EncodedVideoFrame(); + + scoped_ptr<std::string> data; + base::TimeTicks timestamp; + bool is_keyframe; + + private: + DISALLOW_IMPLICIT_CONSTRUCTORS(EncodedVideoFrame); + }; + std::deque<scoped_ptr<EncodedVideoFrame>> encoded_frames_queue_; + + DISALLOW_COPY_AND_ASSIGN(WebmMuxer); +}; + +} // namespace media + +#endif // MEDIA_MUXERS_WEBM_MUXER_H_ diff --git a/media/muxers/webm_muxer_unittest.cc b/media/muxers/webm_muxer_unittest.cc new file mode 100644 index 0000000..03634de --- /dev/null +++ b/media/muxers/webm_muxer_unittest.cc @@ -0,0 +1,250 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <stddef.h> +#include <stdint.h> + +#include "base/bind.h" +#include "base/location.h" +#include "base/macros.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "media/audio/audio_parameters.h" +#include "media/base/channel_layout.h" +#include "media/base/video_frame.h" +#include "media/muxers/webm_muxer.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using ::testing::_; +using ::testing::AllOf; +using ::testing::AnyNumber; +using ::testing::AtLeast; +using ::testing::Eq; +using ::testing::InSequence; +using ::testing::Mock; +using ::testing::Not; +using ::testing::Sequence; +using ::testing::TestWithParam; +using ::testing::ValuesIn; +using ::testing::WithArgs; + +namespace media { + +struct kTestParams { + VideoCodec codec; + size_t num_video_tracks; + size_t num_audio_tracks; +}; + +class WebmMuxerTest : public TestWithParam<kTestParams> { + public: + WebmMuxerTest() + : webm_muxer_( + GetParam().codec, + GetParam().num_video_tracks, + GetParam().num_audio_tracks, + base::Bind(&WebmMuxerTest::WriteCallback, base::Unretained(this))), + last_encoded_length_(0), + accumulated_position_(0) { + EXPECT_EQ(webm_muxer_.Position(), 0); + const mkvmuxer::int64 kRandomNewPosition = 333; + EXPECT_EQ(webm_muxer_.Position(kRandomNewPosition), -1); + EXPECT_FALSE(webm_muxer_.Seekable()); + } + + MOCK_METHOD1(WriteCallback, void(base::StringPiece)); + + void SaveEncodedDataLen(const base::StringPiece& encoded_data) { + last_encoded_length_ = encoded_data.size(); + accumulated_position_ += encoded_data.size(); + } + + mkvmuxer::int64 GetWebmMuxerPosition() const { + return webm_muxer_.Position(); + } + + mkvmuxer::Segment::Mode GetWebmSegmentMode() const { + return webm_muxer_.segment_.mode(); + } + + mkvmuxer::int32 WebmMuxerWrite(const void* buf, mkvmuxer::uint32 len) { + return webm_muxer_.Write(buf, len); + } + + WebmMuxer webm_muxer_; + + size_t last_encoded_length_; + int64_t accumulated_position_; + + private: + DISALLOW_COPY_AND_ASSIGN(WebmMuxerTest); +}; + +// Checks that the WriteCallback is called with appropriate params when +// WebmMuxer::Write() method is called. +TEST_P(WebmMuxerTest, Write) { + const base::StringPiece encoded_data("abcdefghijklmnopqrstuvwxyz"); + + EXPECT_CALL(*this, WriteCallback(encoded_data)); + WebmMuxerWrite(encoded_data.data(), encoded_data.size()); + + EXPECT_EQ(GetWebmMuxerPosition(), static_cast<int64_t>(encoded_data.size())); +} + +// This test sends two frames and checks that the WriteCallback is called with +// appropriate params in both cases. +TEST_P(WebmMuxerTest, OnEncodedVideoTwoFrames) { + if (GetParam().num_audio_tracks > 0) + return; + + const gfx::Size frame_size(160, 80); + const scoped_refptr<VideoFrame> video_frame = + VideoFrame::CreateBlackFrame(frame_size); + const std::string encoded_data("abcdefghijklmnopqrstuvwxyz"); + + EXPECT_CALL(*this, WriteCallback(_)) + .Times(AtLeast(1)) + .WillRepeatedly( + WithArgs<0>(Invoke(this, &WebmMuxerTest::SaveEncodedDataLen))); + webm_muxer_.OnEncodedVideo(video_frame, + make_scoped_ptr(new std::string(encoded_data)), + base::TimeTicks::Now(), false /* keyframe */); + + // First time around WriteCallback() is pinged a number of times to write the + // Matroska header, but at the end it dumps |encoded_data|. + EXPECT_EQ(last_encoded_length_, encoded_data.size()); + EXPECT_EQ(GetWebmMuxerPosition(), accumulated_position_); + EXPECT_GE(GetWebmMuxerPosition(), static_cast<int64_t>(last_encoded_length_)); + EXPECT_EQ(GetWebmSegmentMode(), mkvmuxer::Segment::kLive); + + const int64_t begin_of_second_block = accumulated_position_; + EXPECT_CALL(*this, WriteCallback(_)) + .Times(AtLeast(1)) + .WillRepeatedly( + WithArgs<0>(Invoke(this, &WebmMuxerTest::SaveEncodedDataLen))); + webm_muxer_.OnEncodedVideo(video_frame, + make_scoped_ptr(new std::string(encoded_data)), + base::TimeTicks::Now(), false /* keyframe */); + + // The second time around the callbacks should include a SimpleBlock header, + // namely the track index, a timestamp and a flags byte, for a total of 6B. + EXPECT_EQ(last_encoded_length_, encoded_data.size()); + EXPECT_EQ(GetWebmMuxerPosition(), accumulated_position_); + const uint32_t kSimpleBlockSize = 6u; + EXPECT_EQ(static_cast<int64_t>(begin_of_second_block + kSimpleBlockSize + + encoded_data.size()), + accumulated_position_); +} + +TEST_P(WebmMuxerTest, OnEncodedAudioTwoFrames) { + if (GetParam().num_video_tracks > 0) + return; + + const int sample_rate = 48000; + const int bits_per_sample = 16; + const int frames_per_buffer = 480; + media::AudioParameters audio_params( + media::AudioParameters::Format::AUDIO_PCM_LOW_LATENCY, + media::CHANNEL_LAYOUT_MONO, sample_rate, bits_per_sample, + frames_per_buffer); + + const std::string encoded_data("abcdefghijklmnopqrstuvwxyz"); + + EXPECT_CALL(*this, WriteCallback(_)) + .Times(AtLeast(1)) + .WillRepeatedly( + WithArgs<0>(Invoke(this, &WebmMuxerTest::SaveEncodedDataLen))); + webm_muxer_.OnEncodedAudio(audio_params, + make_scoped_ptr(new std::string(encoded_data)), + base::TimeTicks::Now()); + + // First time around WriteCallback() is pinged a number of times to write the + // Matroska header, but at the end it dumps |encoded_data|. + EXPECT_EQ(last_encoded_length_, encoded_data.size()); + EXPECT_EQ(GetWebmMuxerPosition(), accumulated_position_); + EXPECT_GE(GetWebmMuxerPosition(), static_cast<int64_t>(last_encoded_length_)); + EXPECT_EQ(GetWebmSegmentMode(), mkvmuxer::Segment::kLive); + + const int64_t begin_of_second_block = accumulated_position_; + EXPECT_CALL(*this, WriteCallback(_)) + .Times(AtLeast(1)) + .WillRepeatedly( + WithArgs<0>(Invoke(this, &WebmMuxerTest::SaveEncodedDataLen))); + webm_muxer_.OnEncodedAudio(audio_params, + make_scoped_ptr(new std::string(encoded_data)), + base::TimeTicks::Now()); + + // The second time around the callbacks should include a SimpleBlock header, + // namely the track index, a timestamp and a flags byte, for a total of 6B. + EXPECT_EQ(last_encoded_length_, encoded_data.size()); + EXPECT_EQ(GetWebmMuxerPosition(), accumulated_position_); + const uint32_t kSimpleBlockSize = 6u; + EXPECT_EQ(static_cast<int64_t>(begin_of_second_block + kSimpleBlockSize + + encoded_data.size()), + accumulated_position_); +} + +// This test verifies that when video data comes before audio data, we save the +// encoded video frames and add it to the video track when audio data arrives. +TEST_P(WebmMuxerTest, VideoIsStoredWhileWaitingForAudio) { + // This test is only relevant if we have both kinds of tracks. + if (GetParam().num_video_tracks == 0 || GetParam().num_audio_tracks == 0) + return; + + // First send a video keyframe + const gfx::Size frame_size(160, 80); + const scoped_refptr<VideoFrame> video_frame = + VideoFrame::CreateBlackFrame(frame_size); + const std::string encoded_video("thisisanencodedvideopacket"); + webm_muxer_.OnEncodedVideo(video_frame, + make_scoped_ptr(new std::string(encoded_video)), + base::TimeTicks::Now(), true /* keyframe */); + // A few encoded non key frames. + const int kNumNonKeyFrames = 2; + for (int i = 0; i < kNumNonKeyFrames; ++i) { + webm_muxer_.OnEncodedVideo(video_frame, + make_scoped_ptr(new std::string(encoded_video)), + base::TimeTicks::Now(), false /* keyframe */); + } + + // Send some audio. The header will be written and muxing will proceed + // normally. + const int sample_rate = 48000; + const int bits_per_sample = 16; + const int frames_per_buffer = 480; + media::AudioParameters audio_params( + media::AudioParameters::Format::AUDIO_PCM_LOW_LATENCY, + media::CHANNEL_LAYOUT_MONO, sample_rate, bits_per_sample, + frames_per_buffer); + const std::string encoded_audio("thisisanencodedaudiopacket"); + + // We should first get the encoded video frames, then the encoded audio frame. + Sequence s; + EXPECT_CALL(*this, WriteCallback(Eq(encoded_video))) + .Times(1 + kNumNonKeyFrames) + .InSequence(s); + EXPECT_CALL(*this, WriteCallback(Eq(encoded_audio))).Times(1).InSequence(s); + // We'll also get lots of other header-related stuff. + EXPECT_CALL(*this, WriteCallback( + AllOf(Not(Eq(encoded_video)), Not(Eq(encoded_audio))))) + .Times(AnyNumber()); + webm_muxer_.OnEncodedAudio(audio_params, + make_scoped_ptr(new std::string(encoded_audio)), + base::TimeTicks::Now()); +} + +const kTestParams kTestCases[] = { + // TODO: consider not enumerating every combination by hand. + {kCodecVP8, 1 /* num_video_tracks */, 0 /*num_audio_tracks*/}, + {kCodecVP8, 0, 1}, + {kCodecVP8, 1, 1}, + {kCodecVP9, 1, 0}, + {kCodecVP9, 0, 1}, + {kCodecVP9, 1, 1}, +}; + +INSTANTIATE_TEST_CASE_P(, WebmMuxerTest, ValuesIn(kTestCases)); + +} // namespace media |