blob: 66528e579e5b89d905ae38966eef54c3852a2732 [file] [log] [blame]
dgozman1137e622017-04-17 19:49:121// Copyright 2014 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "chrome/browser/devtools/devtools_eye_dropper.h"
6
Yuri Wiitalafc5fe702018-06-20 06:14:007#include <utility>
8
dgozman1137e622017-04-17 19:49:129#include "base/bind.h"
Yuri Wiitala4a74fb02018-08-29 06:09:3510#include "base/memory/shared_memory_mapping.h"
dgozman1137e622017-04-17 19:49:1211#include "build/build_config.h"
Saman Sami2acabd12018-03-10 00:51:0912#include "cc/paint/skia_paint_canvas.h"
13#include "components/viz/common/features.h"
dgozman1137e622017-04-17 19:49:1214#include "content/public/browser/render_view_host.h"
15#include "content/public/browser/render_widget_host.h"
16#include "content/public/browser/render_widget_host_view.h"
17#include "content/public/browser/web_contents.h"
Saman Samid81a0e642018-03-19 04:51:1518#include "content/public/common/content_features.h"
dgozman1137e622017-04-17 19:49:1219#include "content/public/common/cursor_info.h"
20#include "content/public/common/screen_info.h"
Saman Sami2acabd12018-03-10 00:51:0921#include "media/base/limits.h"
Yuri Wiitala4a74fb02018-08-29 06:09:3522#include "media/base/video_frame.h"
Takuto Ikuta0ee5934a2019-01-31 05:49:3823#include "media/capture/mojom/video_capture_types.mojom.h"
Blink Reformata30d4232018-04-07 15:31:0624#include "third_party/blink/public/platform/web_input_event.h"
25#include "third_party/blink/public/platform/web_mouse_event.h"
dgozman1137e622017-04-17 19:49:1226#include "third_party/skia/include/core/SkCanvas.h"
27#include "third_party/skia/include/core/SkPaint.h"
28#include "third_party/skia/include/core/SkPath.h"
Mike Klein7008dab2018-09-28 21:58:5129#include "third_party/skia/include/core/SkPixmap.h"
dgozman1137e622017-04-17 19:49:1230#include "ui/gfx/geometry/size_conversions.h"
31
32DevToolsEyeDropper::DevToolsEyeDropper(content::WebContents* web_contents,
33 EyeDropperCallback callback)
34 : content::WebContentsObserver(web_contents),
35 callback_(callback),
36 last_cursor_x_(-1),
37 last_cursor_y_(-1),
38 host_(nullptr),
39 weak_factory_(this) {
40 mouse_event_callback_ =
41 base::Bind(&DevToolsEyeDropper::HandleMouseEvent, base::Unretained(this));
42 content::RenderViewHost* rvh = web_contents->GetRenderViewHost();
Saman Sami5cc093d2018-08-08 12:12:2543 if (rvh)
dgozman1137e622017-04-17 19:49:1244 AttachToHost(rvh->GetWidget());
dgozman1137e622017-04-17 19:49:1245}
46
47DevToolsEyeDropper::~DevToolsEyeDropper() {
48 DetachFromHost();
49}
50
51void DevToolsEyeDropper::AttachToHost(content::RenderWidgetHost* host) {
52 host_ = host;
53 host_->AddMouseEventCallback(mouse_event_callback_);
Saman Sami2acabd12018-03-10 00:51:0954
Saman Samie1e85762018-06-01 18:38:3355 // The view can be null if the renderer process has crashed.
56 // (https://crbug.com/847363)
57 if (!host_->GetView())
58 return;
59
Saman Sami2acabd12018-03-10 00:51:0960 // Capturing a full-page screenshot can be costly so we shouldn't do it too
61 // often. We can capture at a lower frame rate without hurting the user
62 // experience.
63 constexpr static int kMaxFrameRate = 15;
64
65 // Create and configure the video capturer.
66 video_capturer_ = host_->GetView()->CreateVideoCapturer();
67 video_capturer_->SetResolutionConstraints(
Saman Sami493222f2018-04-05 03:41:1768 host_->GetView()->GetViewBounds().size(),
69 host_->GetView()->GetViewBounds().size(), true);
Saman Sami561d7742018-03-17 02:22:3270 video_capturer_->SetAutoThrottlingEnabled(false);
71 video_capturer_->SetMinSizeChangePeriod(base::TimeDelta());
Saman Sami00d35442018-03-20 20:22:3972 video_capturer_->SetFormat(media::PIXEL_FORMAT_ARGB,
Fredrik Hubinette088f6dc2018-10-04 19:42:3073 gfx::ColorSpace::CreateREC709());
Saman Sami2acabd12018-03-10 00:51:0974 video_capturer_->SetMinCapturePeriod(base::TimeDelta::FromSeconds(1) /
75 kMaxFrameRate);
Saman Samid2dc25f2018-05-24 20:41:0776 video_capturer_->Start(this);
dgozman1137e622017-04-17 19:49:1277}
78
79void DevToolsEyeDropper::DetachFromHost() {
80 if (!host_)
81 return;
82 host_->RemoveMouseEventCallback(mouse_event_callback_);
83 content::CursorInfo cursor_info;
84 cursor_info.type = blink::WebCursorInfo::kTypePointer;
85 host_->SetCursor(cursor_info);
Saman Sami2acabd12018-03-10 00:51:0986 video_capturer_.reset();
dgozman1137e622017-04-17 19:49:1287 host_ = nullptr;
88}
89
90void DevToolsEyeDropper::RenderViewCreated(content::RenderViewHost* host) {
Saman Sami5cc093d2018-08-08 12:12:2591 if (!host_)
dgozman1137e622017-04-17 19:49:1292 AttachToHost(host->GetWidget());
dgozman1137e622017-04-17 19:49:1293}
94
95void DevToolsEyeDropper::RenderViewDeleted(content::RenderViewHost* host) {
96 if (host->GetWidget() == host_) {
97 DetachFromHost();
98 ResetFrame();
99 }
100}
101
102void DevToolsEyeDropper::RenderViewHostChanged(
103 content::RenderViewHost* old_host,
104 content::RenderViewHost* new_host) {
105 if ((old_host && old_host->GetWidget() == host_) || (!old_host && !host_)) {
106 DetachFromHost();
107 AttachToHost(new_host->GetWidget());
dgozman1137e622017-04-17 19:49:12108 }
109}
110
dgozman1137e622017-04-17 19:49:12111void DevToolsEyeDropper::ResetFrame() {
112 frame_.reset();
113 last_cursor_x_ = -1;
114 last_cursor_y_ = -1;
115}
116
dgozman1137e622017-04-17 19:49:12117bool DevToolsEyeDropper::HandleMouseEvent(const blink::WebMouseEvent& event) {
118 last_cursor_x_ = event.PositionInWidget().x;
119 last_cursor_y_ = event.PositionInWidget().y;
120 if (frame_.drawsNothing())
121 return true;
122
123 if (event.button == blink::WebMouseEvent::Button::kLeft &&
124 (event.GetType() == blink::WebInputEvent::kMouseDown ||
125 event.GetType() == blink::WebInputEvent::kMouseMove)) {
126 if (last_cursor_x_ < 0 || last_cursor_x_ >= frame_.width() ||
127 last_cursor_y_ < 0 || last_cursor_y_ >= frame_.height()) {
128 return true;
129 }
130
dgozman1137e622017-04-17 19:49:12131 SkColor sk_color = frame_.getColor(last_cursor_x_, last_cursor_y_);
Christopher Cameron95fb5ee62017-08-23 12:50:58132
Mike Klein7008dab2018-09-28 21:58:51133 // The picked colors are expected to be sRGB. Convert from |frame_|'s color
134 // space to sRGB.
Mike Klein7008dab2018-09-28 21:58:51135 SkPixmap pm(
136 SkImageInfo::Make(1, 1, kBGRA_8888_SkColorType, kUnpremul_SkAlphaType,
Yuri Wiitalaed44fc42018-12-15 03:14:24137 frame_.refColorSpace()),
Mike Klein7008dab2018-09-28 21:58:51138 &sk_color, sizeof(sk_color));
Mike Klein7008dab2018-09-28 21:58:51139 uint8_t rgba_color[4];
140 bool ok = pm.readPixels(
141 SkImageInfo::Make(1, 1, kRGBA_8888_SkColorType, kUnpremul_SkAlphaType,
142 SkColorSpace::MakeSRGB()),
143 rgba_color, sizeof(rgba_color));
144 DCHECK(ok);
Christopher Cameron95fb5ee62017-08-23 12:50:58145
146 callback_.Run(rgba_color[0], rgba_color[1], rgba_color[2], rgba_color[3]);
dgozman1137e622017-04-17 19:49:12147 }
148 UpdateCursor();
149 return true;
150}
151
152void DevToolsEyeDropper::UpdateCursor() {
153 if (!host_ || frame_.drawsNothing())
154 return;
155
156 if (last_cursor_x_ < 0 || last_cursor_x_ >= frame_.width() ||
157 last_cursor_y_ < 0 || last_cursor_y_ >= frame_.height()) {
158 return;
159 }
160
161// Due to platform limitations, we are using two different cursors
162// depending on the platform. Mac and Win have large cursors with two circles
163// for original spot and its magnified projection; Linux gets smaller (64 px)
164// magnified projection only with centered hotspot.
165// Mac Retina requires cursor to be > 120px in order to render smoothly.
166
167#if defined(OS_LINUX)
168 const float kCursorSize = 63;
169 const float kDiameter = 63;
170 const float kHotspotOffset = 32;
171 const float kHotspotRadius = 0;
172 const float kPixelSize = 9;
173#else
174 const float kCursorSize = 150;
175 const float kDiameter = 110;
176 const float kHotspotOffset = 25;
177 const float kHotspotRadius = 5;
178 const float kPixelSize = 10;
179#endif
180
181 content::ScreenInfo screen_info;
182 host_->GetScreenInfo(&screen_info);
183 double device_scale_factor = screen_info.device_scale_factor;
184
185 SkBitmap result;
186 result.allocN32Pixels(kCursorSize * device_scale_factor,
187 kCursorSize * device_scale_factor);
188 result.eraseARGB(0, 0, 0, 0);
189
190 SkCanvas canvas(result);
191 canvas.scale(device_scale_factor, device_scale_factor);
192 canvas.translate(0.5f, 0.5f);
193
194 SkPaint paint;
195
196 // Paint original spot with cross.
197 if (kHotspotRadius > 0) {
198 paint.setStrokeWidth(1);
199 paint.setAntiAlias(false);
200 paint.setColor(SK_ColorDKGRAY);
201 paint.setStyle(SkPaint::kStroke_Style);
202
203 canvas.drawLine(kHotspotOffset, kHotspotOffset - 2 * kHotspotRadius,
204 kHotspotOffset, kHotspotOffset - kHotspotRadius, paint);
205 canvas.drawLine(kHotspotOffset, kHotspotOffset + kHotspotRadius,
206 kHotspotOffset, kHotspotOffset + 2 * kHotspotRadius, paint);
207 canvas.drawLine(kHotspotOffset - 2 * kHotspotRadius, kHotspotOffset,
208 kHotspotOffset - kHotspotRadius, kHotspotOffset, paint);
209 canvas.drawLine(kHotspotOffset + kHotspotRadius, kHotspotOffset,
210 kHotspotOffset + 2 * kHotspotRadius, kHotspotOffset, paint);
211
212 paint.setStrokeWidth(2);
213 paint.setAntiAlias(true);
214 canvas.drawCircle(kHotspotOffset, kHotspotOffset, kHotspotRadius, paint);
215 }
216
217 // Clip circle for magnified projection.
218 float padding = (kCursorSize - kDiameter) / 2;
219 SkPath clip_path;
220 clip_path.addOval(SkRect::MakeXYWH(padding, padding, kDiameter, kDiameter));
221 clip_path.close();
222 canvas.clipPath(clip_path, SkClipOp::kIntersect, true);
223
224 // Project pixels.
225 int pixel_count = kDiameter / kPixelSize;
226 SkRect src_rect = SkRect::MakeXYWH(last_cursor_x_ - pixel_count / 2,
227 last_cursor_y_ - pixel_count / 2,
228 pixel_count, pixel_count);
229 SkRect dst_rect = SkRect::MakeXYWH(padding, padding, kDiameter, kDiameter);
230 canvas.drawBitmapRect(frame_, src_rect, dst_rect, NULL);
231
232 // Paint grid.
233 paint.setStrokeWidth(1);
234 paint.setAntiAlias(false);
235 paint.setColor(SK_ColorGRAY);
236 for (int i = 0; i < pixel_count; ++i) {
237 canvas.drawLine(padding + i * kPixelSize, padding, padding + i * kPixelSize,
238 kCursorSize - padding, paint);
239 canvas.drawLine(padding, padding + i * kPixelSize, kCursorSize - padding,
240 padding + i * kPixelSize, paint);
241 }
242
243 // Paint central pixel in red.
244 SkRect pixel =
245 SkRect::MakeXYWH((kCursorSize - kPixelSize) / 2,
246 (kCursorSize - kPixelSize) / 2, kPixelSize, kPixelSize);
247 paint.setColor(SK_ColorRED);
248 paint.setStyle(SkPaint::kStroke_Style);
249 canvas.drawRect(pixel, paint);
250
251 // Paint outline.
252 paint.setStrokeWidth(2);
253 paint.setColor(SK_ColorDKGRAY);
254 paint.setAntiAlias(true);
255 canvas.drawCircle(kCursorSize / 2, kCursorSize / 2, kDiameter / 2, paint);
256
257 content::CursorInfo cursor_info;
258 cursor_info.type = blink::WebCursorInfo::kTypeCustom;
259 cursor_info.image_scale_factor = device_scale_factor;
260 cursor_info.custom_image = result;
261 cursor_info.hotspot = gfx::Point(kHotspotOffset * device_scale_factor,
262 kHotspotOffset * device_scale_factor);
263 host_->SetCursor(cursor_info);
264}
Saman Sami2acabd12018-03-10 00:51:09265
266void DevToolsEyeDropper::OnFrameCaptured(
Yuri Wiitala4a74fb02018-08-29 06:09:35267 base::ReadOnlySharedMemoryRegion data,
Saman Sami2acabd12018-03-10 00:51:09268 ::media::mojom::VideoFrameInfoPtr info,
Saman Sami2acabd12018-03-10 00:51:09269 const gfx::Rect& content_rect,
270 viz::mojom::FrameSinkVideoConsumerFrameCallbacksPtr callbacks) {
Saman Sami493222f2018-04-05 03:41:17271 gfx::Size view_size = host_->GetView()->GetViewBounds().size();
272 if (view_size != content_rect.size()) {
273 video_capturer_->SetResolutionConstraints(view_size, view_size, true);
274 video_capturer_->RequestRefreshFrame();
275 return;
276 }
277
Yuri Wiitala4a74fb02018-08-29 06:09:35278 if (!data.IsValid()) {
Saman Sami2acabd12018-03-10 00:51:09279 callbacks->Done();
280 return;
281 }
Yuri Wiitala4a74fb02018-08-29 06:09:35282 base::ReadOnlySharedMemoryMapping mapping = data.Map();
283 if (!mapping.IsValid()) {
Saman Sami2acabd12018-03-10 00:51:09284 DLOG(ERROR) << "Shared memory mapping failed.";
285 return;
286 }
Yuri Wiitala4a74fb02018-08-29 06:09:35287 if (mapping.size() <
288 media::VideoFrame::AllocationSize(info->pixel_format, info->coded_size)) {
289 DLOG(ERROR) << "Shared memory size was less than expected.";
290 return;
291 }
Yuri Wiitalaed44fc42018-12-15 03:14:24292 if (!info->color_space) {
293 DLOG(ERROR) << "Missing mandatory color space info.";
294 return;
295 }
Saman Sami2acabd12018-03-10 00:51:09296
Yuri Wiitala4a74fb02018-08-29 06:09:35297 // The SkBitmap's pixels will be marked as immutable, but the installPixels()
298 // API requires a non-const pointer. So, cast away the const.
299 void* const pixels = const_cast<void*>(mapping.memory());
300
301 // Call installPixels() with a |releaseProc| that: 1) notifies the capturer
302 // that this consumer has finished with the frame, and 2) releases the shared
303 // memory mapping.
304 struct FramePinner {
305 // Keeps the shared memory that backs |frame_| mapped.
306 base::ReadOnlySharedMemoryMapping mapping;
307 // Prevents FrameSinkVideoCapturer from recycling the shared memory that
308 // backs |frame_|.
309 viz::mojom::FrameSinkVideoConsumerFrameCallbacksPtr releaser;
310 };
311 frame_.installPixels(
312 SkImageInfo::MakeN32(content_rect.width(), content_rect.height(),
Yuri Wiitalaed44fc42018-12-15 03:14:24313 kPremul_SkAlphaType,
314 info->color_space->ToSkColorSpace()),
Yuri Wiitala4a74fb02018-08-29 06:09:35315 pixels,
316 media::VideoFrame::RowBytes(media::VideoFrame::kARGBPlane,
317 info->pixel_format, info->coded_size.width()),
318 [](void* addr, void* context) {
319 delete static_cast<FramePinner*>(context);
320 },
321 new FramePinner{std::move(mapping), std::move(callbacks)});
322 frame_.setImmutable();
Saman Sami2acabd12018-03-10 00:51:09323
Saman Sami2acabd12018-03-10 00:51:09324 UpdateCursor();
325}
326
Saman Sami2acabd12018-03-10 00:51:09327void DevToolsEyeDropper::OnStopped() {}