diff options
author | mcasas <mcasas@chromium.org> | 2015-07-23 03:20:25 -0700 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2015-07-23 10:20:57 +0000 |
commit | 6f2bce27d46c0fd56d26143acdfdcdb1a7fc463f (patch) | |
tree | 4c76683bb9fe0d5814033db888c42f4a3a073a7d | |
parent | 0bbb0262be0570bb4d9d06444c86ed84609afb1c (diff) | |
download | chromium_src-6f2bce27d46c0fd56d26143acdfdcdb1a7fc463f.zip chromium_src-6f2bce27d46c0fd56d26143acdfdcdb1a7fc463f.tar.gz chromium_src-6f2bce27d46c0fd56d26143acdfdcdb1a7fc463f.tar.bz2 |
media/capture: Adding WebmMuxer class and unittests
WebmMuxer gets encoded video packets and pings a
callback with packetised "blobs" following Live WebM
(simplified Matroska container) format.
See DD @ https://goo.gl/kreaQj for the plan and
https://codereview.chromium.org/1211973012/ for a
hack of all parts together.
BUG=262211
Review URL: https://codereview.chromium.org/1225123006
Cr-Commit-Position: refs/heads/master@{#340066}
-rw-r--r-- | media/BUILD.gn | 8 | ||||
-rw-r--r-- | media/DEPS | 1 | ||||
-rw-r--r-- | media/capture/webm_muxer.cc | 98 | ||||
-rw-r--r-- | media/capture/webm_muxer.h | 87 | ||||
-rw-r--r-- | media/capture/webm_muxer_unittest.cc | 128 | ||||
-rw-r--r-- | media/media.gyp | 25 | ||||
-rw-r--r-- | media/media_options.gni | 6 | ||||
-rw-r--r-- | media/media_variables.gypi | 4 |
8 files changed, 355 insertions, 2 deletions
diff --git a/media/BUILD.gn b/media/BUILD.gn index 8db1443..52935a6 100644 --- a/media/BUILD.gn +++ b/media/BUILD.gn @@ -332,6 +332,14 @@ component("media") { deps += [ "//third_party/libvpx" ] } + if (media_use_libwebm) { + sources += [ + "capture/webm_muxer.cc", + "capture/webm_muxer.h", + ] + deps += [ "//third_party/libwebm" ] + } + if (is_android) { sources += [ "capture/video/android/video_capture_device_android.cc", @@ -8,6 +8,7 @@ include_rules = [ "+jni", "+skia/ext", "+third_party/ffmpeg", + "+third_party/libwebm", "+third_party/libvpx", "+third_party/libyuv", "+third_party/opus", diff --git a/media/capture/webm_muxer.cc b/media/capture/webm_muxer.cc new file mode 100644 index 0000000..8f67041 --- /dev/null +++ b/media/capture/webm_muxer.cc @@ -0,0 +1,98 @@ +// 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/capture/webm_muxer.h" + +#include <limits> + +namespace media { + +WebmMuxer::WebmMuxer(const WriteDataCB& write_data_callback) + : write_data_callback_(write_data_callback), + position_(0) { + DCHECK(thread_checker_.CalledOnValidThread()); + 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"); +} + +WebmMuxer::~WebmMuxer() { + DCHECK(thread_checker_.CalledOnValidThread()); + segment_.Finalize(); +} + +uint64_t WebmMuxer::AddVideoTrack(const gfx::Size& frame_size, + double frame_rate) { + DCHECK(thread_checker_.CalledOnValidThread()); + + const uint64_t track_index = + segment_.AddVideoTrack(frame_size.width(), frame_size.height(), 0); + + mkvmuxer::VideoTrack* const video_track = + reinterpret_cast<mkvmuxer::VideoTrack*>( + segment_.GetTrackByNumber(track_index)); + DCHECK(video_track); + video_track->set_codec_id(mkvmuxer::Tracks::kVp8CodecId); + DCHECK_EQ(video_track->crop_right(), 0ull); + DCHECK_EQ(video_track->crop_left(), 0ull); + DCHECK_EQ(video_track->crop_top(), 0ull); + DCHECK_EQ(video_track->crop_bottom(), 0ull); + + video_track->set_frame_rate(frame_rate); + video_track->set_default_duration(base::Time::kMicrosecondsPerSecond / + frame_rate); + // Segment's timestamps should be in milliseconds, DCHECK it. See + // http://www.webmproject.org/docs/container/#muxer-guidelines + DCHECK_EQ(segment_.GetSegmentInfo()->timecode_scale(), 1000000ull); + + return track_index; +} + +void WebmMuxer::OnEncodedVideo(uint64_t track_number, + const base::StringPiece& encoded_data, + base::TimeDelta timestamp, + bool keyframe) { + DCHECK(thread_checker_.CalledOnValidThread()); + // |track_number|, a caller-side identifier, cannot be zero (!), see + // http://www.matroska.org/technical/specs/index.html#Tracks + DCHECK_GT(track_number, 0u); + DCHECK(segment_.GetTrackByNumber(track_number)); + + segment_.AddFrame(reinterpret_cast<const uint8_t*>(encoded_data.data()), + encoded_data.size(), track_number, timestamp.InMilliseconds(), keyframe); +} + +mkvmuxer::int32 WebmMuxer::Write(const void* buf, mkvmuxer::uint32 len) { + DCHECK(thread_checker_.CalledOnValidThread()); + 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."; +} + +} // namespace media diff --git a/media/capture/webm_muxer.h b/media/capture/webm_muxer.h new file mode 100644 index 0000000..eb93e00 --- /dev/null +++ b/media/capture/webm_muxer.h @@ -0,0 +1,87 @@ +// 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_FILTERS_LIBWEBM_MUXER_H_ +#define MEDIA_FILTERS_LIBWEBM_MUXER_H_ + +#include <set> + +#include "base/callback.h" +#include "base/gtest_prod_util.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 "third_party/libwebm/source/mkvmuxer.hpp" +#include "ui/gfx/geometry/size.h" + +namespace media { + +// 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 frame. WebM container has no Trailer. +// Clients will push encoded VPx video frames one by one via OnEncodedVideo() +// with indication of the Track they belong to, and libwebm will eventually ping +// the WriteDataCB passed on contructor with the wrapped encoded data. All +// operations must happen in a single thread, where WebmMuxer is created and +// destroyed. +// [1] http://www.webmproject.org/docs/container/ +// [2] http://www.matroska.org/technical/specs/index.html +// TODO(mcasas): Add support for Audio muxing. +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. The chunk is described as a byte + // array and a byte length. + using WriteDataCB = base::Callback<void(const base::StringPiece&)>; + + explicit WebmMuxer(const WriteDataCB& write_data_callback); + ~WebmMuxer() override; + + // Creates and adds a new video track. Called before receiving the first + // frame of a given Track, adds |frame_size| and |frame_rate| to the Segment + // info, although individual frames passed to OnEncodedVideo() can have any + // frame size. Returns the track_index to be used for OnEncodedVideo(). + uint64_t AddVideoTrack(const gfx::Size& frame_size, double frame_rate); + + // Adds a frame with |encoded_data.data()| to WebM Segment. |track_number| is + // a caller-side identifier and must have been provided by AddVideoTrack(). + // TODO(mcasas): Revisit how |encoded_data| is passed once the whole recording + // chain is setup (http://crbug.com/262211). + void OnEncodedVideo(uint64_t track_number, + const base::StringPiece& encoded_data, + base::TimeDelta timestamp, + bool keyframe); + + private: + friend class WebmMuxerTest; + + // 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; + + // Used to DCHECK that we are called on the correct thread. + base::ThreadChecker thread_checker_; + + // 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_; + + DISALLOW_COPY_AND_ASSIGN(WebmMuxer); +}; + +} // namespace media + +#endif // MEDIA_FILTERS_LIBWEBM_MUXER_H_ diff --git a/media/capture/webm_muxer_unittest.cc b/media/capture/webm_muxer_unittest.cc new file mode 100644 index 0000000..6ae525c --- /dev/null +++ b/media/capture/webm_muxer_unittest.cc @@ -0,0 +1,128 @@ +// 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 "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/capture/webm_muxer.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using ::testing::_; +using ::testing::AtLeast; +using ::testing::Mock; +using ::testing::WithArgs; + +namespace media { + +// Dummy interface class to be able to MOCK its only function below. +class EventHandlerInterface { + public: + virtual void WriteCallback(const base::StringPiece& encoded_data) = 0; + virtual ~EventHandlerInterface() {} +}; + +class WebmMuxerTest : public testing::Test, public EventHandlerInterface { + public: + WebmMuxerTest() + : webm_muxer_(base::Bind(&WebmMuxerTest::WriteCallback, + base::Unretained(this))), + last_encoded_length_(0), + accumulated_position_(0) { + EXPECT_EQ(webm_muxer_.Position(), 0); + EXPECT_FALSE(webm_muxer_.Seekable()); + EXPECT_EQ(webm_muxer_.segment_.mode(), mkvmuxer::Segment::kLive); + } + + MOCK_METHOD1(WriteCallback, void(const 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(); + } + + const mkvmuxer::Segment& GetWebmMuxerSegment() const { + return webm_muxer_.segment_; + } + + 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 AddVideoTrack adds a Track. +TEST_F(WebmMuxerTest, AddVideoTrack) { + const uint64_t track_number = webm_muxer_.AddVideoTrack(gfx::Size(320, 240), + 30.0f); + EXPECT_TRUE(GetWebmMuxerSegment().GetTrackByNumber(track_number)); +} + +// Checks that the WriteCallback is called with appropriate params when +// WebmMuxer::Write() method is called. +TEST_F(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_F(WebmMuxerTest, OnEncodedVideoNormalFrames) { + const base::StringPiece encoded_data("abcdefghijklmnopqrstuvwxyz"); + const uint64_t track_number = webm_muxer_.AddVideoTrack(gfx::Size(320, 240), + 30.0f); + + EXPECT_CALL(*this, WriteCallback(_)) + .Times(AtLeast(1)) + .WillRepeatedly(WithArgs<0>( + Invoke(this, &WebmMuxerTest::SaveEncodedDataLen))); + webm_muxer_.OnEncodedVideo(track_number, + encoded_data, + base::TimeDelta::FromMicroseconds(0), + 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_)); + + 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(track_number, + encoded_data, + base::TimeDelta::FromMicroseconds(1), + 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_); +} + +} // namespace media diff --git a/media/media.gyp b/media/media.gyp index 8729b5b..e0192fd 100644 --- a/media/media.gyp +++ b/media/media.gyp @@ -477,6 +477,8 @@ 'capture/video/win/video_capture_device_mf_win.h', 'capture/video/win/video_capture_device_win.cc', 'capture/video/win/video_capture_device_win.h', + 'capture/webm_muxer.cc', + 'capture/webm_muxer.h', 'cdm/aes_decryptor.cc', 'cdm/aes_decryptor.h', 'cdm/default_cdm_factory.cc', @@ -687,6 +689,17 @@ 'filters/vpx_video_decoder.h', ], }], + ['media_use_libwebm==1', { + 'dependencies': [ + '<(DEPTH)/third_party/libwebm/libwebm.gyp:libwebm', + ], + }, { # media_use_libwebm==0 + # Exclude the sources that depend on libwebm. + 'sources!': [ + 'capture/webm_muxer.cc', + 'capture/webm_muxer.h', + ], + }], ['enable_browser_cdms==1', { 'sources': [ 'base/browser_cdm.cc', @@ -1248,6 +1261,7 @@ 'filters/video_renderer_algorithm_unittest.cc', 'filters/vp8_bool_decoder_unittest.cc', 'filters/vp8_parser_unittest.cc', + 'capture/webm_muxer_unittest.cc', 'formats/common/offset_byte_queue_unittest.cc', 'formats/webm/cluster_builder.cc', 'formats/webm/cluster_builder.h', @@ -1307,6 +1321,17 @@ 'test/pipeline_integration_test_base.cc', ], }], + ['media_use_libwebm==1', { + 'dependencies': [ + '<(DEPTH)/third_party/libwebm/libwebm.gyp:libwebm', + ], + }, { # media_use_libwebm==0 + # Exclude the sources that depend on libwebm. + 'sources!': [ + 'capture/webm_muxer_unittest.cc', + ], + }], + ['(os_posix==1 and OS!="mac") or (OS=="win" and component!="shared_library" and win_use_allocator_shim==1)', { 'conditions': [ ['use_allocator!="none"', { diff --git a/media/media_options.gni b/media/media_options.gni index cd7440a..a3b3a8a 100644 --- a/media/media_options.gni +++ b/media/media_options.gni @@ -17,10 +17,14 @@ declare_args() { # decoding of VP9 and VP8A type content. media_use_libvpx = true - # Neither Android nor iOS use ffmpeg or libvpx. + # Enable libwebm for multiplexing video and audio for JS recording API. + media_use_libwebm = true + + # Neither Android nor iOS use ffmpeg, libvpx nor libwebm. if (is_android || is_ios) { media_use_ffmpeg = false media_use_libvpx = false + media_use_libwebm = false } # Override to dynamically link the cras (ChromeOS audio) library. diff --git a/media/media_variables.gypi b/media/media_variables.gypi index f359e36..b203ca0 100644 --- a/media/media_variables.gypi +++ b/media/media_variables.gypi @@ -7,14 +7,16 @@ 'variables': { 'conditions': [ ['OS == "android" or OS == "ios"', { - # Android and iOS don't use FFmpeg or libvpx by default. + # Android and iOS don't use FFmpeg, libvpx nor libwebm by default. # Set media_use_ffmpeg=1 for Android builds to compile experimental # support for FFmpeg and the desktop media pipeline. 'media_use_ffmpeg%': 0, 'media_use_libvpx%': 0, + 'media_use_libwebm%': 0, }, { 'media_use_ffmpeg%': 1, 'media_use_libvpx%': 1, + 'media_use_libwebm%': 1, }], ], }, |