diff options
author | wez@chromium.org <wez@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-06-25 20:03:34 +0000 |
---|---|---|
committer | wez@chromium.org <wez@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-06-25 20:03:34 +0000 |
commit | 6d30912e73f8f144777d5a01e87cd273dbdfbb38 (patch) | |
tree | b301e2ff524bdf564cf1c669c6b5e33a01ea8085 /remoting | |
parent | c76305371e9f64614031f4d6160be61ba7dbe5aa (diff) | |
download | chromium_src-6d30912e73f8f144777d5a01e87cd273dbdfbb38.zip chromium_src-6d30912e73f8f144777d5a01e87cd273dbdfbb38.tar.gz chromium_src-6d30912e73f8f144777d5a01e87cd273dbdfbb38.tar.bz2 |
Add VideoFrameRecorder for use recording test frame sequences.
This will be used to record representative frame sequences for performance evaluation of the new VP9 encoder.
BUG=260879
Review URL: https://codereview.chromium.org/339073002
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@279795 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'remoting')
-rw-r--r-- | remoting/host/video_frame_recorder.cc | 207 | ||||
-rw-r--r-- | remoting/host/video_frame_recorder.h | 100 | ||||
-rw-r--r-- | remoting/host/video_frame_recorder_unittest.cc | 279 | ||||
-rw-r--r-- | remoting/remoting_host.gypi | 2 | ||||
-rw-r--r-- | remoting/remoting_test.gypi | 1 |
5 files changed, 589 insertions, 0 deletions
diff --git a/remoting/host/video_frame_recorder.cc b/remoting/host/video_frame_recorder.cc new file mode 100644 index 0000000..cccfe7c --- /dev/null +++ b/remoting/host/video_frame_recorder.cc @@ -0,0 +1,207 @@ +// Copyright 2014 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 "remoting/host/video_frame_recorder.h" + +#include "base/bind.h" +#include "base/location.h" +#include "base/single_thread_task_runner.h" +#include "base/stl_util.h" +#include "base/thread_task_runner_handle.h" +#include "remoting/codec/video_encoder.h" +#include "remoting/proto/video.pb.h" +#include "third_party/webrtc/modules/desktop_capture/desktop_frame.h" +#include "third_party/webrtc/modules/desktop_capture/desktop_geometry.h" +#include "third_party/webrtc/modules/desktop_capture/desktop_region.h" + +namespace remoting { + +static int64_t FrameContentSize(const webrtc::DesktopFrame* frame) { + DCHECK_GT(frame->stride(), 0); + return frame->stride() * frame->size().height(); +} + +// VideoEncoder wrapper used to intercept frames passed to a real VideoEncoder. +class VideoFrameRecorder::RecordingVideoEncoder : public VideoEncoder { + public: + RecordingVideoEncoder(scoped_ptr<VideoEncoder> encoder, + scoped_refptr<base::TaskRunner> recorder_task_runner, + base::WeakPtr<VideoFrameRecorder> recorder) + : encoder_(encoder.Pass()), + recorder_task_runner_(recorder_task_runner), + recorder_(recorder), + enable_recording_(false), + weak_factory_(this) { + DCHECK(encoder_); + DCHECK(recorder_task_runner_); + } + + base::WeakPtr<RecordingVideoEncoder> AsWeakPtr() { + return weak_factory_.GetWeakPtr(); + } + + void SetEnableRecording(bool enable_recording) { + DCHECK(!encoder_task_runner_ || + encoder_task_runner_->BelongsToCurrentThread()); + enable_recording_ = enable_recording; + } + + // remoting::VideoEncoder interface. + virtual void SetLosslessEncode(bool want_lossless) OVERRIDE { + encoder_->SetLosslessEncode(want_lossless); + } + virtual void SetLosslessColor(bool want_lossless) OVERRIDE { + encoder_->SetLosslessColor(want_lossless); + } + virtual scoped_ptr<VideoPacket> Encode( + const webrtc::DesktopFrame& frame) OVERRIDE { + // If this is the first Encode() then store the TaskRunner and inform the + // VideoFrameRecorder so it can post SetEnableRecording() on it. + if (!encoder_task_runner_) { + encoder_task_runner_ = base::ThreadTaskRunnerHandle::Get(); + recorder_task_runner_->PostTask(FROM_HERE, + base::Bind(&VideoFrameRecorder::SetEncoderTaskRunner, + recorder_, + encoder_task_runner_)); + } + + DCHECK(encoder_task_runner_->BelongsToCurrentThread()); + + if (enable_recording_) { + // Copy the frame and post it to the VideoFrameRecorder to store. + scoped_ptr<webrtc::DesktopFrame> frame_copy( + new webrtc::BasicDesktopFrame(frame.size())); + *frame_copy->mutable_updated_region() = frame.updated_region(); + frame_copy->set_dpi(frame.dpi()); + frame_copy->CopyPixelsFrom(frame.data(), + frame.stride(), + webrtc::DesktopRect::MakeSize(frame.size())); + recorder_task_runner_->PostTask(FROM_HERE, + base::Bind(&VideoFrameRecorder::RecordFrame, + recorder_, + base::Passed(&frame_copy))); + } + + return encoder_->Encode(frame); + } + + private: + scoped_ptr<VideoEncoder> encoder_; + scoped_refptr<base::TaskRunner> recorder_task_runner_; + base::WeakPtr<VideoFrameRecorder> recorder_; + + bool enable_recording_; + scoped_refptr<base::SingleThreadTaskRunner> encoder_task_runner_; + + base::WeakPtrFactory<RecordingVideoEncoder> weak_factory_; + + DISALLOW_COPY_AND_ASSIGN(RecordingVideoEncoder); +}; + +VideoFrameRecorder::VideoFrameRecorder() + : content_bytes_(0), + max_content_bytes_(0), + enable_recording_(false), + weak_factory_(this) { +} + +VideoFrameRecorder::~VideoFrameRecorder() { + SetEnableRecording(false); + STLDeleteElements(&recorded_frames_); +} + +scoped_ptr<VideoEncoder> VideoFrameRecorder::WrapVideoEncoder( + scoped_ptr<VideoEncoder> encoder) { + DCHECK(!caller_task_runner_); + caller_task_runner_ = base::ThreadTaskRunnerHandle::Get(); + + scoped_ptr<RecordingVideoEncoder> recording_encoder( + new RecordingVideoEncoder(encoder.Pass(), + caller_task_runner_, + weak_factory_.GetWeakPtr())); + recording_encoder_ = recording_encoder->AsWeakPtr(); + + return recording_encoder.PassAs<VideoEncoder>(); +} + +void VideoFrameRecorder::SetEnableRecording(bool enable_recording) { + DCHECK(!caller_task_runner_ || caller_task_runner_->BelongsToCurrentThread()); + + if (enable_recording_ == enable_recording) { + return; + } + enable_recording_ = enable_recording; + + if (encoder_task_runner_) { + encoder_task_runner_->PostTask(FROM_HERE, + base::Bind(&RecordingVideoEncoder::SetEnableRecording, + recording_encoder_, + enable_recording_)); + } +} + +void VideoFrameRecorder::SetMaxContentBytes(int64_t max_content_bytes) { + DCHECK(!caller_task_runner_ || caller_task_runner_->BelongsToCurrentThread()); + DCHECK_GE(max_content_bytes, 0); + + max_content_bytes_ = max_content_bytes; +} + +scoped_ptr<webrtc::DesktopFrame> VideoFrameRecorder::NextFrame() { + DCHECK(caller_task_runner_->BelongsToCurrentThread()); + + scoped_ptr<webrtc::DesktopFrame> frame; + if (!recorded_frames_.empty()) { + frame.reset(recorded_frames_.front()); + recorded_frames_.pop_front(); + content_bytes_ -= FrameContentSize(frame.get()); + DCHECK_GE(content_bytes_, 0); + } + + return frame.Pass(); +} + +void VideoFrameRecorder::SetEncoderTaskRunner( + scoped_refptr<base::TaskRunner> task_runner) { + DCHECK(caller_task_runner_->BelongsToCurrentThread()); + DCHECK(!encoder_task_runner_); + DCHECK(task_runner); + + encoder_task_runner_ = task_runner; + + // If the caller already enabled recording, inform the recording encoder. + if (enable_recording_ && encoder_task_runner_) { + encoder_task_runner_->PostTask(FROM_HERE, + base::Bind(&RecordingVideoEncoder::SetEnableRecording, + recording_encoder_, + enable_recording_)); + } +} + +void VideoFrameRecorder::RecordFrame(scoped_ptr<webrtc::DesktopFrame> frame) { + DCHECK(caller_task_runner_->BelongsToCurrentThread()); + + int64_t frame_bytes = FrameContentSize(frame.get()); + DCHECK_GE(frame_bytes, 0); + + // Purge existing frames until there is space for the new one. + while (content_bytes_ + frame_bytes > max_content_bytes_ && + !recorded_frames_.empty()) { + scoped_ptr<webrtc::DesktopFrame> drop_frame(recorded_frames_.front()); + recorded_frames_.pop_front(); + content_bytes_ -= FrameContentSize(drop_frame.get()); + DCHECK_GE(content_bytes_, 0); + } + + // If the frame is still too big, ignore it. + if (content_bytes_ + frame_bytes > max_content_bytes_) { + return; + } + + // Store the frame and update the content byte count. + recorded_frames_.push_back(frame.release()); + content_bytes_ += frame_bytes; +} + +} // namespace remoting diff --git a/remoting/host/video_frame_recorder.h b/remoting/host/video_frame_recorder.h new file mode 100644 index 0000000..c204c9a --- /dev/null +++ b/remoting/host/video_frame_recorder.h @@ -0,0 +1,100 @@ +// Copyright 2014 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 REMOTING_HOST_VIDEO_FRAME_RECORDER_H_ +#define REMOTING_HOST_VIDEO_FRAME_RECORDER_H_ + +#include <stdint.h> +#include <list> + +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/single_thread_task_runner.h" + +namespace webrtc { +class DesktopFrame; +} + +namespace remoting { + +class VideoEncoder; + +// Allows sequences of DesktopFrames passed to a VideoEncoder to be recorded. +// +// VideoFrameRecorder is design to support applications which use a dedicated +// thread for video encoding, but need to manage that process from a "main" +// or "control" thread. +// +// On the control thread: +// 1. Create the VideoFrameRecorder on the controlling thread. +// 2. Specify the amount of memory that may be used for recording. +// 3. Call WrapVideoEncoder(), passing the actual VideoEncoder that will be +// used to encode frames. +// 4. Hand the returned wrapper VideoEncoder of to the video encoding thread, +// to call in place of the actual VideoEncoder. +// 5. Start/stop frame recording as necessary. +// 6. Use NextFrame() to read each recorded frame in sequence. +// +// The wrapper VideoEncoder is designed to be handed off to the video encoding +// thread, and used and torn down there. +// +// The VideoFrameRecorder and VideoEncoder may be torn down in any order; frame +// recording will stop as soon as either is destroyed. + +class VideoFrameRecorder { + public: + VideoFrameRecorder(); + virtual ~VideoFrameRecorder(); + + // Wraps the supplied VideoEncoder, returning a replacement VideoEncoder that + // will route frames to the recorder, as well as passing them for encoding. + // This may be called at most once on each VideoFrameRecorder instance. + scoped_ptr<VideoEncoder> WrapVideoEncoder(scoped_ptr<VideoEncoder> encoder); + + // Enables/disables frame recording. Frame recording is initially disabled. + void SetEnableRecording(bool enable_recording); + + // Sets the maximum number of bytes of pixel data that may be recorded. + // When this maximum is reached older frames will be discard to make space + // for new ones. + void SetMaxContentBytes(int64_t max_content_bytes); + + // Pops the next recorded frame in the sequence, and returns it. + scoped_ptr<webrtc::DesktopFrame> NextFrame(); + + private: + class RecordingVideoEncoder; + friend class RecordingVideoEncoder; + + void SetEncoderTaskRunner(scoped_refptr<base::TaskRunner> task_runner); + void RecordFrame(scoped_ptr<webrtc::DesktopFrame> frame); + + // The recorded frames, in sequence. + std::list<webrtc::DesktopFrame*> recorded_frames_; + + // Size of the recorded frames' content, in bytes. + int64_t content_bytes_; + + // Size that recorded frames' content must not exceed. + int64_t max_content_bytes_; + + // True if recording is started, false otherwise. + bool enable_recording_; + + // Task runner on which the wrapper VideoEncoder is being run. + scoped_refptr<base::TaskRunner> encoder_task_runner_; + + // Weak reference to the wrapper VideoEncoder, to use to control it. + base::WeakPtr<RecordingVideoEncoder> recording_encoder_; + + scoped_refptr<base::SingleThreadTaskRunner> caller_task_runner_; + base::WeakPtrFactory<VideoFrameRecorder> weak_factory_; + + DISALLOW_COPY_AND_ASSIGN(VideoFrameRecorder); +}; + +} // namespace remoting + +#endif // REMOTING_HOST_VIDEO_FRAME_RECORDER_H_ diff --git a/remoting/host/video_frame_recorder_unittest.cc b/remoting/host/video_frame_recorder_unittest.cc new file mode 100644 index 0000000..907c4a9 --- /dev/null +++ b/remoting/host/video_frame_recorder_unittest.cc @@ -0,0 +1,279 @@ +// Copyright 2014 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 "remoting/host/video_frame_recorder.h" + +#include "base/message_loop/message_loop.h" +#include "base/run_loop.h" +#include "remoting/codec/video_encoder_verbatim.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/webrtc/modules/desktop_capture/desktop_frame.h" +#include "third_party/webrtc/modules/desktop_capture/desktop_geometry.h" +#include "third_party/webrtc/modules/desktop_capture/desktop_region.h" + +namespace webrtc { + +// Define equality operator for DesktopFrame to allow use of EXPECT_EQ(). +static bool operator==(const DesktopFrame& a, + const DesktopFrame& b) { + if ((a.size().equals(b.size())) && + (a.updated_region().Equals(b.updated_region())) && + (a.dpi().equals(b.dpi()))) { + for (int i = 0; i < a.size().height(); ++i) { + if (memcmp(a.data() + a.stride() * i, + b.data() + b.stride() * i, + a.size().width() * DesktopFrame::kBytesPerPixel) != 0) { + return false; + } + } + return true; + } + return false; +} + +} // namespace + +namespace remoting { + +const int64_t kMaxContentBytes = 10 * 1024 * 1024; +const int kWidth = 640; +const int kHeight = 480; +const int kTestFrameCount = 6; + +class VideoFrameRecorderTest : public testing::Test { + public: + VideoFrameRecorderTest(); + + virtual void SetUp() OVERRIDE; + virtual void TearDown() OVERRIDE; + + void CreateAndWrapEncoder(); + scoped_ptr<webrtc::DesktopFrame> CreateNextFrame(); + void CreateTestFrames(); + void EncodeTestFrames(); + void EncodeDummyFrame(); + void StartRecording(); + void VerifyTestFrames(); + + protected: + base::MessageLoop message_loop_; + + scoped_ptr<VideoFrameRecorder> recorder_; + scoped_ptr<VideoEncoder> encoder_; + + std::list<webrtc::DesktopFrame*> test_frames_; + int frame_count_; +}; + +VideoFrameRecorderTest::VideoFrameRecorderTest() : frame_count_(0) {} + +void VideoFrameRecorderTest::SetUp() { + recorder_.reset(new VideoFrameRecorder()); + recorder_->SetMaxContentBytes(kMaxContentBytes); +} + +void VideoFrameRecorderTest::TearDown() { + // Allow events posted to the recorder_, if still valid, to be processed. + base::RunLoop().RunUntilIdle(); + + // Tear down the recorder, if necessary. + recorder_.reset(); + + // Process any events resulting from recorder teardown. + base::RunLoop().RunUntilIdle(); +} + +void VideoFrameRecorderTest::CreateAndWrapEncoder() { + scoped_ptr<VideoEncoder> encoder(new VideoEncoderVerbatim()); + encoder_ = recorder_->WrapVideoEncoder(encoder.Pass()); + + // Encode a dummy frame to bind the wrapper to the TaskRunner. + EncodeDummyFrame(); +} + +scoped_ptr<webrtc::DesktopFrame> VideoFrameRecorderTest::CreateNextFrame() { + scoped_ptr<webrtc::DesktopFrame> frame( + new webrtc::BasicDesktopFrame(webrtc::DesktopSize(kWidth, kHeight))); + + // Fill content, DPI and updated-region based on |frame_count_| so that each + // generated frame is different. + memset(frame->data(), frame_count_, frame->stride() * kHeight); + frame->set_dpi(webrtc::DesktopVector(frame_count_, frame_count_)); + frame->mutable_updated_region()->SetRect( + webrtc::DesktopRect::MakeWH(frame_count_, frame_count_)); + ++frame_count_; + + return frame.Pass(); +} + +void VideoFrameRecorderTest::CreateTestFrames() { + for (int i=0; i < kTestFrameCount; ++i) { + test_frames_.push_back(CreateNextFrame().release()); + } +} + +void VideoFrameRecorderTest::EncodeTestFrames() { + std::list<webrtc::DesktopFrame*>::iterator i; + for (i = test_frames_.begin(); i != test_frames_.end(); ++i) { + scoped_ptr<VideoPacket> packet = encoder_->Encode(*(*i)); + + // Process tasks to let the recorder pick up the frame. + base::RunLoop().RunUntilIdle(); + } +} + +void VideoFrameRecorderTest::EncodeDummyFrame() { + webrtc::BasicDesktopFrame dummy_frame(webrtc::DesktopSize(kWidth, kHeight)); + scoped_ptr<VideoPacket> packet = encoder_->Encode(dummy_frame); + base::RunLoop().RunUntilIdle(); +} + +void VideoFrameRecorderTest::StartRecording() { + // Start the recorder and pump events to let things initialize. + recorder_->SetEnableRecording(true); + base::RunLoop().RunUntilIdle(); +} + +void VideoFrameRecorderTest::VerifyTestFrames() { + // Verify that the recorded frames match the ones passed to the encoder. + while (!test_frames_.empty()) { + scoped_ptr<webrtc::DesktopFrame> recorded_frame(recorder_->NextFrame()); + ASSERT_TRUE(recorded_frame); + + scoped_ptr<webrtc::DesktopFrame> expected_frame(test_frames_.front()); + test_frames_.pop_front(); + + EXPECT_EQ(*recorded_frame, *expected_frame); + } + + EXPECT_FALSE(recorder_->NextFrame()); +} + +// Basic test that creating & tearing down VideoFrameRecorder doesn't crash. +TEST_F(VideoFrameRecorderTest, CreateDestroy) { +} + +// Basic test that creating, starting, stopping and destroying a +// VideoFrameRecorder don't end the world. +TEST_F(VideoFrameRecorderTest, StartStop) { + StartRecording(); + recorder_->SetEnableRecording(false); +} + +// Test that tearing down the VideoFrameRecorder while the VideoEncoder +// wrapper exists doesn't crash. +TEST_F(VideoFrameRecorderTest, DestroyVideoFrameRecorderFirst) { + CreateAndWrapEncoder(); + + // Start the recorder, so that the wrapper will push frames to it. + StartRecording(); + + // Tear down the recorder. + recorder_.reset(); + + // Encode a dummy frame via the wrapper to ensure we don't crash. + EncodeDummyFrame(); +} + +// Test that creating & tearing down the wrapper while the +// VideoFrameRecorder still exists doesn't crash. +TEST_F(VideoFrameRecorderTest, DestroyVideoEncoderWrapperFirst) { + CreateAndWrapEncoder(); + + // Start the recorder, so that the wrapper will push frames to it. + StartRecording(); + + // Encode a dummy frame via the wrapper to ensure we don't crash. + EncodeDummyFrame(); + + // Tear down the encoder wrapper. + encoder_.reset(); + + // Test teardown will stop the recorder and process pending events. +} + +// Test that when asked to encode a short sequence of frames, those frames are +// all recorded, in sequence. +TEST_F(VideoFrameRecorderTest, RecordFrames) { + CreateAndWrapEncoder(); + + // Start the recorder, so that the wrapper will push frames to it. + StartRecording(); + + // Create frames, store them and pass them to the encoder. + CreateTestFrames(); + EncodeTestFrames(); + + // Verify that the recorded frames match the ones passed to the encoder. + VerifyTestFrames(); +} + +// Test that when asked to record more frames than the maximum content bytes +// limit allows, the first encoded frames are dropped. +TEST_F(VideoFrameRecorderTest, MaxContentBytesEnforced) { + CreateAndWrapEncoder(); + + // Configure a maximum content size sufficient for five and a half frames. + int64 frame_bytes = kWidth * kHeight * webrtc::DesktopFrame::kBytesPerPixel; + recorder_->SetMaxContentBytes((frame_bytes * 11) / 2); + + // Start the recorder, so that the wrapper will push frames to it. + StartRecording(); + + // Create frames, store them and pass them to the encoder. + CreateTestFrames(); + EncodeTestFrames(); + + // Only five of the supplied frames should have been recorded. + while (test_frames_.size() > 5) { + scoped_ptr<webrtc::DesktopFrame> frame(test_frames_.front()); + test_frames_.pop_front(); + } + + // Verify that the recorded frames match the ones passed to the encoder. + VerifyTestFrames(); +} + +// Test that when asked to record more frames than the maximum content bytes +// limit allows, the first encoded frames are dropped. +TEST_F(VideoFrameRecorderTest, ContentBytesUpdatedByNextFrame) { + CreateAndWrapEncoder(); + + // Configure a maximum content size sufficient for kTestFrameCount frames. + int64 frame_bytes = kWidth * kHeight * webrtc::DesktopFrame::kBytesPerPixel; + recorder_->SetMaxContentBytes(frame_bytes * kTestFrameCount); + + // Start the recorder, so that the wrapper will push frames to it. + StartRecording(); + + // Encode a frame, to record it, and consume it from the recorder. + EncodeDummyFrame(); + scoped_ptr<webrtc::DesktopFrame> frame = recorder_->NextFrame(); + EXPECT_TRUE(frame); + + // Create frames, store them and pass them to the encoder. + CreateTestFrames(); + EncodeTestFrames(); + + // Verify that the recorded frames match the ones passed to the encoder. + VerifyTestFrames(); +} + +// Test that when asked to encode a short sequence of frames, none are recorded +// if recording was not enabled. +TEST_F(VideoFrameRecorderTest, EncodeButDontRecord) { + CreateAndWrapEncoder(); + + // Create frames, store them and pass them to the encoder. + CreateTestFrames(); + EncodeTestFrames(); + + // Clear the list of expected test frames, since none should be recorded. + test_frames_.clear(); + + // Verify that the recorded frames match the ones passed to the encoder. + VerifyTestFrames(); +} + +} // namespace remoting diff --git a/remoting/remoting_host.gypi b/remoting/remoting_host.gypi index 47d60fa..d7d87ec 100644 --- a/remoting/remoting_host.gypi +++ b/remoting/remoting_host.gypi @@ -244,6 +244,8 @@ 'host/usage_stats_consent_win.cc', 'host/username.cc', 'host/username.h', + 'host/video_frame_recorder.cc', + 'host/video_frame_recorder.h', 'host/video_scheduler.cc', 'host/video_scheduler.h', 'host/win/com_imported_mstscax.tlh', diff --git a/remoting/remoting_test.gypi b/remoting/remoting_test.gypi index 7fef1c3..35111b4 100644 --- a/remoting/remoting_test.gypi +++ b/remoting/remoting_test.gypi @@ -114,6 +114,7 @@ 'host/setup/pin_validator_unittest.cc', 'host/shaped_screen_capturer_unittest.cc', 'host/token_validator_factory_impl_unittest.cc', + 'host/video_frame_recorder_unittest.cc', 'host/video_scheduler_unittest.cc', 'host/win/rdp_client_unittest.cc', 'host/win/worker_process_launcher.cc', |