diff --git a/content/browser/media/capture/capture_resolution_chooser.cc b/content/browser/media/capture/capture_resolution_chooser.cc new file mode 100644 index 0000000000000..698882ab8d2ab --- /dev/null +++ b/content/browser/media/capture/capture_resolution_chooser.cc @@ -0,0 +1,120 @@ +// 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 "content/browser/media/capture/capture_resolution_chooser.h" + +#include "media/base/limits.h" +#include "media/base/video_util.h" + +namespace content { + +namespace { + +// Compute the minimum frame size from the given |max_frame_size| and +// |resolution_change_policy|. +gfx::Size ComputeMinimumCaptureSize( + const gfx::Size& max_frame_size, + media::ResolutionChangePolicy resolution_change_policy) { + switch (resolution_change_policy) { + case media::RESOLUTION_POLICY_FIXED_RESOLUTION: + return max_frame_size; + case media::RESOLUTION_POLICY_FIXED_ASPECT_RATIO: { + // TODO(miu): This is a place-holder until "min constraints" are plumbed- + // in from the MediaStream framework. http://crbug.com/473336 + const int kMinLines = 180; + if (max_frame_size.height() <= kMinLines) + return max_frame_size; + const gfx::Size result( + kMinLines * max_frame_size.width() / max_frame_size.height(), + kMinLines); + if (result.width() <= 0 || result.width() > media::limits::kMaxDimension) + return max_frame_size; + return result; + } + case media::RESOLUTION_POLICY_ANY_WITHIN_LIMIT: + return gfx::Size(1, 1); + case media::RESOLUTION_POLICY_LAST: + break; + } + NOTREACHED(); + return gfx::Size(1, 1); +} + +// Returns |size|, unless it exceeds |max_size| or is under |min_size|. When +// the bounds are exceeded, computes and returns an alternate size of similar +// aspect ratio that is within the bounds. +gfx::Size ComputeBoundedCaptureSize(const gfx::Size& size, + const gfx::Size& min_size, + const gfx::Size& max_size) { + if (size.width() > max_size.width() || size.height() > max_size.height()) { + gfx::Size result = media::ScaleSizeToFitWithinTarget(size, max_size); + result.SetToMax(min_size); + return result; + } else if (size.width() < min_size.width() || + size.height() < min_size.height()) { + gfx::Size result = media::ScaleSizeToEncompassTarget(size, min_size); + result.SetToMin(max_size); + return result; + } else { + return size; + } +} + +} // namespace + +CaptureResolutionChooser::CaptureResolutionChooser( + const gfx::Size& max_frame_size, + media::ResolutionChangePolicy resolution_change_policy) + : max_frame_size_(max_frame_size), + min_frame_size_(ComputeMinimumCaptureSize(max_frame_size, + resolution_change_policy)), + resolution_change_policy_(resolution_change_policy), + constrained_size_(max_frame_size) { + DCHECK_LT(0, max_frame_size_.width()); + DCHECK_LT(0, max_frame_size_.height()); + DCHECK_LE(min_frame_size_.width(), max_frame_size_.width()); + DCHECK_LE(min_frame_size_.height(), max_frame_size_.height()); + + RecomputeCaptureSize(); +} + +CaptureResolutionChooser::~CaptureResolutionChooser() {} + +void CaptureResolutionChooser::SetSourceSize(const gfx::Size& source_size) { + if (source_size.IsEmpty()) + return; + + switch (resolution_change_policy_) { + case media::RESOLUTION_POLICY_FIXED_RESOLUTION: + // Source size changes do not affect the frame resolution. Frame + // resolution is always fixed to |max_frame_size_|. + break; + + case media::RESOLUTION_POLICY_FIXED_ASPECT_RATIO: + constrained_size_ = ComputeBoundedCaptureSize( + media::PadToMatchAspectRatio(source_size, max_frame_size_), + min_frame_size_, + max_frame_size_); + RecomputeCaptureSize(); + break; + + case media::RESOLUTION_POLICY_ANY_WITHIN_LIMIT: + constrained_size_ = ComputeBoundedCaptureSize( + source_size, min_frame_size_, max_frame_size_); + RecomputeCaptureSize(); + break; + + case media::RESOLUTION_POLICY_LAST: + NOTREACHED(); + } +} + +void CaptureResolutionChooser::RecomputeCaptureSize() { + // TODO(miu): An upcoming change will introduce the ability to find the best + // capture resolution, given the current capabilities of the system. + // http://crbug.com/156767 + capture_size_ = constrained_size_; +} + +} // namespace content diff --git a/content/browser/media/capture/capture_resolution_chooser.h b/content/browser/media/capture/capture_resolution_chooser.h new file mode 100644 index 0000000000000..f1bda79acb903 --- /dev/null +++ b/content/browser/media/capture/capture_resolution_chooser.h @@ -0,0 +1,59 @@ +// 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 CONTENT_BROWSER_MEDIA_CAPTURE_CAPTURE_RESOLUTION_CHOOSER_H_ +#define CONTENT_BROWSER_MEDIA_CAPTURE_CAPTURE_RESOLUTION_CHOOSER_H_ + +#include "content/common/content_export.h" +#include "media/base/video_capture_types.h" +#include "ui/gfx/geometry/size.h" + +namespace content { + +// Encapsulates the logic that determines the capture frame resolution based on: +// 1. The configured maximum frame resolution and resolution change policy. +// 2. The resolution of the source content. +// 3. The current capabilities of the end-to-end system, in terms of the +// maximum number of pixels per frame. +class CONTENT_EXPORT CaptureResolutionChooser { + public: + // media::ResolutionChangePolicy determines whether the variable frame + // resolutions being computed must adhere to a fixed aspect ratio or not, or + // that there must only be a single fixed resolution. + CaptureResolutionChooser( + const gfx::Size& max_frame_size, + media::ResolutionChangePolicy resolution_change_policy); + ~CaptureResolutionChooser(); + + // Returns the current capture frame resolution to use. + gfx::Size capture_size() const { + return capture_size_; + } + + // Updates the capture size based on a change in the resolution of the source + // content. + void SetSourceSize(const gfx::Size& source_size); + + private: + // Called after any update that requires |capture_size_| be re-computed. + void RecomputeCaptureSize(); + + // Hard constraints. + const gfx::Size max_frame_size_; + const gfx::Size min_frame_size_; // Computed from the ctor arguments. + + // Specifies the set of heuristics to use. + const media::ResolutionChangePolicy resolution_change_policy_; + + // The capture frame resolution to use, ignoring the limitations imposed by + // the capability metric. + gfx::Size constrained_size_; + + // The current computed capture frame resolution. + gfx::Size capture_size_; +}; + +} // namespace content + +#endif // CONTENT_BROWSER_MEDIA_CAPTURE_RESOLUTION_CHOOSER_H_ diff --git a/content/browser/media/capture/capture_resolution_chooser_unittest.cc b/content/browser/media/capture/capture_resolution_chooser_unittest.cc new file mode 100644 index 0000000000000..d91c0cec8ddf5 --- /dev/null +++ b/content/browser/media/capture/capture_resolution_chooser_unittest.cc @@ -0,0 +1,169 @@ +// 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 "content/browser/media/capture/capture_resolution_chooser.h" + +#include "base/location.h" +#include "testing/gtest/include/gtest/gtest.h" + +using tracked_objects::Location; + +namespace content { + +namespace { + +// 16:9 maximum and minimum frame sizes. +const int kMaxFrameWidth = 3840; +const int kMaxFrameHeight = 2160; +const int kMinFrameWidth = 320; +const int kMinFrameHeight = 180; + +// Checks whether |size| is strictly between (inclusive) |min_size| and +// |max_size| and has the same aspect ratio as |max_size|. +void ExpectIsWithinBoundsAndSameAspectRatio(const Location& location, + const gfx::Size& min_size, + const gfx::Size& max_size, + const gfx::Size& size) { + SCOPED_TRACE(::testing::Message() << "From here: " << location.ToString()); + EXPECT_LE(min_size.width(), size.width()); + EXPECT_LE(min_size.height(), size.height()); + EXPECT_GE(max_size.width(), size.width()); + EXPECT_GE(max_size.height(), size.height()); + EXPECT_NEAR(static_cast(max_size.width()) / max_size.height(), + static_cast(size.width()) / size.height(), + 0.01); +} + +} // namespace + +TEST(CaptureResolutionChooserTest, + FixedResolutionPolicy_CaptureSizeAlwaysFixed) { + const gfx::Size the_one_frame_size(kMaxFrameWidth, kMaxFrameHeight); + CaptureResolutionChooser chooser(the_one_frame_size, + media::RESOLUTION_POLICY_FIXED_RESOLUTION); + EXPECT_EQ(the_one_frame_size, chooser.capture_size()); + + chooser.SetSourceSize(the_one_frame_size); + EXPECT_EQ(the_one_frame_size, chooser.capture_size()); + + chooser.SetSourceSize(gfx::Size(kMaxFrameWidth + 424, kMaxFrameHeight - 101)); + EXPECT_EQ(the_one_frame_size, chooser.capture_size()); + + chooser.SetSourceSize(gfx::Size(kMaxFrameWidth - 202, kMaxFrameHeight + 56)); + EXPECT_EQ(the_one_frame_size, chooser.capture_size()); + + chooser.SetSourceSize(gfx::Size(kMinFrameWidth, kMinFrameHeight)); + EXPECT_EQ(the_one_frame_size, chooser.capture_size()); +} + +TEST(CaptureResolutionChooserTest, + FixedAspectRatioPolicy_CaptureSizeHasSameAspectRatio) { + CaptureResolutionChooser chooser( + gfx::Size(kMaxFrameWidth, kMaxFrameHeight), + media::RESOLUTION_POLICY_FIXED_ASPECT_RATIO); + + // Starting condition. + const gfx::Size min_size(kMinFrameWidth, kMinFrameHeight); + const gfx::Size max_size(kMaxFrameWidth, kMaxFrameHeight); + ExpectIsWithinBoundsAndSameAspectRatio( + FROM_HERE, min_size, max_size, chooser.capture_size()); + + // Max size in --> max size out. + chooser.SetSourceSize(gfx::Size(kMaxFrameWidth, kMaxFrameHeight)); + ExpectIsWithinBoundsAndSameAspectRatio( + FROM_HERE, min_size, max_size, chooser.capture_size()); + + // Various source sizes within bounds. + chooser.SetSourceSize(gfx::Size(640, 480)); + ExpectIsWithinBoundsAndSameAspectRatio( + FROM_HERE, min_size, max_size, chooser.capture_size()); + + chooser.SetSourceSize(gfx::Size(480, 640)); + ExpectIsWithinBoundsAndSameAspectRatio( + FROM_HERE, min_size, max_size, chooser.capture_size()); + + chooser.SetSourceSize(gfx::Size(640, 640)); + ExpectIsWithinBoundsAndSameAspectRatio( + FROM_HERE, min_size, max_size, chooser.capture_size()); + + // Bad source size results in no update. + const gfx::Size unchanged_size = chooser.capture_size(); + chooser.SetSourceSize(gfx::Size(0, 0)); + EXPECT_EQ(unchanged_size, chooser.capture_size()); + + // Downscaling size (preserving aspect ratio) when source size exceeds the + // upper bounds. + chooser.SetSourceSize(gfx::Size(kMaxFrameWidth * 2, kMaxFrameHeight * 2)); + ExpectIsWithinBoundsAndSameAspectRatio( + FROM_HERE, min_size, max_size, chooser.capture_size()); + + chooser.SetSourceSize(gfx::Size(kMaxFrameWidth * 2, kMaxFrameHeight)); + ExpectIsWithinBoundsAndSameAspectRatio( + FROM_HERE, min_size, max_size, chooser.capture_size()); + + chooser.SetSourceSize(gfx::Size(kMaxFrameWidth, kMaxFrameHeight * 2)); + ExpectIsWithinBoundsAndSameAspectRatio( + FROM_HERE, min_size, max_size, chooser.capture_size()); + + // Upscaling size (preserving aspect ratio) when source size is under the + // lower bounds. + chooser.SetSourceSize(gfx::Size(kMinFrameWidth / 2, kMinFrameHeight / 2)); + ExpectIsWithinBoundsAndSameAspectRatio( + FROM_HERE, min_size, max_size, chooser.capture_size()); + + chooser.SetSourceSize(gfx::Size(kMinFrameWidth / 2, kMaxFrameHeight)); + ExpectIsWithinBoundsAndSameAspectRatio( + FROM_HERE, min_size, max_size, chooser.capture_size()); + + chooser.SetSourceSize(gfx::Size(kMinFrameWidth, kMinFrameHeight / 2)); + ExpectIsWithinBoundsAndSameAspectRatio( + FROM_HERE, min_size, max_size, chooser.capture_size()); +} + +TEST(CaptureResolutionChooserTest, + AnyWithinLimitPolicy_CaptureSizeIsAnythingWithinLimits) { + const gfx::Size max_size(kMaxFrameWidth, kMaxFrameHeight); + CaptureResolutionChooser chooser( + max_size, media::RESOLUTION_POLICY_ANY_WITHIN_LIMIT); + + // Starting condition. + EXPECT_EQ(max_size, chooser.capture_size()); + + // Max size in --> max size out. + chooser.SetSourceSize(max_size); + EXPECT_EQ(max_size, chooser.capture_size()); + + // Various source sizes within bounds. + chooser.SetSourceSize(gfx::Size(640, 480)); + EXPECT_EQ(gfx::Size(640, 480), chooser.capture_size()); + + chooser.SetSourceSize(gfx::Size(480, 640)); + EXPECT_EQ(gfx::Size(480, 640), chooser.capture_size()); + + chooser.SetSourceSize(gfx::Size(640, 640)); + EXPECT_EQ(gfx::Size(640, 640), chooser.capture_size()); + + chooser.SetSourceSize(gfx::Size(2, 2)); + EXPECT_EQ(gfx::Size(2, 2), chooser.capture_size()); + + // Bad source size results in no update. + const gfx::Size unchanged_size = chooser.capture_size(); + chooser.SetSourceSize(gfx::Size(0, 0)); + EXPECT_EQ(unchanged_size, chooser.capture_size()); + + // Downscaling size (preserving aspect ratio) when source size exceeds the + // upper bounds. + chooser.SetSourceSize(gfx::Size(kMaxFrameWidth * 2, kMaxFrameHeight * 2)); + EXPECT_EQ(max_size, chooser.capture_size()); + + chooser.SetSourceSize(gfx::Size(kMaxFrameWidth * 2, kMaxFrameHeight)); + EXPECT_EQ(gfx::Size(kMaxFrameWidth, kMaxFrameHeight / 2), + chooser.capture_size()); + + chooser.SetSourceSize(gfx::Size(kMaxFrameWidth, kMaxFrameHeight * 2)); + EXPECT_EQ(gfx::Size(kMaxFrameWidth / 2, kMaxFrameHeight), + chooser.capture_size()); +} + +} // namespace content diff --git a/content/browser/media/capture/content_video_capture_device_core.cc b/content/browser/media/capture/content_video_capture_device_core.cc index 8d44104c1c418..da0862ccb8ee4 100644 --- a/content/browser/media/capture/content_video_capture_device_core.cc +++ b/content/browser/media/capture/content_video_capture_device_core.cc @@ -49,7 +49,9 @@ ThreadSafeCaptureOracle::ThreadSafeCaptureOracle( oracle_(base::TimeDelta::FromMicroseconds( static_cast(1000000.0 / params.requested_format.frame_rate + 0.5 /* to round to nearest int */))), - params_(params) {} + params_(params), + resolution_chooser_(params.requested_format.frame_size, + params.resolution_change_policy) {} ThreadSafeCaptureOracle::~ThreadSafeCaptureOracle() {} @@ -67,9 +69,7 @@ bool ThreadSafeCaptureOracle::ObserveEventAndDecideCapture( if (!client_) return false; // Capture is stopped. - if (capture_size_.IsEmpty()) - capture_size_ = max_frame_size(); - const gfx::Size visible_size = capture_size_; + const gfx::Size visible_size = resolution_chooser_.capture_size(); // Always round up the coded size to multiple of 16 pixels. // See http://crbug.com/402151. const gfx::Size coded_size((visible_size.width() + 15) & ~15, @@ -141,30 +141,15 @@ bool ThreadSafeCaptureOracle::ObserveEventAndDecideCapture( gfx::Size ThreadSafeCaptureOracle::GetCaptureSize() const { base::AutoLock guard(lock_); - return capture_size_.IsEmpty() ? max_frame_size() : capture_size_; + return resolution_chooser_.capture_size(); } void ThreadSafeCaptureOracle::UpdateCaptureSize(const gfx::Size& source_size) { base::AutoLock guard(lock_); - - // Update |capture_size_| based on |source_size| if either: 1) The resolution - // change policy specifies fixed frame sizes and |capture_size_| has not yet - // been set; or 2) The resolution change policy specifies dynamic frame - // sizes. - if (capture_size_.IsEmpty() || params_.resolution_change_policy == - media::RESOLUTION_POLICY_ANY_WITHIN_LIMIT) { - capture_size_ = source_size; - // The capture size should not exceed the maximum frame size. - if (capture_size_.width() > max_frame_size().width() || - capture_size_.height() > max_frame_size().height()) { - capture_size_ = media::ComputeLetterboxRegion( - gfx::Rect(max_frame_size()), capture_size_).size(); - } - // The capture size must be even and not less than the minimum frame size. - capture_size_ = gfx::Size( - std::max(kMinFrameWidth, MakeEven(capture_size_.width())), - std::max(kMinFrameHeight, MakeEven(capture_size_.height()))); - } + resolution_chooser_.SetSourceSize(source_size); + VLOG(1) << "Source size changed to " << source_size.ToString() + << " --> Capture size is now " + << resolution_chooser_.capture_size().ToString(); } void ThreadSafeCaptureOracle::Stop() { @@ -234,23 +219,15 @@ void ContentVideoCaptureDeviceCore::AllocateAndStart( return; } - if (params.requested_format.frame_size.width() < kMinFrameWidth || - params.requested_format.frame_size.height() < kMinFrameHeight) { - std::string error_msg = - "invalid frame size: " + params.requested_format.frame_size.ToString(); - DVLOG(1) << error_msg; - client->OnError(error_msg); - return; - } - - media::VideoCaptureParams new_params = params; - // Frame dimensions must each be an even integer since the client wants (or - // will convert to) YUV420. - new_params.requested_format.frame_size.SetSize( - MakeEven(params.requested_format.frame_size.width()), - MakeEven(params.requested_format.frame_size.height())); + if (params.requested_format.frame_size.IsEmpty()) { + std::string error_msg = + "invalid frame size: " + params.requested_format.frame_size.ToString(); + DVLOG(1) << error_msg; + client->OnError(error_msg); + return; + } - oracle_proxy_ = new ThreadSafeCaptureOracle(client.Pass(), new_params); + oracle_proxy_ = new ThreadSafeCaptureOracle(client.Pass(), params); // Starts the capture machine asynchronously. BrowserThread::PostTaskAndReplyWithResult( @@ -259,7 +236,7 @@ void ContentVideoCaptureDeviceCore::AllocateAndStart( base::Bind(&VideoCaptureMachine::Start, base::Unretained(capture_machine_.get()), oracle_proxy_, - new_params), + params), base::Bind(&ContentVideoCaptureDeviceCore::CaptureStarted, AsWeakPtr())); TransitionStateTo(kCapturing); diff --git a/content/browser/media/capture/content_video_capture_device_core.h b/content/browser/media/capture/content_video_capture_device_core.h index f477a157439a3..67dffd6ae65a9 100644 --- a/content/browser/media/capture/content_video_capture_device_core.h +++ b/content/browser/media/capture/content_video_capture_device_core.h @@ -11,6 +11,7 @@ #include "base/memory/weak_ptr.h" #include "base/threading/thread.h" #include "base/threading/thread_checker.h" +#include "content/browser/media/capture/capture_resolution_chooser.h" #include "content/browser/media/capture/video_capture_oracle.h" #include "content/common/content_export.h" #include "media/base/video_frame.h" @@ -23,15 +24,6 @@ class VideoFrame; namespace content { -const int kMinFrameWidth = 2; -const int kMinFrameHeight = 2; - -// Returns the nearest even integer closer to zero. -template -IntType MakeEven(IntType x) { - return x & static_cast(-2); -} - class VideoCaptureMachine; // Thread-safe, refcounted proxy to the VideoCaptureOracle. This proxy wraps @@ -104,8 +96,8 @@ class ThreadSafeCaptureOracle // The video capture parameters used to construct the oracle proxy. const media::VideoCaptureParams params_; - // The current video capture size. - gfx::Size capture_size_; + // Determines video capture frame sizes. + CaptureResolutionChooser resolution_chooser_; }; // Keeps track of the video capture source frames and executes copying on the diff --git a/content/browser/media/capture/desktop_capture_device.cc b/content/browser/media/capture/desktop_capture_device.cc index a11e473e83cf6..e42a678daff3c 100644 --- a/content/browser/media/capture/desktop_capture_device.cc +++ b/content/browser/media/capture/desktop_capture_device.cc @@ -12,6 +12,7 @@ #include "base/synchronization/lock.h" #include "base/threading/thread.h" #include "base/timer/timer.h" +#include "content/browser/media/capture/capture_resolution_chooser.h" #include "content/browser/media/capture/desktop_capture_device_uma_types.h" #include "content/public/browser/browser_thread.h" #include "content/public/browser/desktop_media_id.h" @@ -72,11 +73,6 @@ class DesktopCaptureDevice::Core : public webrtc::DesktopCapturer::Callback { webrtc::SharedMemory* CreateSharedMemory(size_t size) override; void OnCaptureCompleted(webrtc::DesktopFrame* frame) override; - // Chooses new output properties based on the supplied source size and the - // properties requested to Allocate(), and dispatches OnFrameInfo[Changed] - // notifications. - void RefreshCaptureFormat(const webrtc::DesktopSize& frame_size); - // Method that is scheduled on |task_runner_| to be called on regular interval // to capture a frame. void OnCaptureTimer(); @@ -97,24 +93,20 @@ class DesktopCaptureDevice::Core : public webrtc::DesktopCapturer::Callback { // on the task_runner_ thread. scoped_ptr client_; - // Requested video capture format (width, height, frame rate, etc). - media::VideoCaptureParams requested_params_; - - // Actual video capture format being generated. - media::VideoCaptureFormat capture_format_; + // Requested video capture frame rate. + float requested_frame_rate_; // Size of frame most recently captured from the source. webrtc::DesktopSize previous_frame_size_; + // Determines the size of frames to deliver to the |client_|. + scoped_ptr resolution_chooser_; + // DesktopFrame into which captured frames are down-scaled and/or letterboxed, // depending upon the caller's requested capture capabilities. If frames can // be returned to the caller directly then this is NULL. scoped_ptr output_frame_; - // Sub-rectangle of |output_frame_| into which the source will be scaled - // and/or letterboxed. - webrtc::DesktopRect output_rect_; - // Timer used to capture the frame. base::OneShotTimer capture_timer_; @@ -167,12 +159,10 @@ void DesktopCaptureDevice::Core::AllocateAndStart( DCHECK(!client_.get()); client_ = client.Pass(); - requested_params_ = params; - - capture_format_ = requested_params_.requested_format; - - // This capturer always outputs ARGB, non-interlaced. - capture_format_.pixel_format = media::PIXEL_FORMAT_ARGB; + requested_frame_rate_ = params.requested_format.frame_rate; + resolution_chooser_.reset(new CaptureResolutionChooser( + params.requested_format.frame_size, + params.resolution_change_policy)); power_save_blocker_.reset( PowerSaveBlocker::Create( @@ -238,16 +228,29 @@ void DesktopCaptureDevice::Core::OnCaptureCompleted( scoped_ptr owned_frame(frame); + // If the frame size has changed, drop the output frame (if any), and + // determine the new output size. + if (!previous_frame_size_.equals(frame->size())) { + output_frame_.reset(); + resolution_chooser_->SetSourceSize(gfx::Size(frame->size().width(), + frame->size().height())); + previous_frame_size_ = frame->size(); + } + // Align to 2x2 pixel boundaries, as required by OnIncomingCapturedData() so + // it can convert the frame to I420 format. + const webrtc::DesktopSize output_size( + resolution_chooser_->capture_size().width() & ~1, + resolution_chooser_->capture_size().height() & ~1); + if (output_size.is_empty()) + return; + // On OSX We receive a 1x1 frame when the shared window is minimized. It // cannot be subsampled to I420 and will be dropped downstream. So we replace // it with a black frame to avoid the video appearing frozen at the last // frame. if (frame->size().width() == 1 || frame->size().height() == 1) { if (!black_frame_.get()) { - black_frame_.reset( - new webrtc::BasicDesktopFrame( - webrtc::DesktopSize(capture_format_.frame_size.width(), - capture_format_.frame_size.height()))); + black_frame_.reset(new webrtc::BasicDesktopFrame(output_size)); memset(black_frame_->data(), 0, black_frame_->stride() * black_frame_->size().height()); @@ -256,11 +259,6 @@ void DesktopCaptureDevice::Core::OnCaptureCompleted( frame = black_frame_.get(); } - // Handle initial frame size and size changes. - RefreshCaptureFormat(frame->size()); - - webrtc::DesktopSize output_size(capture_format_.frame_size.width(), - capture_format_.frame_size.height()); size_t output_bytes = output_size.width() * output_size.height() * webrtc::DesktopFrame::kBytesPerPixel; const uint8_t* output_data = NULL; @@ -270,7 +268,7 @@ void DesktopCaptureDevice::Core::OnCaptureCompleted( // match the output size. // Allocate a buffer of the correct size to scale the frame into. - // |output_frame_| is cleared whenever |output_rect_| changes, so we don't + // |output_frame_| is cleared whenever the output size changes, so we don't // need to worry about clearing out stale pixel data in letterboxed areas. if (!output_frame_) { output_frame_.reset(new webrtc::BasicDesktopFrame(output_size)); @@ -280,13 +278,15 @@ void DesktopCaptureDevice::Core::OnCaptureCompleted( // TODO(wez): Optimize this to scale only changed portions of the output, // using ARGBScaleClip(). + const webrtc::DesktopRect output_rect = + ComputeLetterboxRect(output_size, frame->size()); uint8_t* output_rect_data = output_frame_->data() + - output_frame_->stride() * output_rect_.top() + - webrtc::DesktopFrame::kBytesPerPixel * output_rect_.left(); + output_frame_->stride() * output_rect.top() + + webrtc::DesktopFrame::kBytesPerPixel * output_rect.left(); libyuv::ARGBScale(frame->data(), frame->stride(), frame->size().width(), frame->size().height(), output_rect_data, output_frame_->stride(), - output_rect_.width(), output_rect_.height(), + output_rect.width(), output_rect.height(), libyuv::kFilterBilinear); output_data = output_frame_->data(); } else if (IsFrameUnpackedOrInverted(frame)) { @@ -311,49 +311,14 @@ void DesktopCaptureDevice::Core::OnCaptureCompleted( } client_->OnIncomingCapturedData( - output_data, output_bytes, capture_format_, 0, base::TimeTicks::Now()); -} - -void DesktopCaptureDevice::Core::RefreshCaptureFormat( - const webrtc::DesktopSize& frame_size) { - if (previous_frame_size_.equals(frame_size)) - return; - - // Clear the output frame, if any, since it will either need resizing, or - // clearing of stale data in letterbox areas, anyway. - output_frame_.reset(); - - if (previous_frame_size_.is_empty() || - requested_params_.resolution_change_policy == - media::RESOLUTION_POLICY_ANY_WITHIN_LIMIT) { - // If this is the first frame, or the receiver supports variable resolution - // then determine the output size by treating the requested width & height - // as maxima. - if (frame_size.width() > - requested_params_.requested_format.frame_size.width() || - frame_size.height() > - requested_params_.requested_format.frame_size.height()) { - output_rect_ = ComputeLetterboxRect( - webrtc::DesktopSize( - requested_params_.requested_format.frame_size.width(), - requested_params_.requested_format.frame_size.height()), - frame_size); - output_rect_.Translate(-output_rect_.left(), -output_rect_.top()); - } else { - output_rect_ = webrtc::DesktopRect::MakeSize(frame_size); - } - capture_format_.frame_size.SetSize(output_rect_.width(), - output_rect_.height()); - } else { - // Otherwise the output frame size cannot change, so just scale and - // letterbox. - output_rect_ = ComputeLetterboxRect( - webrtc::DesktopSize(capture_format_.frame_size.width(), - capture_format_.frame_size.height()), - frame_size); - } - - previous_frame_size_ = frame_size; + output_data, + output_bytes, + media::VideoCaptureFormat(gfx::Size(output_size.width(), + output_size.height()), + requested_frame_rate_, + media::PIXEL_FORMAT_ARGB), + 0, + base::TimeTicks::Now()); } void DesktopCaptureDevice::Core::OnCaptureTimer() { @@ -375,7 +340,8 @@ void DesktopCaptureDevice::Core::CaptureFrameAndScheduleNext() { // Limit frame-rate to reduce CPU consumption. base::TimeDelta capture_period = std::max( (last_capture_duration * 100) / kMaximumCpuConsumptionPercentage, - base::TimeDelta::FromSeconds(1) / capture_format_.frame_rate); + base::TimeDelta::FromMicroseconds(static_cast( + 1000000.0 / requested_frame_rate_ + 0.5 /* round to nearest int */))); // Schedule a task for the next frame. capture_timer_.Start(FROM_HERE, capture_period - last_capture_duration, diff --git a/content/browser/media/capture/desktop_capture_device_unittest.cc b/content/browser/media/capture/desktop_capture_device_unittest.cc index c86ddd3857ec5..56e06ff66b42a 100644 --- a/content/browser/media/capture/desktop_capture_device_unittest.cc +++ b/content/browser/media/capture/desktop_capture_device_unittest.cc @@ -4,6 +4,7 @@ #include "content/browser/media/capture/desktop_capture_device.h" +#include #include #include "base/basictypes.h" @@ -22,8 +23,10 @@ using ::testing::_; using ::testing::AnyNumber; using ::testing::DoAll; using ::testing::Expectation; +using ::testing::Invoke; using ::testing::InvokeWithoutArgs; using ::testing::SaveArg; +using ::testing::WithArg; namespace content { @@ -33,10 +36,10 @@ MATCHER_P2(EqualsCaptureCapability, width, height, "") { return arg.width == width && arg.height == height; } -const int kTestFrameWidth1 = 100; -const int kTestFrameHeight1 = 100; -const int kTestFrameWidth2 = 200; -const int kTestFrameHeight2 = 150; +const int kTestFrameWidth1 = 500; +const int kTestFrameHeight1 = 500; +const int kTestFrameWidth2 = 400; +const int kTestFrameHeight2 = 300; const int kFrameRate = 30; @@ -208,6 +211,32 @@ class FakeScreenCapturer : public webrtc::ScreenCapturer { bool generate_cropped_frames_; }; +// Helper used to check that only two specific frame sizes are delivered to the +// OnIncomingCapturedData() callback. +class FormatChecker { + public: + FormatChecker(const gfx::Size& size_for_even_frames, + const gfx::Size& size_for_odd_frames) + : size_for_even_frames_(size_for_even_frames), + size_for_odd_frames_(size_for_odd_frames), + frame_count_(0) {} + + void ExpectAcceptableSize(const media::VideoCaptureFormat& format) { + if (frame_count_ % 2 == 0) + EXPECT_EQ(size_for_even_frames_, format.frame_size); + else + EXPECT_EQ(size_for_odd_frames_, format.frame_size); + ++frame_count_; + EXPECT_EQ(kFrameRate, format.frame_rate); + EXPECT_EQ(media::PIXEL_FORMAT_ARGB, format.pixel_format); + } + + private: + const gfx::Size size_for_even_frames_; + const gfx::Size size_for_odd_frames_; + int frame_count_; +}; + } // namespace class DesktopCaptureDeviceTest : public testing::Test { @@ -277,15 +306,15 @@ TEST_F(DesktopCaptureDeviceTest, ScreenResolutionChangeConstantResolution) { CreateScreenCaptureDevice(scoped_ptr(mock_capturer)); - media::VideoCaptureFormat format; + FormatChecker format_checker(gfx::Size(kTestFrameWidth1, kTestFrameHeight1), + gfx::Size(kTestFrameWidth1, kTestFrameHeight1)); base::WaitableEvent done_event(false, false); - int frame_size; scoped_ptr client(new MockDeviceClient()); EXPECT_CALL(*client, OnError(_)).Times(0); EXPECT_CALL(*client, OnIncomingCapturedData(_, _, _, _, _)).WillRepeatedly( - DoAll(SaveArg<1>(&frame_size), - SaveArg<2>(&format), + DoAll(WithArg<2>(Invoke(&format_checker, + &FormatChecker::ExpectAcceptableSize)), InvokeWithoutArgs(&done_event, &base::WaitableEvent::Signal))); media::VideoCaptureParams capture_params; @@ -293,23 +322,66 @@ TEST_F(DesktopCaptureDeviceTest, ScreenResolutionChangeConstantResolution) { kTestFrameHeight1); capture_params.requested_format.frame_rate = kFrameRate; capture_params.requested_format.pixel_format = media::PIXEL_FORMAT_I420; + capture_params.resolution_change_policy = + media::RESOLUTION_POLICY_FIXED_RESOLUTION; capture_device_->AllocateAndStart(capture_params, client.Pass()); // Capture at least two frames, to ensure that the source frame size has - // changed while capturing. - EXPECT_TRUE(done_event.TimedWait(TestTimeouts::action_max_timeout())); - done_event.Reset(); - EXPECT_TRUE(done_event.TimedWait(TestTimeouts::action_max_timeout())); + // changed to two different sizes while capturing. The mock for + // OnIncomingCapturedData() will use FormatChecker to examine the format of + // each frame being delivered. + for (int i = 0; i < 2; ++i) { + EXPECT_TRUE(done_event.TimedWait(TestTimeouts::action_max_timeout())); + done_event.Reset(); + } capture_device_->StopAndDeAllocate(); +} - EXPECT_EQ(kTestFrameWidth1, format.frame_size.width()); - EXPECT_EQ(kTestFrameHeight1, format.frame_size.height()); - EXPECT_EQ(kFrameRate, format.frame_rate); - EXPECT_EQ(media::PIXEL_FORMAT_ARGB, format.pixel_format); +// Test that screen capturer behaves correctly if the source frame size changes, +// where the video frames sent the the client vary in resolution but maintain +// the same aspect ratio. +TEST_F(DesktopCaptureDeviceTest, ScreenResolutionChangeFixedAspectRatio) { + FakeScreenCapturer* mock_capturer = new FakeScreenCapturer(); - EXPECT_EQ(format.frame_size.GetArea() * 4, frame_size); + CreateScreenCaptureDevice(scoped_ptr(mock_capturer)); + + FormatChecker format_checker(gfx::Size(888, 500), gfx::Size(532, 300)); + base::WaitableEvent done_event(false, false); + + scoped_ptr client(new MockDeviceClient()); + EXPECT_CALL(*client, OnError(_)).Times(0); + EXPECT_CALL(*client, OnIncomingCapturedData(_, _, _, _, _)).WillRepeatedly( + DoAll(WithArg<2>(Invoke(&format_checker, + &FormatChecker::ExpectAcceptableSize)), + InvokeWithoutArgs(&done_event, &base::WaitableEvent::Signal))); + + media::VideoCaptureParams capture_params; + const gfx::Size high_def_16_by_9(1920, 1080); + ASSERT_GE(high_def_16_by_9.width(), + std::max(kTestFrameWidth1, kTestFrameWidth2)); + ASSERT_GE(high_def_16_by_9.height(), + std::max(kTestFrameHeight1, kTestFrameHeight2)); + capture_params.requested_format.frame_size = high_def_16_by_9; + capture_params.requested_format.frame_rate = kFrameRate; + capture_params.requested_format.pixel_format = media::PIXEL_FORMAT_I420; + capture_params.resolution_change_policy = + media::RESOLUTION_POLICY_FIXED_ASPECT_RATIO; + + capture_device_->AllocateAndStart( + capture_params, client.Pass()); + + // Capture at least three frames, to ensure that the source frame size has + // changed to two different sizes while capturing. The mock for + // OnIncomingCapturedData() will use FormatChecker to examine the format of + // each frame being delivered. + for (int i = 0; i < 3; ++i) { + EXPECT_TRUE(done_event.TimedWait(TestTimeouts::action_max_timeout())); + done_event.Reset(); + } + + capture_device_->StopAndDeAllocate(); } // Test that screen capturer behaves correctly if the source frame size changes @@ -319,38 +391,42 @@ TEST_F(DesktopCaptureDeviceTest, ScreenResolutionChangeVariableResolution) { CreateScreenCaptureDevice(scoped_ptr(mock_capturer)); - media::VideoCaptureFormat format; + FormatChecker format_checker(gfx::Size(kTestFrameWidth1, kTestFrameHeight1), + gfx::Size(kTestFrameWidth2, kTestFrameHeight2)); base::WaitableEvent done_event(false, false); scoped_ptr client(new MockDeviceClient()); EXPECT_CALL(*client, OnError(_)).Times(0); EXPECT_CALL(*client, OnIncomingCapturedData(_, _, _, _, _)).WillRepeatedly( - DoAll(SaveArg<2>(&format), + DoAll(WithArg<2>(Invoke(&format_checker, + &FormatChecker::ExpectAcceptableSize)), InvokeWithoutArgs(&done_event, &base::WaitableEvent::Signal))); media::VideoCaptureParams capture_params; - capture_params.requested_format.frame_size.SetSize(kTestFrameWidth2, - kTestFrameHeight2); + const gfx::Size high_def_16_by_9(1920, 1080); + ASSERT_GE(high_def_16_by_9.width(), + std::max(kTestFrameWidth1, kTestFrameWidth2)); + ASSERT_GE(high_def_16_by_9.height(), + std::max(kTestFrameHeight1, kTestFrameHeight2)); + capture_params.requested_format.frame_size = high_def_16_by_9; capture_params.requested_format.frame_rate = kFrameRate; capture_params.requested_format.pixel_format = media::PIXEL_FORMAT_I420; + capture_params.resolution_change_policy = + media::RESOLUTION_POLICY_ANY_WITHIN_LIMIT; capture_device_->AllocateAndStart( capture_params, client.Pass()); // Capture at least three frames, to ensure that the source frame size has - // changed at least twice while capturing. - EXPECT_TRUE(done_event.TimedWait(TestTimeouts::action_max_timeout())); - done_event.Reset(); - EXPECT_TRUE(done_event.TimedWait(TestTimeouts::action_max_timeout())); - done_event.Reset(); - EXPECT_TRUE(done_event.TimedWait(TestTimeouts::action_max_timeout())); + // changed to two different sizes while capturing. The mock for + // OnIncomingCapturedData() will use FormatChecker to examine the format of + // each frame being delivered. + for (int i = 0; i < 3; ++i) { + EXPECT_TRUE(done_event.TimedWait(TestTimeouts::action_max_timeout())); + done_event.Reset(); + } capture_device_->StopAndDeAllocate(); - - EXPECT_EQ(kTestFrameWidth1, format.frame_size.width()); - EXPECT_EQ(kTestFrameHeight1, format.frame_size.height()); - EXPECT_EQ(kFrameRate, format.frame_rate); - EXPECT_EQ(media::PIXEL_FORMAT_ARGB, format.pixel_format); } // This test verifies that an unpacked frame is converted to a packed frame. diff --git a/content/browser/media/capture/web_contents_audio_input_stream.cc b/content/browser/media/capture/web_contents_audio_input_stream.cc index 629d1061f9f86..8fd1f976d5999 100644 --- a/content/browser/media/capture/web_contents_audio_input_stream.cc +++ b/content/browser/media/capture/web_contents_audio_input_stream.cc @@ -91,7 +91,7 @@ class WebContentsAudioInputStream::Impl // Called by WebContentsTracker when the target of the audio mirroring has // changed. - void OnTargetChanged(RenderWidgetHost* target); + void OnTargetChanged(bool had_target); // Injected dependencies. const int initial_render_process_id_; @@ -305,11 +305,10 @@ void WebContentsAudioInputStream::Impl::ReleaseInput( delete stream; } -void WebContentsAudioInputStream::Impl::OnTargetChanged( - RenderWidgetHost* target) { +void WebContentsAudioInputStream::Impl::OnTargetChanged(bool had_target) { DCHECK(thread_checker_.CalledOnValidThread()); - is_target_lost_ = !target; + is_target_lost_ = !had_target; if (state_ == MIRRORING) { if (is_target_lost_) { diff --git a/content/browser/media/capture/web_contents_audio_input_stream_unittest.cc b/content/browser/media/capture/web_contents_audio_input_stream_unittest.cc index ffe8b4b5d3f16..0eebd7a8083b9 100644 --- a/content/browser/media/capture/web_contents_audio_input_stream_unittest.cc +++ b/content/browser/media/capture/web_contents_audio_input_stream_unittest.cc @@ -356,13 +356,7 @@ class WebContentsAudioInputStreamTest : public testing::Test { private: void SimulateChangeCallback(int render_process_id, int render_frame_id) { ASSERT_FALSE(change_callback_.is_null()); - if (render_process_id == -1 || render_frame_id == -1) { - change_callback_.Run(NULL); - } else { - // For our tests, any non-NULL value will suffice since it will not be - // dereferenced. - change_callback_.Run(reinterpret_cast(0xdeadbee5)); - } + change_callback_.Run(render_process_id != -1 && render_frame_id != -1); } scoped_ptr thread_bundle_; diff --git a/content/browser/media/capture/web_contents_tracker.cc b/content/browser/media/capture/web_contents_tracker.cc index a9e161f1ce3fc..5007a583af89f 100644 --- a/content/browser/media/capture/web_contents_tracker.cc +++ b/content/browser/media/capture/web_contents_tracker.cc @@ -44,6 +44,7 @@ void WebContentsTracker::Stop() { DCHECK(task_runner_->BelongsToCurrentThread()); callback_.Reset(); + resize_callback_.Reset(); if (BrowserThread::CurrentlyOn(BrowserThread::UI)) { WebContentsObserver::Observe(NULL); @@ -78,6 +79,12 @@ RenderWidgetHost* WebContentsTracker::GetTargetRenderWidgetHost() const { return rwh; } +void WebContentsTracker::SetResizeChangeCallback( + const base::Closure& callback) { + DCHECK(!task_runner_.get() || task_runner_->BelongsToCurrentThread()); + resize_callback_ = callback; +} + void WebContentsTracker::OnPossibleTargetChange(bool force_callback_run) { DCHECK_CURRENTLY_ON(BrowserThread::UI); @@ -89,19 +96,29 @@ void WebContentsTracker::OnPossibleTargetChange(bool force_callback_run) { last_target_ = rwh; if (task_runner_->BelongsToCurrentThread()) { - MaybeDoCallback(rwh); - } else { - task_runner_->PostTask( - FROM_HERE, - base::Bind(&WebContentsTracker::MaybeDoCallback, this, rwh)); + MaybeDoCallback(rwh != nullptr); + return; } + + task_runner_->PostTask( + FROM_HERE, + base::Bind(&WebContentsTracker::MaybeDoCallback, this, rwh != nullptr)); } -void WebContentsTracker::MaybeDoCallback(RenderWidgetHost* rwh) { +void WebContentsTracker::MaybeDoCallback(bool was_still_tracking) { DCHECK(task_runner_->BelongsToCurrentThread()); if (!callback_.is_null()) - callback_.Run(rwh); + callback_.Run(was_still_tracking); + if (was_still_tracking) + MaybeDoResizeCallback(); +} + +void WebContentsTracker::MaybeDoResizeCallback() { + DCHECK(task_runner_->BelongsToCurrentThread()); + + if (!resize_callback_.is_null()) + resize_callback_.Run(); } void WebContentsTracker::StartObservingWebContents(int render_process_id, @@ -128,6 +145,19 @@ void WebContentsTracker::RenderFrameHostChanged(RenderFrameHost* old_host, OnPossibleTargetChange(false); } +void WebContentsTracker::MainFrameWasResized(bool width_changed) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + if (task_runner_->BelongsToCurrentThread()) { + MaybeDoResizeCallback(); + return; + } + + task_runner_->PostTask( + FROM_HERE, + base::Bind(&WebContentsTracker::MaybeDoResizeCallback, this)); +} + void WebContentsTracker::WebContentsDestroyed() { Observe(NULL); OnPossibleTargetChange(false); diff --git a/content/browser/media/capture/web_contents_tracker.h b/content/browser/media/capture/web_contents_tracker.h index 613c971d44531..591c5e35a9cd5 100644 --- a/content/browser/media/capture/web_contents_tracker.h +++ b/content/browser/media/capture/web_contents_tracker.h @@ -40,15 +40,15 @@ class CONTENT_EXPORT WebContentsTracker explicit WebContentsTracker(bool track_fullscreen_rwh); // Callback to indicate a new RenderWidgetHost should be targeted for capture. - // This is also invoked with NULL to indicate tracking will not continue + // This is also invoked with false to indicate tracking will not continue // (i.e., the WebContents instance was not found or has been destroyed). - typedef base::Callback ChangeCallback; + typedef base::Callback ChangeCallback; // Start tracking. The last-known |render_process_id| and // |main_render_frame_id| are provided, and |callback| will be run once to - // indicate the current capture target (this may occur during the invocation - // of Start(), or in the future). The callback will be invoked on the same - // thread calling Start(). + // indicate whether tracking successfully started (this may occur during the + // invocation of Start(), or in the future). The callback will be invoked on + // the same thread calling Start(). virtual void Start(int render_process_id, int main_render_frame_id, const ChangeCallback& callback); @@ -59,6 +59,12 @@ class CONTENT_EXPORT WebContentsTracker // Current target. This must only be called on the UI BrowserThread. RenderWidgetHost* GetTargetRenderWidgetHost() const; + // Set a callback that is run whenever the main frame of the WebContents is + // resized. This method must be called on the same thread that calls + // Start()/Stop(), and |callback| will be run on that same thread. Calling + // the Stop() method guarantees the callback will never be invoked again. + void SetResizeChangeCallback(const base::Closure& callback); + protected: friend class base::RefCountedThreadSafe; ~WebContentsTracker() override; @@ -72,7 +78,12 @@ class CONTENT_EXPORT WebContentsTracker // Called on the thread that Start()/Stop() are called on. Checks whether the // callback is still valid and, if so, runs it. - void MaybeDoCallback(RenderWidgetHost* rwh); + void MaybeDoCallback(bool was_still_tracking); + + // Called on the thread that Start()/Stop() are called on. Checks whether the + // callback is still valid and, if so, runs it to indicate the main frame has + // changed in size. + void MaybeDoResizeCallback(); // Look-up the current WebContents instance associated with the given // |render_process_id| and |main_render_frame_id| and begin observing it. @@ -86,6 +97,10 @@ class CONTENT_EXPORT WebContentsTracker void RenderFrameHostChanged(RenderFrameHost* old_host, RenderFrameHost* new_host) override; + // WebContentsObserver override to notify the client that the source size has + // changed. + void MainFrameWasResized(bool width_changed) override; + // WebContentsObserver override to notify the client that the capture target // has been permanently lost. void WebContentsDestroyed() override; @@ -109,6 +124,9 @@ class CONTENT_EXPORT WebContentsTracker // This is used to eliminate duplicate callback runs. RenderWidgetHost* last_target_; + // Callback to run when the target RenderWidgetHost has resized. + base::Closure resize_callback_; + DISALLOW_COPY_AND_ASSIGN(WebContentsTracker); }; diff --git a/content/browser/media/capture/web_contents_video_capture_device.cc b/content/browser/media/capture/web_contents_video_capture_device.cc index 8380682fc5064..292d856f8a813 100644 --- a/content/browser/media/capture/web_contents_video_capture_device.cc +++ b/content/browser/media/capture/web_contents_video_capture_device.cc @@ -79,7 +79,6 @@ #include "third_party/skia/include/core/SkBitmap.h" #include "third_party/skia/include/core/SkColor.h" #include "ui/gfx/display.h" -#include "ui/gfx/geometry/size.h" #include "ui/gfx/geometry/size_conversions.h" #include "ui/gfx/screen.h" @@ -87,20 +86,6 @@ namespace content { namespace { -// Compute a letterbox region, aligned to even coordinates. -gfx::Rect ComputeYV12LetterboxRegion(const gfx::Rect& visible_rect, - const gfx::Size& content_size) { - - gfx::Rect result = media::ComputeLetterboxRegion(visible_rect, content_size); - - result.set_x(MakeEven(result.x())); - result.set_y(MakeEven(result.y())); - result.set_width(std::max(kMinFrameWidth, MakeEven(result.width()))); - result.set_height(std::max(kMinFrameHeight, MakeEven(result.height()))); - - return result; -} - void DeleteOnWorkerThread(scoped_ptr render_thread, const base::Closure& callback) { render_thread.reset(); @@ -254,8 +239,12 @@ class WebContentsCaptureMachine : public VideoCaptureMachine { deliver_frame_cb, bool success); - // Remove the old subscription, and start a new one if |rwh| is not NULL. - void RenewFrameSubscription(RenderWidgetHost* rwh); + // Remove the old subscription, and attempt to start a new one if |had_target| + // is true. + void RenewFrameSubscription(bool had_target); + + // Called whenever the render widget is resized. + void UpdateCaptureSize(); // Parameters saved in constructor. const int initial_render_process_id_; @@ -396,7 +385,7 @@ void RenderVideoFrame(const SkBitmap& input, // Calculate the width and height of the content region in the |output|, based // on the aspect ratio of |input|. - gfx::Rect region_in_frame = ComputeYV12LetterboxRegion( + const gfx::Rect region_in_frame = media::ComputeLetterboxRegion( output->visible_rect(), gfx::Size(input.width(), input.height())); // Scale the bitmap to the required size, if necessary. @@ -425,12 +414,20 @@ void RenderVideoFrame(const SkBitmap& input, TRACE_EVENT_ASYNC_STEP_INTO0("gpu.capture", "Capture", output.get(), "YUV"); { - SkAutoLockPixels scaled_bitmap_locker(scaled_bitmap); + // Align to 2x2 pixel boundaries, as required by + // media::CopyRGBToVideoFrame(). + const gfx::Rect region_in_yv12_frame(region_in_frame.x() & ~1, + region_in_frame.y() & ~1, + region_in_frame.width() & ~1, + region_in_frame.height() & ~1); + if (region_in_yv12_frame.IsEmpty()) + return; + SkAutoLockPixels scaled_bitmap_locker(scaled_bitmap); media::CopyRGBToVideoFrame( reinterpret_cast(scaled_bitmap.getPixels()), scaled_bitmap.rowBytes(), - region_in_frame, + region_in_yv12_frame, output.get()); } @@ -501,6 +498,9 @@ bool WebContentsCaptureMachine::Start( // Note: Creation of the first WeakPtr in the following statement will cause // IsStarted() to return true from now on. + tracker_->SetResizeChangeCallback( + base::Bind(&WebContentsCaptureMachine::UpdateCaptureSize, + weak_ptr_factory_.GetWeakPtr())); tracker_->Start(initial_render_process_id_, initial_main_render_frame_id_, base::Bind(&WebContentsCaptureMachine::RenewFrameSubscription, weak_ptr_factory_.GetWeakPtr())); @@ -522,7 +522,7 @@ void WebContentsCaptureMachine::Stop(const base::Closure& callback) { // Note: RenewFrameSubscription() must be called before stopping |tracker_| so // the web_contents() can be notified that the capturing is ending. - RenewFrameSubscription(NULL); + RenewFrameSubscription(false); tracker_->Stop(); // The render thread cannot be stopped on the UI thread, so post a message @@ -551,11 +551,6 @@ void WebContentsCaptureMachine::Capture( } gfx::Size view_size = view->GetViewBounds().size(); - gfx::Size fitted_size; - if (!view_size.IsEmpty()) { - fitted_size = ComputeYV12LetterboxRegion(target->visible_rect(), - view_size).size(); - } if (view_size != last_view_size_) { last_view_size_ = view_size; @@ -574,6 +569,8 @@ void WebContentsCaptureMachine::Capture( weak_ptr_factory_.GetWeakPtr(), start_time, deliver_frame_cb)); } else { + const gfx::Size fitted_size = view_size.IsEmpty() ? gfx::Size() : + media::ComputeLetterboxRegion(target->visible_rect(), view_size).size(); rwh->CopyFromBackingStore( gfx::Rect(), fitted_size, // Size here is a request not always honored. @@ -589,7 +586,10 @@ void WebContentsCaptureMachine::Capture( gfx::Size WebContentsCaptureMachine::ComputeOptimalTargetSize() const { DCHECK_CURRENTLY_ON(BrowserThread::UI); - gfx::Size optimal_size = oracle_proxy_->GetCaptureSize(); + // TODO(miu): Propagate capture frame size changes as new "preferred size" + // updates, rather than just using the max frame size. + // http://crbug.com/350491 + gfx::Size optimal_size = oracle_proxy_->max_frame_size(); // If the ratio between physical and logical pixels is greater than 1:1, // shrink |optimal_size| by that amount. Then, when external code resizes the @@ -657,9 +657,12 @@ void WebContentsCaptureMachine::DidCopyFromCompositingSurfaceToVideoFrame( deliver_frame_cb.Run(start_time, success); } -void WebContentsCaptureMachine::RenewFrameSubscription(RenderWidgetHost* rwh) { +void WebContentsCaptureMachine::RenewFrameSubscription(bool had_target) { DCHECK_CURRENTLY_ON(BrowserThread::UI); + RenderWidgetHost* const rwh = + had_target ? tracker_->GetTargetRenderWidgetHost() : nullptr; + // Always destroy the old subscription before creating a new one. const bool had_subscription = !!subscription_; subscription_.reset(); @@ -688,6 +691,18 @@ void WebContentsCaptureMachine::RenewFrameSubscription(RenderWidgetHost* rwh) { weak_ptr_factory_.GetWeakPtr()))); } +void WebContentsCaptureMachine::UpdateCaptureSize() { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + if (!oracle_proxy_) + return; + RenderWidgetHost* const rwh = tracker_->GetTargetRenderWidgetHost(); + RenderWidgetHostView* const view = rwh ? rwh->GetView() : nullptr; + if (!view) + return; + oracle_proxy_->UpdateCaptureSize(view->GetViewBounds().size()); +} + } // namespace WebContentsVideoCaptureDevice::WebContentsVideoCaptureDevice( diff --git a/content/browser/media/capture/web_contents_video_capture_device_unittest.cc b/content/browser/media/capture/web_contents_video_capture_device_unittest.cc index ab430a7625f31..02b297f35e07f 100644 --- a/content/browser/media/capture/web_contents_video_capture_device_unittest.cc +++ b/content/browser/media/capture/web_contents_video_capture_device_unittest.cc @@ -11,11 +11,13 @@ #include "base/time/time.h" #include "base/timer/timer.h" #include "content/browser/browser_thread_impl.h" +#include "content/browser/frame_host/render_frame_host_impl.h" #include "content/browser/media/capture/video_capture_oracle.h" #include "content/browser/media/capture/web_contents_capture_util.h" #include "content/browser/renderer_host/media/video_capture_buffer_pool.h" #include "content/browser/renderer_host/render_view_host_factory.h" #include "content/browser/renderer_host/render_widget_host_impl.h" +#include "content/browser/web_contents/web_contents_impl.h" #include "content/public/browser/notification_service.h" #include "content/public/browser/notification_types.h" #include "content/public/browser/render_widget_host_view_frame_subscriber.h" @@ -77,7 +79,6 @@ SkColor ConvertRgbToYuv(SkColor rgb) { // counted. class CaptureTestSourceController { public: - CaptureTestSourceController() : color_(SK_ColorMAGENTA), copy_result_size_(kTestWidth, kTestHeight), @@ -162,13 +163,22 @@ class CaptureTestView : public TestRenderWidgetHostView { explicit CaptureTestView(RenderWidgetHostImpl* rwh, CaptureTestSourceController* controller) : TestRenderWidgetHostView(rwh), - controller_(controller) {} + controller_(controller), + fake_bounds_(100, 100, 100 + kTestWidth, 100 + kTestHeight) {} ~CaptureTestView() override {} // TestRenderWidgetHostView overrides. gfx::Rect GetViewBounds() const override { - return gfx::Rect(100, 100, 100 + kTestWidth, 100 + kTestHeight); + return fake_bounds_; + } + + void SetSize(const gfx::Size& size) override { + SetBounds(gfx::Rect(fake_bounds_.origin(), size)); + } + + void SetBounds(const gfx::Rect& rect) override { + fake_bounds_ = rect; } bool CanCopyToVideoFrame() const override { @@ -213,6 +223,7 @@ class CaptureTestView : public TestRenderWidgetHostView { private: scoped_ptr subscriber_; CaptureTestSourceController* const controller_; + gfx::Rect fake_bounds_; DISALLOW_IMPLICIT_CONSTRUCTORS(CaptureTestView); }; @@ -308,9 +319,10 @@ class CaptureTestRenderViewHostFactory : public RenderViewHostFactory { // WebContentsVideoCaptureDevice. class StubClient : public media::VideoCaptureDevice::Client { public: - StubClient(const base::Callback& color_callback, - const base::Closure& error_callback) - : color_callback_(color_callback), + StubClient( + const base::Callback& report_callback, + const base::Closure& error_callback) + : report_callback_(report_callback), error_callback_(error_callback) { buffer_pool_ = new VideoCaptureBufferPool(2); } @@ -360,20 +372,29 @@ class StubClient : public media::VideoCaptureDevice::Client { scoped_ptr buffer, const scoped_refptr& frame, const base::TimeTicks& timestamp) override { - EXPECT_EQ(gfx::Size(kTestWidth, kTestHeight), frame->visible_rect().size()); + EXPECT_FALSE(frame->visible_rect().IsEmpty()); EXPECT_EQ(media::VideoFrame::I420, frame->format()); double frame_rate = 0; EXPECT_TRUE( frame->metadata()->GetDouble(media::VideoFrameMetadata::FRAME_RATE, &frame_rate)); EXPECT_EQ(kTestFramesPerSecond, frame_rate); - uint8 yuv[3]; - for (int plane = 0; plane < 3; ++plane) - yuv[plane] = frame->visible_data(plane)[0]; - // TODO(nick): We just look at the first pixel presently, because if - // the analysis is too slow, the backlog of frames will grow without bound - // and trouble erupts. http://crbug.com/174519 - color_callback_.Run((SkColorSetRGB(yuv[0], yuv[1], yuv[2]))); + + // TODO(miu): We just look at the center pixel presently, because if the + // analysis is too slow, the backlog of frames will grow without bound and + // trouble erupts. http://crbug.com/174519 + using media::VideoFrame; + const gfx::Point center = frame->visible_rect().CenterPoint(); + const int center_offset_y = + (frame->stride(VideoFrame::kYPlane) * center.y()) + center.x(); + const int center_offset_uv = + (frame->stride(VideoFrame::kUPlane) * (center.y() / 2)) + + (center.x() / 2); + report_callback_.Run( + SkColorSetRGB(frame->data(VideoFrame::kYPlane)[center_offset_y], + frame->data(VideoFrame::kUPlane)[center_offset_uv], + frame->data(VideoFrame::kVPlane)[center_offset_uv]), + frame->visible_rect().size()); } void OnError(const std::string& reason) override { error_callback_.Run(); } @@ -407,7 +428,7 @@ class StubClient : public media::VideoCaptureDevice::Client { }; scoped_refptr buffer_pool_; - base::Callback color_callback_; + base::Callback report_callback_; base::Closure error_callback_; DISALLOW_COPY_AND_ASSIGN(StubClient); @@ -417,9 +438,11 @@ class StubClientObserver { public: StubClientObserver() : error_encountered_(false), - wait_color_yuv_(0xcafe1950) { + wait_color_yuv_(0xcafe1950), + wait_size_(kTestWidth, kTestHeight) { client_.reset(new StubClient( - base::Bind(&StubClientObserver::OnColor, base::Unretained(this)), + base::Bind(&StubClientObserver::DidDeliverFrame, + base::Unretained(this)), base::Bind(&StubClientObserver::OnError, base::Unretained(this)))); } @@ -429,16 +452,30 @@ class StubClientObserver { return client_.Pass(); } - void QuitIfConditionMet(SkColor color) { + void QuitIfConditionsMet(SkColor color, const gfx::Size& size) { base::AutoLock guard(lock_); - if (wait_color_yuv_ == color || error_encountered_) + if (error_encountered_) + base::MessageLoop::current()->Quit(); + else if (wait_color_yuv_ == color && wait_size_.IsEmpty()) + base::MessageLoop::current()->Quit(); + else if (wait_color_yuv_ == color && wait_size_ == size) base::MessageLoop::current()->Quit(); } + // Run the current loop until a frame is delivered with the |expected_color| + // and any non-empty frame size. void WaitForNextColor(SkColor expected_color) { + WaitForNextColorAndFrameSize(expected_color, gfx::Size()); + } + + // Run the current loop until a frame is delivered with the |expected_color| + // and is of the |expected_size|. + void WaitForNextColorAndFrameSize(SkColor expected_color, + const gfx::Size& expected_size) { { base::AutoLock guard(lock_); wait_color_yuv_ = ConvertRgbToYuv(expected_color); + wait_size_ = expected_size; error_encountered_ = false; } RunCurrentLoopWithDeadline(); @@ -452,6 +489,7 @@ class StubClientObserver { { base::AutoLock guard(lock_); wait_color_yuv_ = kNotInterested; + wait_size_ = gfx::Size(); error_encountered_ = false; } RunCurrentLoopWithDeadline(); @@ -472,22 +510,25 @@ class StubClientObserver { error_encountered_ = true; } BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, base::Bind( - &StubClientObserver::QuitIfConditionMet, + &StubClientObserver::QuitIfConditionsMet, base::Unretained(this), - kNothingYet)); + kNothingYet, + gfx::Size())); } - void OnColor(SkColor color) { + void DidDeliverFrame(SkColor color, const gfx::Size& size) { BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, base::Bind( - &StubClientObserver::QuitIfConditionMet, + &StubClientObserver::QuitIfConditionsMet, base::Unretained(this), - color)); + color, + size)); } private: base::Lock lock_; bool error_encountered_; SkColor wait_color_yuv_; + gfx::Size wait_size_; scoped_ptr client_; DISALLOW_COPY_AND_ASSIGN(StubClientObserver); @@ -622,6 +663,22 @@ class WebContentsVideoCaptureDeviceTest : public testing::Test { } } + void SimulateSourceSizeChange(const gfx::Size& size) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + CaptureTestView* test_view = static_cast( + web_contents_->GetRenderViewHost()->GetView()); + test_view->SetSize(size); + // Normally, RenderWidgetHostImpl would notify WebContentsImpl that the size + // has changed. However, in this test setup where there is no render + // process, we must notify WebContentsImpl directly. + WebContentsImpl* const as_web_contents_impl = + static_cast(web_contents_.get()); + RenderWidgetHostDelegate* const as_rwh_delegate = + static_cast(as_web_contents_impl); + as_rwh_delegate->RenderWidgetWasResized( + as_web_contents_impl->GetMainFrame()->GetRenderWidgetHost(), true); + } + void DestroyVideoCaptureDevice() { device_.reset(); } StubClientObserver* client_observer() { @@ -879,5 +936,158 @@ TEST_F(WebContentsVideoCaptureDeviceTest, BadFramesGoodFrames) { device()->StopAndDeAllocate(); } +// Tests that, when configured with the FIXED_ASPECT_RATIO resolution change +// policy, the source size changes result in video frames of possibly varying +// resolutions, but all with the same aspect ratio. +TEST_F(WebContentsVideoCaptureDeviceTest, VariableResolution_FixedAspectRatio) { + media::VideoCaptureParams capture_params; + capture_params.requested_format.frame_size.SetSize(kTestWidth, kTestHeight); + capture_params.requested_format.frame_rate = kTestFramesPerSecond; + capture_params.requested_format.pixel_format = media::PIXEL_FORMAT_I420; + capture_params.resolution_change_policy = + media::RESOLUTION_POLICY_FIXED_ASPECT_RATIO; + + device()->AllocateAndStart(capture_params, client_observer()->PassClient()); + + for (int i = 0; i < 6; i++) { + const char* name = NULL; + switch (i % 3) { + case 0: + source()->SetCanCopyToVideoFrame(true); + source()->SetUseFrameSubscriber(false); + name = "VideoFrame"; + break; + case 1: + source()->SetCanCopyToVideoFrame(false); + source()->SetUseFrameSubscriber(true); + name = "Subscriber"; + break; + case 2: + source()->SetCanCopyToVideoFrame(false); + source()->SetUseFrameSubscriber(false); + name = "SkBitmap"; + break; + default: + FAIL(); + } + + SCOPED_TRACE(base::StringPrintf("Using %s path, iteration #%d", name, i)); + + // Source size equals maximum size. Expect delivered frames to be + // kTestWidth by kTestHeight. + source()->SetSolidColor(SK_ColorRED); + SimulateSourceSizeChange(gfx::Size(kTestWidth, kTestHeight)); + SimulateDrawEvent(); + ASSERT_NO_FATAL_FAILURE(client_observer()->WaitForNextColorAndFrameSize( + SK_ColorRED, gfx::Size(kTestWidth, kTestHeight))); + + // Source size is half in both dimensions. Expect delivered frames to be of + // the same aspect ratio as kTestWidth by kTestHeight, but larger than the + // half size because the minimum height is 180 lines. + source()->SetSolidColor(SK_ColorGREEN); + SimulateSourceSizeChange(gfx::Size(kTestWidth / 2, kTestHeight / 2)); + SimulateDrawEvent(); + ASSERT_NO_FATAL_FAILURE(client_observer()->WaitForNextColorAndFrameSize( + SK_ColorGREEN, gfx::Size(180 * kTestWidth / kTestHeight, 180))); + + // Source size changes aspect ratio. Expect delivered frames to be padded + // in the horizontal dimension to preserve aspect ratio. + source()->SetSolidColor(SK_ColorBLUE); + SimulateSourceSizeChange(gfx::Size(kTestWidth / 2, kTestHeight)); + SimulateDrawEvent(); + ASSERT_NO_FATAL_FAILURE(client_observer()->WaitForNextColorAndFrameSize( + SK_ColorBLUE, gfx::Size(kTestWidth, kTestHeight))); + + // Source size changes aspect ratio again. Expect delivered frames to be + // padded in the vertical dimension to preserve aspect ratio. + source()->SetSolidColor(SK_ColorBLACK); + SimulateSourceSizeChange(gfx::Size(kTestWidth, kTestHeight / 2)); + SimulateDrawEvent(); + ASSERT_NO_FATAL_FAILURE(client_observer()->WaitForNextColorAndFrameSize( + SK_ColorBLACK, gfx::Size(kTestWidth, kTestHeight))); + } + + device()->StopAndDeAllocate(); +} + +// Tests that, when configured with the ANY_WITHIN_LIMIT resolution change +// policy, the source size changes result in video frames of possibly varying +// resolutions. +TEST_F(WebContentsVideoCaptureDeviceTest, VariableResolution_AnyWithinLimits) { + media::VideoCaptureParams capture_params; + capture_params.requested_format.frame_size.SetSize(kTestWidth, kTestHeight); + capture_params.requested_format.frame_rate = kTestFramesPerSecond; + capture_params.requested_format.pixel_format = media::PIXEL_FORMAT_I420; + capture_params.resolution_change_policy = + media::RESOLUTION_POLICY_ANY_WITHIN_LIMIT; + + device()->AllocateAndStart(capture_params, client_observer()->PassClient()); + + for (int i = 0; i < 6; i++) { + const char* name = NULL; + switch (i % 3) { + case 0: + source()->SetCanCopyToVideoFrame(true); + source()->SetUseFrameSubscriber(false); + name = "VideoFrame"; + break; + case 1: + source()->SetCanCopyToVideoFrame(false); + source()->SetUseFrameSubscriber(true); + name = "Subscriber"; + break; + case 2: + source()->SetCanCopyToVideoFrame(false); + source()->SetUseFrameSubscriber(false); + name = "SkBitmap"; + break; + default: + FAIL(); + } + + SCOPED_TRACE(base::StringPrintf("Using %s path, iteration #%d", name, i)); + + // Source size equals maximum size. Expect delivered frames to be + // kTestWidth by kTestHeight. + source()->SetSolidColor(SK_ColorRED); + SimulateSourceSizeChange(gfx::Size(kTestWidth, kTestHeight)); + SimulateDrawEvent(); + ASSERT_NO_FATAL_FAILURE(client_observer()->WaitForNextColorAndFrameSize( + SK_ColorRED, gfx::Size(kTestWidth, kTestHeight))); + + // Source size is half in both dimensions. Expect delivered frames to also + // be half in both dimensions. + source()->SetSolidColor(SK_ColorGREEN); + SimulateSourceSizeChange(gfx::Size(kTestWidth / 2, kTestHeight / 2)); + SimulateDrawEvent(); + ASSERT_NO_FATAL_FAILURE(client_observer()->WaitForNextColorAndFrameSize( + SK_ColorGREEN, gfx::Size(kTestWidth / 2, kTestHeight / 2))); + + // Source size changes to something arbitrary. Since the source size is + // less than the maximum size, expect delivered frames to be the same size + // as the source size. + source()->SetSolidColor(SK_ColorBLUE); + gfx::Size arbitrary_source_size(kTestWidth / 2 + 42, kTestHeight - 10); + SimulateSourceSizeChange(arbitrary_source_size); + SimulateDrawEvent(); + ASSERT_NO_FATAL_FAILURE(client_observer()->WaitForNextColorAndFrameSize( + SK_ColorBLUE, arbitrary_source_size)); + + // Source size changes to something arbitrary that exceeds the maximum frame + // size. Since the source size exceeds the maximum size, expect delivered + // frames to be downscaled. + source()->SetSolidColor(SK_ColorBLACK); + arbitrary_source_size = gfx::Size(kTestWidth * 2 + 99, kTestHeight / 2); + SimulateSourceSizeChange(arbitrary_source_size); + SimulateDrawEvent(); + ASSERT_NO_FATAL_FAILURE(client_observer()->WaitForNextColorAndFrameSize( + SK_ColorBLACK, gfx::Size(kTestWidth, + kTestWidth * arbitrary_source_size.height() / + arbitrary_source_size.width()))); + } + + device()->StopAndDeAllocate(); +} + } // namespace } // namespace content diff --git a/content/content_browser.gypi b/content/content_browser.gypi index 6f563ce13dbfe..9ae48ee8dac4d 100644 --- a/content/content_browser.gypi +++ b/content/content_browser.gypi @@ -973,6 +973,8 @@ 'browser/media/capture/animated_content_sampler.h', 'browser/media/capture/audio_mirroring_manager.cc', 'browser/media/capture/audio_mirroring_manager.h', + 'browser/media/capture/capture_resolution_chooser.cc', + 'browser/media/capture/capture_resolution_chooser.h', 'browser/media/capture/content_video_capture_device_core.cc', 'browser/media/capture/content_video_capture_device_core.h', 'browser/media/capture/feedback_signal_accumulator.cc', diff --git a/content/content_tests.gypi b/content/content_tests.gypi index 188141a81479c..86ac1d2512fad 100644 --- a/content/content_tests.gypi +++ b/content/content_tests.gypi @@ -471,6 +471,7 @@ 'browser/media/audio_stream_monitor_unittest.cc', 'browser/media/capture/animated_content_sampler_unittest.cc', 'browser/media/capture/audio_mirroring_manager_unittest.cc', + 'browser/media/capture/capture_resolution_chooser_unittest.cc', 'browser/media/capture/feedback_signal_accumulator_unittest.cc', 'browser/media/capture/smooth_event_sampler_unittest.cc', 'browser/media/capture/video_capture_oracle_unittest.cc', diff --git a/media/base/video_util.cc b/media/base/video_util.cc index d7946743eae09..e04a5a3c54395 100644 --- a/media/base/video_util.cc +++ b/media/base/video_util.cc @@ -270,6 +270,23 @@ void RotatePlaneByPixels( } } +// Common logic for the letterboxing and scale-within/scale-encompassing +// functions. Scales |size| to either fit within or encompass |target|, +// depending on whether |fit_within_target| is true. +static gfx::Size ScaleSizeToTarget(const gfx::Size& size, + const gfx::Size& target, + bool fit_within_target) { + if (size.IsEmpty()) + return gfx::Size(); // Corner case: Aspect ratio is undefined. + + const int64 x = static_cast(size.width()) * target.height(); + const int64 y = static_cast(size.height()) * target.width(); + const bool use_target_width = fit_within_target ? (y < x) : (x < y); + return use_target_width ? + gfx::Size(target.width(), static_cast(y / size.width())) : + gfx::Size(static_cast(x / size.height()), target.height()); +} + gfx::Rect ComputeLetterboxRegion(const gfx::Rect& bounds, const gfx::Size& content) { // If |content| has an undefined aspect ratio, let's not try to divide by @@ -277,19 +294,33 @@ gfx::Rect ComputeLetterboxRegion(const gfx::Rect& bounds, if (content.IsEmpty()) return gfx::Rect(); - int64 x = static_cast(content.width()) * bounds.height(); - int64 y = static_cast(content.height()) * bounds.width(); - - gfx::Size letterbox(bounds.width(), bounds.height()); - if (y < x) - letterbox.set_height(static_cast(y / content.width())); - else - letterbox.set_width(static_cast(x / content.height())); gfx::Rect result = bounds; - result.ClampToCenteredSize(letterbox); + result.ClampToCenteredSize(ScaleSizeToTarget(content, bounds.size(), true)); return result; } +gfx::Size ScaleSizeToFitWithinTarget(const gfx::Size& size, + const gfx::Size& target) { + return ScaleSizeToTarget(size, target, true); +} + +gfx::Size ScaleSizeToEncompassTarget(const gfx::Size& size, + const gfx::Size& target) { + return ScaleSizeToTarget(size, target, false); +} + +gfx::Size PadToMatchAspectRatio(const gfx::Size& size, + const gfx::Size& target) { + if (target.IsEmpty()) + return gfx::Size(); // Aspect ratio is undefined. + + const int64 x = static_cast(size.width()) * target.height(); + const int64 y = static_cast(size.height()) * target.width(); + if (x < y) + return gfx::Size(static_cast(y / target.height()), size.height()); + return gfx::Size(size.width(), static_cast(x / target.width())); +} + void CopyRGBToVideoFrame(const uint8* source, int stride, const gfx::Rect& region_in_frame, diff --git a/media/base/video_util.h b/media/base/video_util.h index 7798ebe016e1a..abbcad41abf56 100644 --- a/media/base/video_util.h +++ b/media/base/video_util.h @@ -84,6 +84,29 @@ MEDIA_EXPORT void RotatePlaneByPixels( MEDIA_EXPORT gfx::Rect ComputeLetterboxRegion(const gfx::Rect& bounds, const gfx::Size& content); +// Return a scaled |size| whose area is less than or equal to |target|, where +// one of its dimensions is equal to |target|'s. The aspect ratio of |size| is +// preserved as closely as possible. If |size| is empty, the result will be +// empty. +MEDIA_EXPORT gfx::Size ScaleSizeToFitWithinTarget(const gfx::Size& size, + const gfx::Size& target); + +// Return a scaled |size| whose area is greater than or equal to |target|, where +// one of its dimensions is equal to |target|'s. The aspect ratio of |size| is +// preserved as closely as possible. If |size| is empty, the result will be +// empty. +MEDIA_EXPORT gfx::Size ScaleSizeToEncompassTarget(const gfx::Size& size, + const gfx::Size& target); + +// Returns |size| with only one of its dimensions increased such that the result +// matches the aspect ratio of |target|. This is different from +// ScaleSizeToEncompassTarget() in two ways: 1) The goal is to match the aspect +// ratio of |target| rather than that of |size|. 2) Only one of the dimensions +// of |size| may change, and it may only be increased (padded). If either +// |size| or |target| is empty, the result will be empty. +MEDIA_EXPORT gfx::Size PadToMatchAspectRatio(const gfx::Size& size, + const gfx::Size& target); + // Copy an RGB bitmap into the specified |region_in_frame| of a YUV video frame. // Fills the regions outside |region_in_frame| with black. MEDIA_EXPORT void CopyRGBToVideoFrame(const uint8* source, diff --git a/media/base/video_util_unittest.cc b/media/base/video_util_unittest.cc index 9ac13c1f61098..79c53159ab8b6 100644 --- a/media/base/video_util_unittest.cc +++ b/media/base/video_util_unittest.cc @@ -328,6 +328,8 @@ TEST_P(VideoUtilRotationTest, Rotate) { INSTANTIATE_TEST_CASE_P(, VideoUtilRotationTest, testing::ValuesIn(kVideoRotationTestData)); +// Tests the ComputeLetterboxRegion function. Also, because of shared code +// internally, this also tests ScaleSizeToFitWithinTarget(). TEST_F(VideoUtilTest, ComputeLetterboxRegion) { EXPECT_EQ(gfx::Rect(167, 0, 666, 500), ComputeLetterboxRegion(gfx::Rect(0, 0, 1000, 500), @@ -348,6 +350,48 @@ TEST_F(VideoUtilTest, ComputeLetterboxRegion) { gfx::Size(0, 0)).IsEmpty()); } +TEST_F(VideoUtilTest, ScaleSizeToEncompassTarget) { + EXPECT_EQ(gfx::Size(1000, 750), + ScaleSizeToEncompassTarget(gfx::Size(640, 480), + gfx::Size(1000, 500))); + EXPECT_EQ(gfx::Size(1333, 1000), + ScaleSizeToEncompassTarget(gfx::Size(640, 480), + gfx::Size(500, 1000))); + EXPECT_EQ(gfx::Size(1000, 562), + ScaleSizeToEncompassTarget(gfx::Size(1920, 1080), + gfx::Size(1000, 500))); + EXPECT_EQ(gfx::Size(133, 100), + ScaleSizeToEncompassTarget(gfx::Size(400, 300), + gfx::Size(100, 100))); + EXPECT_EQ(gfx::Size(2666666666, 2000000000), + ScaleSizeToEncompassTarget(gfx::Size(40000, 30000), + gfx::Size(2000000000, 2000000000))); + EXPECT_TRUE(ScaleSizeToEncompassTarget( + gfx::Size(0, 0), gfx::Size(2000000000, 2000000000)).IsEmpty()); +} + +TEST_F(VideoUtilTest, PadToMatchAspectRatio) { + EXPECT_EQ(gfx::Size(640, 480), + PadToMatchAspectRatio(gfx::Size(640, 480), gfx::Size(640, 480))); + EXPECT_EQ(gfx::Size(640, 480), + PadToMatchAspectRatio(gfx::Size(640, 480), gfx::Size(4, 3))); + EXPECT_EQ(gfx::Size(960, 480), + PadToMatchAspectRatio(gfx::Size(640, 480), gfx::Size(1000, 500))); + EXPECT_EQ(gfx::Size(640, 1280), + PadToMatchAspectRatio(gfx::Size(640, 480), gfx::Size(500, 1000))); + EXPECT_EQ(gfx::Size(2160, 1080), + PadToMatchAspectRatio(gfx::Size(1920, 1080), gfx::Size(1000, 500))); + EXPECT_EQ(gfx::Size(400, 400), + PadToMatchAspectRatio(gfx::Size(400, 300), gfx::Size(100, 100))); + EXPECT_EQ(gfx::Size(400, 400), + PadToMatchAspectRatio(gfx::Size(300, 400), gfx::Size(100, 100))); + EXPECT_EQ(gfx::Size(40000, 40000), + PadToMatchAspectRatio(gfx::Size(40000, 30000), + gfx::Size(2000000000, 2000000000))); + EXPECT_TRUE(PadToMatchAspectRatio( + gfx::Size(40000, 30000), gfx::Size(0, 0)).IsEmpty()); +} + TEST_F(VideoUtilTest, LetterboxYUV) { int width = 40; int height = 30;