blob: ab594361a2bdf7b92287bdf2e3ee2aa0b2c34be8 [file] [log] [blame]
// Copyright 2018 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/lame_window_capturer_chromeos.h"
#include <algorithm>
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/location.h"
#include "components/viz/common/frame_sinks/copy_output_request.h"
#include "components/viz/common/frame_sinks/copy_output_result.h"
#include "media/base/limits.h"
#include "media/base/video_util.h"
#include "mojo/public/cpp/base/shared_memory_utils.h"
#include "mojo/public/cpp/bindings/strong_binding.h"
#include "ui/gfx/geometry/rect.h"
using media::VideoFrame;
using media::VideoFrameMetadata;
namespace content {
namespace {
// Returns |raw_size| with width and height truncated to even-numbered values.
gfx::Size AdjustSizeForI420Format(const gfx::Size& raw_size) {
return gfx::Size(raw_size.width() & ~1, raw_size.height() & ~1);
}
} // namespace
// static
constexpr base::TimeDelta LameWindowCapturerChromeOS::kAbsoluteMinCapturePeriod;
LameWindowCapturerChromeOS::LameWindowCapturerChromeOS(aura::Window* target)
: target_(target),
copy_request_source_(base::UnguessableToken::Create()),
weak_factory_(this) {
if (target_) {
target_->AddObserver(this);
}
}
LameWindowCapturerChromeOS::~LameWindowCapturerChromeOS() {
if (target_) {
target_->RemoveObserver(this);
}
}
void LameWindowCapturerChromeOS::SetFormat(media::VideoPixelFormat format,
const gfx::ColorSpace& color_space) {
if (format != media::PIXEL_FORMAT_I420) {
LOG(DFATAL) << "Invalid pixel format: Only I420 is supported.";
}
if (color_space.IsValid() && color_space != gfx::ColorSpace::CreateREC709()) {
LOG(DFATAL) << "Unsupported color space: Only BT.709 is supported.";
}
}
void LameWindowCapturerChromeOS::SetMinCapturePeriod(
base::TimeDelta min_capture_period) {
capture_period_ = std::max(min_capture_period, kAbsoluteMinCapturePeriod);
// If the capture period is being changed while the timer is already running,
// re-start with the new capture period.
if (timer_.IsRunning()) {
timer_.Start(FROM_HERE, capture_period_, this,
&LameWindowCapturerChromeOS::CaptureNextFrame);
}
}
void LameWindowCapturerChromeOS::SetMinSizeChangePeriod(
base::TimeDelta min_period) {}
void LameWindowCapturerChromeOS::SetResolutionConstraints(
const gfx::Size& min_size,
const gfx::Size& max_size,
bool use_fixed_aspect_ratio) {
if (max_size.width() <= 1 || max_size.height() <= 1 ||
max_size.width() > media::limits::kMaxDimension ||
max_size.height() > media::limits::kMaxDimension) {
LOG(DFATAL) << "Invalid max_size (" << max_size.ToString()
<< "): It must be within media::limits.";
return;
}
// Set the capture size to the max size, adjusted for the I420 format.
capture_size_ = AdjustSizeForI420Format(max_size);
DCHECK(!capture_size_.IsEmpty());
// Cancel any in-flight captures that would be using the old size and clear
// the buffer pool.
weak_factory_.InvalidateWeakPtrs();
buffer_pool_.clear();
in_flight_count_ = 0;
}
void LameWindowCapturerChromeOS::SetAutoThrottlingEnabled(bool enabled) {
NOTIMPLEMENTED();
}
void LameWindowCapturerChromeOS::ChangeTarget(
const base::Optional<viz::FrameSinkId>& frame_sink_id) {
// The LameWindowCapturerChromeOS does not capture from compositor frame
// sinks.
}
void LameWindowCapturerChromeOS::Start(
viz::mojom::FrameSinkVideoConsumerPtr consumer) {
DCHECK(consumer);
Stop();
consumer_ = std::move(consumer);
// In the future, if the connection to the consumer is lost before a call to
// Stop(), make that call on its behalf.
consumer_.set_connection_error_handler(base::BindOnce(
&LameWindowCapturerChromeOS::Stop, base::Unretained(this)));
timer_.Start(FROM_HERE, capture_period_, this,
&LameWindowCapturerChromeOS::CaptureNextFrame);
}
void LameWindowCapturerChromeOS::Stop() {
// Stop the timer, cancel any in-flight frames, and clear the buffer pool.
timer_.Stop();
weak_factory_.InvalidateWeakPtrs();
buffer_pool_.clear();
in_flight_count_ = 0;
if (consumer_) {
consumer_->OnStopped();
consumer_.reset();
}
}
void LameWindowCapturerChromeOS::RequestRefreshFrame() {
// This is ignored because the LameWindowCapturerChromeOS captures frames
// continuously.
}
void LameWindowCapturerChromeOS::CreateOverlay(
int32_t stacking_index,
viz::mojom::FrameSinkVideoCaptureOverlayRequest request) {
// LameWindowCapturerChromeOS only supports one overlay at a time. If one
// already exists, the following will cause it to be dropped.
overlay_ =
std::make_unique<LameCaptureOverlayChromeOS>(this, std::move(request));
}
class LameWindowCapturerChromeOS::InFlightFrame
: public viz::mojom::FrameSinkVideoConsumerFrameCallbacks {
public:
InFlightFrame(base::WeakPtr<LameWindowCapturerChromeOS> capturer,
base::MappedReadOnlyRegion buffer)
: capturer_(std::move(capturer)), buffer_(std::move(buffer)) {}
~InFlightFrame() final { Done(); }
base::ReadOnlySharedMemoryRegion CloneBufferHandle() {
return buffer_.region.Duplicate();
}
VideoFrame* video_frame() const { return video_frame_.get(); }
void set_video_frame(scoped_refptr<VideoFrame> frame) {
video_frame_ = std::move(frame);
}
const gfx::Rect& content_rect() const { return content_rect_; }
void set_content_rect(const gfx::Rect& rect) { content_rect_ = rect; }
void set_overlay_renderer(LameCaptureOverlayChromeOS::OnceRenderer renderer) {
overlay_renderer_ = std::move(renderer);
}
void RenderOptionalOverlay() {
if (overlay_renderer_) {
std::move(overlay_renderer_).Run(video_frame_.get());
}
}
void Done() final {
video_frame_ = nullptr;
if (auto* capturer = capturer_.get()) {
DCHECK_GT(capturer->in_flight_count_, 0);
--capturer->in_flight_count_;
// If the capture size hasn't changed, return the buffer to the pool.
if (buffer_.mapping.size() ==
VideoFrame::AllocationSize(media::PIXEL_FORMAT_I420,
capturer->capture_size_)) {
capturer->buffer_pool_.emplace_back(std::move(buffer_));
}
capturer_ = nullptr;
}
buffer_ = base::MappedReadOnlyRegion();
}
void ProvideFeedback(double utilization) final {}
private:
base::WeakPtr<LameWindowCapturerChromeOS> capturer_;
base::MappedReadOnlyRegion buffer_;
scoped_refptr<VideoFrame> video_frame_;
gfx::Rect content_rect_;
LameCaptureOverlayChromeOS::OnceRenderer overlay_renderer_;
DISALLOW_COPY_AND_ASSIGN(InFlightFrame);
};
void LameWindowCapturerChromeOS::OnOverlayConnectionLost(
LameCaptureOverlayChromeOS* overlay) {
if (overlay_.get() == overlay) {
overlay_.reset();
}
}
void LameWindowCapturerChromeOS::CaptureNextFrame() {
// If the maximum frame in-flight count has been reached, skip this frame.
if (in_flight_count_ >= kMaxFramesInFlight) {
return;
}
// Attempt to re-use a buffer from the pool. Otherwise, create a new one.
const size_t allocation_size =
VideoFrame::AllocationSize(media::PIXEL_FORMAT_I420, capture_size_);
base::MappedReadOnlyRegion buffer;
if (buffer_pool_.empty()) {
buffer = mojo::CreateReadOnlySharedMemoryRegion(allocation_size);
if (!buffer.IsValid()) {
// If the shared memory region creation failed, just abort this frame,
// hoping the issue is a transient one (e.g., lack of an available region
// in the address space).
return;
}
} else {
buffer = std::move(buffer_pool_.back());
buffer_pool_.pop_back();
DCHECK(buffer.IsValid());
DCHECK_EQ(buffer.mapping.size(), allocation_size);
}
void* const backing_memory = buffer.mapping.memory();
// At this point, frame capture will proceed. Create an InFlightFrame to track
// population and consumption of the frame, and to eventually return the
// buffer to the pool and decrement |in_flight_count_|.
++in_flight_count_;
auto in_flight_frame = std::make_unique<InFlightFrame>(
weak_factory_.GetWeakPtr(), std::move(buffer));
// Create a VideoFrame that wraps the mapped buffer.
const base::TimeTicks begin_time = base::TimeTicks::Now();
if (first_frame_reference_time_.is_null()) {
first_frame_reference_time_ = begin_time;
}
in_flight_frame->set_video_frame(VideoFrame::WrapExternalData(
media::PIXEL_FORMAT_I420, capture_size_, gfx::Rect(capture_size_),
capture_size_, static_cast<uint8_t*>(backing_memory), allocation_size,
begin_time - first_frame_reference_time_));
auto* const frame = in_flight_frame->video_frame();
DCHECK(frame);
VideoFrameMetadata* const metadata = frame->metadata();
metadata->SetTimeTicks(VideoFrameMetadata::CAPTURE_BEGIN_TIME, begin_time);
metadata->SetTimeDelta(VideoFrameMetadata::FRAME_DURATION, capture_period_);
metadata->SetDouble(VideoFrameMetadata::FRAME_RATE,
1.0 / capture_period_.InSecondsF());
metadata->SetTimeTicks(VideoFrameMetadata::REFERENCE_TIME, begin_time);
frame->set_color_space(gfx::ColorSpace::CreateREC709());
// Compute the region of the VideoFrame that will contain the content. If
// there is nothing to copy from/to (e.g., the target is gone, or is sized too
// small), send a blank black frame immediately.
const gfx::Size source_size =
target_ ? target_->bounds().size() : gfx::Size();
const gfx::Rect content_rect = source_size.IsEmpty()
? gfx::Rect()
: media::ComputeLetterboxRegionForI420(
frame->visible_rect(), source_size);
in_flight_frame->set_content_rect(content_rect);
if (content_rect.IsEmpty()) {
media::LetterboxVideoFrame(frame, gfx::Rect());
DeliverFrame(std::move(in_flight_frame));
return;
}
DCHECK(target_);
if (overlay_) {
in_flight_frame->set_overlay_renderer(overlay_->MakeRenderer(content_rect));
}
// Request a copy of the Layer associated with the |target_| aura::Window.
auto request = std::make_unique<viz::CopyOutputRequest>(
// Note: As of this writing, I420_PLANES is not supported external to VIZ.
viz::CopyOutputRequest::ResultFormat::RGBA_BITMAP,
base::BindOnce(&LameWindowCapturerChromeOS::DidCopyFrame,
weak_factory_.GetWeakPtr(), std::move(in_flight_frame)));
request->set_source(copy_request_source_);
request->set_area(gfx::Rect(source_size));
request->SetScaleRatio(
gfx::Vector2d(source_size.width(), source_size.height()),
gfx::Vector2d(content_rect.width(), content_rect.height()));
request->set_result_selection(gfx::Rect(content_rect.size()));
target_->layer()->RequestCopyOfOutput(std::move(request));
}
void LameWindowCapturerChromeOS::DidCopyFrame(
std::unique_ptr<InFlightFrame> in_flight_frame,
std::unique_ptr<viz::CopyOutputResult> result) {
// Populate the VideoFrame from the CopyOutputResult.
auto* const frame = in_flight_frame->video_frame();
DCHECK(frame);
const auto& content_rect = in_flight_frame->content_rect();
const int y_stride = frame->stride(VideoFrame::kYPlane);
uint8_t* const y = frame->visible_data(VideoFrame::kYPlane) +
content_rect.y() * y_stride + content_rect.x();
const int u_stride = frame->stride(VideoFrame::kUPlane);
uint8_t* const u = frame->visible_data(VideoFrame::kUPlane) +
(content_rect.y() / 2) * u_stride + (content_rect.x() / 2);
const int v_stride = frame->stride(VideoFrame::kVPlane);
uint8_t* const v = frame->visible_data(VideoFrame::kVPlane) +
(content_rect.y() / 2) * v_stride + (content_rect.x() / 2);
if (!result->ReadI420Planes(y, y_stride, u, u_stride, v, v_stride)) {
return; // Copy request failed, punt.
}
in_flight_frame->RenderOptionalOverlay();
// The result may be smaller than what was requested, if unforeseen clamping
// to the source boundaries occurred by the executor of the copy request.
// However, the result should never contain more than what was requested.
DCHECK_LE(result->size().width(), content_rect.width());
DCHECK_LE(result->size().height(), content_rect.height());
media::LetterboxVideoFrame(
frame, gfx::Rect(content_rect.origin(),
AdjustSizeForI420Format(result->size())));
DeliverFrame(std::move(in_flight_frame));
}
void LameWindowCapturerChromeOS::DeliverFrame(
std::unique_ptr<InFlightFrame> in_flight_frame) {
auto* const frame = in_flight_frame->video_frame();
DCHECK(frame);
frame->metadata()->SetTimeTicks(VideoFrameMetadata::CAPTURE_END_TIME,
base::TimeTicks::Now());
// Clone the buffer handle for the consumer.
base::ReadOnlySharedMemoryRegion handle =
in_flight_frame->CloneBufferHandle();
if (!handle.IsValid()) {
return; // This should only fail if the OS is exhausted of handles.
}
// Assemble frame layout, format, and metadata into a mojo struct to send to
// the consumer.
media::mojom::VideoFrameInfoPtr info = media::mojom::VideoFrameInfo::New();
info->timestamp = frame->timestamp();
info->metadata = frame->metadata()->GetInternalValues().Clone();
info->pixel_format = frame->format();
info->coded_size = frame->coded_size();
info->visible_rect = frame->visible_rect();
DCHECK(frame->ColorSpace().IsValid()); // Ensure it was set by this point.
info->color_space = frame->ColorSpace();
const gfx::Rect update_rect = frame->visible_rect();
const gfx::Rect content_rect = in_flight_frame->content_rect();
// Create a mojo message pipe and bind to the InFlightFrame to wait for the
// Done() signal from the consumer. The mojo::StrongBinding takes ownership of
// the InFlightFrame.
viz::mojom::FrameSinkVideoConsumerFrameCallbacksPtr callbacks;
mojo::MakeStrongBinding(std::move(in_flight_frame),
mojo::MakeRequest(&callbacks));
// Send the frame to the consumer.
consumer_->OnFrameCaptured(std::move(handle), std::move(info), update_rect,
content_rect, std::move(callbacks));
}
void LameWindowCapturerChromeOS::OnWindowDestroying(aura::Window* window) {
if (window == target_) {
target_->RemoveObserver(this);
target_ = nullptr;
// The capturer may continue running, but it will notice the target is gone
// and produce blank black frames hereafter.
}
}
} // namespace content