blob: 55822d8468cd7a31148e6bbe3a691031cc7787a5 [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"
Blink Reformata30d4232018-04-07 15:31:0623#include "third_party/blink/public/platform/web_input_event.h"
24#include "third_party/blink/public/platform/web_mouse_event.h"
dgozman1137e622017-04-17 19:49:1225#include "third_party/skia/include/core/SkCanvas.h"
dgozman1137e622017-04-17 19:49:1226#include "third_party/skia/include/core/SkPaint.h"
27#include "third_party/skia/include/core/SkPath.h"
Mike Klein7008dab2018-09-28 21:58:5128#include "third_party/skia/include/core/SkPixmap.h"
dgozman1137e622017-04-17 19:49:1229#include "ui/gfx/geometry/size_conversions.h"
30
31DevToolsEyeDropper::DevToolsEyeDropper(content::WebContents* web_contents,
32 EyeDropperCallback callback)
33 : content::WebContentsObserver(web_contents),
34 callback_(callback),
35 last_cursor_x_(-1),
36 last_cursor_y_(-1),
37 host_(nullptr),
dgozman1137e622017-04-17 19:49:1238 weak_factory_(this) {
39 mouse_event_callback_ =
40 base::Bind(&DevToolsEyeDropper::HandleMouseEvent, base::Unretained(this));
41 content::RenderViewHost* rvh = web_contents->GetRenderViewHost();
Saman Sami5cc093d2018-08-08 12:12:2542 if (rvh)
dgozman1137e622017-04-17 19:49:1243 AttachToHost(rvh->GetWidget());
dgozman1137e622017-04-17 19:49:1244}
45
46DevToolsEyeDropper::~DevToolsEyeDropper() {
47 DetachFromHost();
48}
49
50void DevToolsEyeDropper::AttachToHost(content::RenderWidgetHost* host) {
51 host_ = host;
52 host_->AddMouseEventCallback(mouse_event_callback_);
Saman Sami2acabd12018-03-10 00:51:0953
Saman Samie1e85762018-06-01 18:38:3354 // The view can be null if the renderer process has crashed.
55 // (https://crbug.com/847363)
56 if (!host_->GetView())
57 return;
58
Saman Sami2acabd12018-03-10 00:51:0959 // Capturing a full-page screenshot can be costly so we shouldn't do it too
60 // often. We can capture at a lower frame rate without hurting the user
61 // experience.
62 constexpr static int kMaxFrameRate = 15;
63
64 // Create and configure the video capturer.
65 video_capturer_ = host_->GetView()->CreateVideoCapturer();
66 video_capturer_->SetResolutionConstraints(
Saman Sami493222f2018-04-05 03:41:1767 host_->GetView()->GetViewBounds().size(),
68 host_->GetView()->GetViewBounds().size(), true);
Saman Sami561d7742018-03-17 02:22:3269 video_capturer_->SetAutoThrottlingEnabled(false);
70 video_capturer_->SetMinSizeChangePeriod(base::TimeDelta());
Saman Sami00d35442018-03-20 20:22:3971 video_capturer_->SetFormat(media::PIXEL_FORMAT_ARGB,
Fredrik Hubinette088f6dc2018-10-04 19:42:3072 gfx::ColorSpace::CreateREC709());
Saman Sami2acabd12018-03-10 00:51:0973 video_capturer_->SetMinCapturePeriod(base::TimeDelta::FromSeconds(1) /
74 kMaxFrameRate);
Saman Samid2dc25f2018-05-24 20:41:0775 video_capturer_->Start(this);
dgozman1137e622017-04-17 19:49:1276}
77
78void DevToolsEyeDropper::DetachFromHost() {
79 if (!host_)
80 return;
81 host_->RemoveMouseEventCallback(mouse_event_callback_);
82 content::CursorInfo cursor_info;
83 cursor_info.type = blink::WebCursorInfo::kTypePointer;
84 host_->SetCursor(cursor_info);
Saman Sami2acabd12018-03-10 00:51:0985 video_capturer_.reset();
dgozman1137e622017-04-17 19:49:1286 host_ = nullptr;
87}
88
89void DevToolsEyeDropper::RenderViewCreated(content::RenderViewHost* host) {
Saman Sami5cc093d2018-08-08 12:12:2590 if (!host_)
dgozman1137e622017-04-17 19:49:1291 AttachToHost(host->GetWidget());
dgozman1137e622017-04-17 19:49:1292}
93
94void DevToolsEyeDropper::RenderViewDeleted(content::RenderViewHost* host) {
95 if (host->GetWidget() == host_) {
96 DetachFromHost();
97 ResetFrame();
98 }
99}
100
101void DevToolsEyeDropper::RenderViewHostChanged(
102 content::RenderViewHost* old_host,
103 content::RenderViewHost* new_host) {
104 if ((old_host && old_host->GetWidget() == host_) || (!old_host && !host_)) {
105 DetachFromHost();
106 AttachToHost(new_host->GetWidget());
dgozman1137e622017-04-17 19:49:12107 }
108}
109
dgozman1137e622017-04-17 19:49:12110void DevToolsEyeDropper::ResetFrame() {
111 frame_.reset();
112 last_cursor_x_ = -1;
113 last_cursor_y_ = -1;
114}
115
dgozman1137e622017-04-17 19:49:12116bool DevToolsEyeDropper::HandleMouseEvent(const blink::WebMouseEvent& event) {
117 last_cursor_x_ = event.PositionInWidget().x;
118 last_cursor_y_ = event.PositionInWidget().y;
119 if (frame_.drawsNothing())
120 return true;
121
122 if (event.button == blink::WebMouseEvent::Button::kLeft &&
123 (event.GetType() == blink::WebInputEvent::kMouseDown ||
124 event.GetType() == blink::WebInputEvent::kMouseMove)) {
125 if (last_cursor_x_ < 0 || last_cursor_x_ >= frame_.width() ||
126 last_cursor_y_ < 0 || last_cursor_y_ >= frame_.height()) {
127 return true;
128 }
129
dgozman1137e622017-04-17 19:49:12130 SkColor sk_color = frame_.getColor(last_cursor_x_, last_cursor_y_);
Christopher Cameron95fb5ee62017-08-23 12:50:58131
Mike Klein7008dab2018-09-28 21:58:51132 // The picked colors are expected to be sRGB. Convert from |frame_|'s color
133 // space to sRGB.
Mike Klein7008dab2018-09-28 21:58:51134 SkPixmap pm(
135 SkImageInfo::Make(1, 1, kBGRA_8888_SkColorType, kUnpremul_SkAlphaType,
Yuri Wiitalaed44fc42018-12-15 03:14:24136 frame_.refColorSpace()),
Mike Klein7008dab2018-09-28 21:58:51137 &sk_color, sizeof(sk_color));
Mike Klein7008dab2018-09-28 21:58:51138 uint8_t rgba_color[4];
139 bool ok = pm.readPixels(
140 SkImageInfo::Make(1, 1, kRGBA_8888_SkColorType, kUnpremul_SkAlphaType,
141 SkColorSpace::MakeSRGB()),
142 rgba_color, sizeof(rgba_color));
143 DCHECK(ok);
Christopher Cameron95fb5ee62017-08-23 12:50:58144
145 callback_.Run(rgba_color[0], rgba_color[1], rgba_color[2], rgba_color[3]);
dgozman1137e622017-04-17 19:49:12146 }
147 UpdateCursor();
148 return true;
149}
150
151void DevToolsEyeDropper::UpdateCursor() {
152 if (!host_ || frame_.drawsNothing())
153 return;
154
155 if (last_cursor_x_ < 0 || last_cursor_x_ >= frame_.width() ||
156 last_cursor_y_ < 0 || last_cursor_y_ >= frame_.height()) {
157 return;
158 }
159
160// Due to platform limitations, we are using two different cursors
161// depending on the platform. Mac and Win have large cursors with two circles
162// for original spot and its magnified projection; Linux gets smaller (64 px)
163// magnified projection only with centered hotspot.
164// Mac Retina requires cursor to be > 120px in order to render smoothly.
165
166#if defined(OS_LINUX)
167 const float kCursorSize = 63;
168 const float kDiameter = 63;
169 const float kHotspotOffset = 32;
170 const float kHotspotRadius = 0;
171 const float kPixelSize = 9;
172#else
173 const float kCursorSize = 150;
174 const float kDiameter = 110;
175 const float kHotspotOffset = 25;
176 const float kHotspotRadius = 5;
177 const float kPixelSize = 10;
178#endif
179
180 content::ScreenInfo screen_info;
181 host_->GetScreenInfo(&screen_info);
182 double device_scale_factor = screen_info.device_scale_factor;
183
184 SkBitmap result;
185 result.allocN32Pixels(kCursorSize * device_scale_factor,
186 kCursorSize * device_scale_factor);
187 result.eraseARGB(0, 0, 0, 0);
188
189 SkCanvas canvas(result);
190 canvas.scale(device_scale_factor, device_scale_factor);
191 canvas.translate(0.5f, 0.5f);
192
193 SkPaint paint;
194
195 // Paint original spot with cross.
196 if (kHotspotRadius > 0) {
197 paint.setStrokeWidth(1);
198 paint.setAntiAlias(false);
199 paint.setColor(SK_ColorDKGRAY);
200 paint.setStyle(SkPaint::kStroke_Style);
201
202 canvas.drawLine(kHotspotOffset, kHotspotOffset - 2 * kHotspotRadius,
203 kHotspotOffset, kHotspotOffset - kHotspotRadius, paint);
204 canvas.drawLine(kHotspotOffset, kHotspotOffset + kHotspotRadius,
205 kHotspotOffset, kHotspotOffset + 2 * kHotspotRadius, paint);
206 canvas.drawLine(kHotspotOffset - 2 * kHotspotRadius, kHotspotOffset,
207 kHotspotOffset - kHotspotRadius, kHotspotOffset, paint);
208 canvas.drawLine(kHotspotOffset + kHotspotRadius, kHotspotOffset,
209 kHotspotOffset + 2 * kHotspotRadius, kHotspotOffset, paint);
210
211 paint.setStrokeWidth(2);
212 paint.setAntiAlias(true);
213 canvas.drawCircle(kHotspotOffset, kHotspotOffset, kHotspotRadius, paint);
214 }
215
216 // Clip circle for magnified projection.
217 float padding = (kCursorSize - kDiameter) / 2;
218 SkPath clip_path;
219 clip_path.addOval(SkRect::MakeXYWH(padding, padding, kDiameter, kDiameter));
220 clip_path.close();
221 canvas.clipPath(clip_path, SkClipOp::kIntersect, true);
222
223 // Project pixels.
224 int pixel_count = kDiameter / kPixelSize;
225 SkRect src_rect = SkRect::MakeXYWH(last_cursor_x_ - pixel_count / 2,
226 last_cursor_y_ - pixel_count / 2,
227 pixel_count, pixel_count);
228 SkRect dst_rect = SkRect::MakeXYWH(padding, padding, kDiameter, kDiameter);
229 canvas.drawBitmapRect(frame_, src_rect, dst_rect, NULL);
230
231 // Paint grid.
232 paint.setStrokeWidth(1);
233 paint.setAntiAlias(false);
234 paint.setColor(SK_ColorGRAY);
235 for (int i = 0; i < pixel_count; ++i) {
236 canvas.drawLine(padding + i * kPixelSize, padding, padding + i * kPixelSize,
237 kCursorSize - padding, paint);
238 canvas.drawLine(padding, padding + i * kPixelSize, kCursorSize - padding,
239 padding + i * kPixelSize, paint);
240 }
241
242 // Paint central pixel in red.
243 SkRect pixel =
244 SkRect::MakeXYWH((kCursorSize - kPixelSize) / 2,
245 (kCursorSize - kPixelSize) / 2, kPixelSize, kPixelSize);
246 paint.setColor(SK_ColorRED);
247 paint.setStyle(SkPaint::kStroke_Style);
248 canvas.drawRect(pixel, paint);
249
250 // Paint outline.
251 paint.setStrokeWidth(2);
252 paint.setColor(SK_ColorDKGRAY);
253 paint.setAntiAlias(true);
254 canvas.drawCircle(kCursorSize / 2, kCursorSize / 2, kDiameter / 2, paint);
255
256 content::CursorInfo cursor_info;
257 cursor_info.type = blink::WebCursorInfo::kTypeCustom;
258 cursor_info.image_scale_factor = device_scale_factor;
259 cursor_info.custom_image = result;
260 cursor_info.hotspot = gfx::Point(kHotspotOffset * device_scale_factor,
261 kHotspotOffset * device_scale_factor);
262 host_->SetCursor(cursor_info);
263}
Saman Sami2acabd12018-03-10 00:51:09264
265void DevToolsEyeDropper::OnFrameCaptured(
Yuri Wiitala4a74fb02018-08-29 06:09:35266 base::ReadOnlySharedMemoryRegion data,
Saman Sami2acabd12018-03-10 00:51:09267 ::media::mojom::VideoFrameInfoPtr info,
268 const gfx::Rect& update_rect,
269 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() {}