blob: 2a90398eee7160443249c1cb29fdf4220219cb9a [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
7#include "base/bind.h"
8#include "build/build_config.h"
Saman Sami2acabd12018-03-10 00:51:099#include "cc/paint/skia_paint_canvas.h"
10#include "components/viz/common/features.h"
dgozman1137e622017-04-17 19:49:1211#include "content/public/browser/render_view_host.h"
12#include "content/public/browser/render_widget_host.h"
13#include "content/public/browser/render_widget_host_view.h"
14#include "content/public/browser/web_contents.h"
Saman Samid81a0e642018-03-19 04:51:1515#include "content/public/common/content_features.h"
dgozman1137e622017-04-17 19:49:1216#include "content/public/common/cursor_info.h"
17#include "content/public/common/screen_info.h"
Saman Sami2acabd12018-03-10 00:51:0918#include "media/base/limits.h"
dgozman1137e622017-04-17 19:49:1219#include "third_party/WebKit/public/platform/WebInputEvent.h"
20#include "third_party/WebKit/public/platform/WebMouseEvent.h"
21#include "third_party/skia/include/core/SkCanvas.h"
Christopher Cameron95fb5ee62017-08-23 12:50:5822#include "third_party/skia/include/core/SkColorSpaceXform.h"
dgozman1137e622017-04-17 19:49:1223#include "third_party/skia/include/core/SkPaint.h"
24#include "third_party/skia/include/core/SkPath.h"
25#include "ui/gfx/geometry/size_conversions.h"
26
27DevToolsEyeDropper::DevToolsEyeDropper(content::WebContents* web_contents,
28 EyeDropperCallback callback)
29 : content::WebContentsObserver(web_contents),
30 callback_(callback),
31 last_cursor_x_(-1),
32 last_cursor_y_(-1),
33 host_(nullptr),
Saman Sami2acabd12018-03-10 00:51:0934 video_consumer_binding_(this),
Saman Samid81a0e642018-03-19 04:51:1535 use_video_capture_api_(
36 base::FeatureList::IsEnabled(features::kVizDisplayCompositor) ||
37 base::FeatureList::IsEnabled(
38 features::kUseVideoCaptureApiForDevToolsSnapshots)),
dgozman1137e622017-04-17 19:49:1239 weak_factory_(this) {
40 mouse_event_callback_ =
41 base::Bind(&DevToolsEyeDropper::HandleMouseEvent, base::Unretained(this));
42 content::RenderViewHost* rvh = web_contents->GetRenderViewHost();
43 if (rvh) {
44 AttachToHost(rvh->GetWidget());
45 UpdateFrame();
46 }
47}
48
49DevToolsEyeDropper::~DevToolsEyeDropper() {
50 DetachFromHost();
51}
52
53void DevToolsEyeDropper::AttachToHost(content::RenderWidgetHost* host) {
54 host_ = host;
55 host_->AddMouseEventCallback(mouse_event_callback_);
Saman Sami2acabd12018-03-10 00:51:0956
Saman Samid81a0e642018-03-19 04:51:1557 if (!use_video_capture_api_)
Saman Sami2acabd12018-03-10 00:51:0958 return;
59
60 // 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(
68 gfx::Size(1, 1),
69 gfx::Size(media::limits::kMaxDimension, media::limits::kMaxDimension),
70 false);
Saman Sami561d7742018-03-17 02:22:3271 video_capturer_->SetAutoThrottlingEnabled(false);
72 video_capturer_->SetMinSizeChangePeriod(base::TimeDelta());
Saman Sami00d35442018-03-20 20:22:3973 video_capturer_->SetFormat(media::PIXEL_FORMAT_ARGB,
74 media::COLOR_SPACE_UNSPECIFIED);
75 video_capturer_->SetMinSizeChangePeriod(base::TimeDelta());
Saman Sami2acabd12018-03-10 00:51:0976 video_capturer_->SetMinCapturePeriod(base::TimeDelta::FromSeconds(1) /
77 kMaxFrameRate);
78
79 // Start video capture.
80 viz::mojom::FrameSinkVideoConsumerPtr consumer;
81 video_consumer_binding_.Bind(mojo::MakeRequest(&consumer));
82 video_capturer_->Start(std::move(consumer));
dgozman1137e622017-04-17 19:49:1283}
84
85void DevToolsEyeDropper::DetachFromHost() {
86 if (!host_)
87 return;
88 host_->RemoveMouseEventCallback(mouse_event_callback_);
89 content::CursorInfo cursor_info;
90 cursor_info.type = blink::WebCursorInfo::kTypePointer;
91 host_->SetCursor(cursor_info);
Saman Sami2acabd12018-03-10 00:51:0992 video_consumer_binding_.Close();
93 video_capturer_.reset();
dgozman1137e622017-04-17 19:49:1294 host_ = nullptr;
95}
96
97void DevToolsEyeDropper::RenderViewCreated(content::RenderViewHost* host) {
98 if (!host_) {
99 AttachToHost(host->GetWidget());
100 UpdateFrame();
101 }
102}
103
104void DevToolsEyeDropper::RenderViewDeleted(content::RenderViewHost* host) {
105 if (host->GetWidget() == host_) {
106 DetachFromHost();
107 ResetFrame();
108 }
109}
110
111void DevToolsEyeDropper::RenderViewHostChanged(
112 content::RenderViewHost* old_host,
113 content::RenderViewHost* new_host) {
114 if ((old_host && old_host->GetWidget() == host_) || (!old_host && !host_)) {
115 DetachFromHost();
116 AttachToHost(new_host->GetWidget());
117 UpdateFrame();
118 }
119}
120
121void DevToolsEyeDropper::DidReceiveCompositorFrame() {
122 UpdateFrame();
123}
124
125void DevToolsEyeDropper::UpdateFrame() {
Saman Samid81a0e642018-03-19 04:51:15126 if (use_video_capture_api_ || !host_ || !host_->GetView())
dgozman1137e622017-04-17 19:49:12127 return;
128
129 // TODO(miu): This is the wrong size. It's the size of the view on-screen, and
130 // not the rendering size of the view. The latter is what is wanted here, so
131 // that the resulting bitmap's pixel coordinates line-up with the
132 // blink::WebMouseEvent coordinates. http://crbug.com/73362
133 gfx::Size should_be_rendering_size = host_->GetView()->GetViewBounds().size();
134 host_->GetView()->CopyFromSurface(
135 gfx::Rect(), should_be_rendering_size,
Yuri Wiitala6a4443f02018-02-27 22:29:27136 base::BindOnce(&DevToolsEyeDropper::FrameUpdated,
137 weak_factory_.GetWeakPtr()));
dgozman1137e622017-04-17 19:49:12138}
139
140void DevToolsEyeDropper::ResetFrame() {
141 frame_.reset();
142 last_cursor_x_ = -1;
143 last_cursor_y_ = -1;
144}
145
Yuri Wiitala6a4443f02018-02-27 22:29:27146void DevToolsEyeDropper::FrameUpdated(const SkBitmap& bitmap) {
Saman Samid81a0e642018-03-19 04:51:15147 DCHECK(!use_video_capture_api_);
Yuri Wiitala6a4443f02018-02-27 22:29:27148 if (bitmap.drawsNothing())
149 return;
150 frame_ = bitmap;
151 UpdateCursor();
dgozman1137e622017-04-17 19:49:12152}
153
154bool DevToolsEyeDropper::HandleMouseEvent(const blink::WebMouseEvent& event) {
155 last_cursor_x_ = event.PositionInWidget().x;
156 last_cursor_y_ = event.PositionInWidget().y;
157 if (frame_.drawsNothing())
158 return true;
159
160 if (event.button == blink::WebMouseEvent::Button::kLeft &&
161 (event.GetType() == blink::WebInputEvent::kMouseDown ||
162 event.GetType() == blink::WebInputEvent::kMouseMove)) {
163 if (last_cursor_x_ < 0 || last_cursor_x_ >= frame_.width() ||
164 last_cursor_y_ < 0 || last_cursor_y_ >= frame_.height()) {
165 return true;
166 }
167
dgozman1137e622017-04-17 19:49:12168 SkColor sk_color = frame_.getColor(last_cursor_x_, last_cursor_y_);
Christopher Cameron95fb5ee62017-08-23 12:50:58169 uint8_t rgba_color[4] = {
170 SkColorGetR(sk_color), SkColorGetG(sk_color), SkColorGetB(sk_color),
171 SkColorGetA(sk_color),
172 };
173
174 // The picked colors are expected to be sRGB. Create a color transform from
175 // |frame_|'s color space to sRGB.
176 // TODO(ccameron): We don't actually know |frame_|'s color space, so just
177 // use |host_|'s current display's color space. This will almost always be
178 // the right color space, but is sloppy.
179 // http://crbug.com/758057
180 content::ScreenInfo screen_info;
181 host_->GetScreenInfo(&screen_info);
182 gfx::ColorSpace frame_color_space = screen_info.color_space;
183 std::unique_ptr<SkColorSpaceXform> frame_color_space_to_srgb_xform =
184 SkColorSpaceXform::New(frame_color_space.ToSkColorSpace().get(),
185 SkColorSpace::MakeSRGB().get());
186 if (frame_color_space_to_srgb_xform) {
187 bool xform_apply_result = frame_color_space_to_srgb_xform->apply(
188 SkColorSpaceXform::kRGBA_8888_ColorFormat, rgba_color,
189 SkColorSpaceXform::kRGBA_8888_ColorFormat, rgba_color, 1,
190 kUnpremul_SkAlphaType);
191 DCHECK(xform_apply_result);
192 }
193
194 callback_.Run(rgba_color[0], rgba_color[1], rgba_color[2], rgba_color[3]);
dgozman1137e622017-04-17 19:49:12195 }
196 UpdateCursor();
197 return true;
198}
199
200void DevToolsEyeDropper::UpdateCursor() {
201 if (!host_ || frame_.drawsNothing())
202 return;
203
204 if (last_cursor_x_ < 0 || last_cursor_x_ >= frame_.width() ||
205 last_cursor_y_ < 0 || last_cursor_y_ >= frame_.height()) {
206 return;
207 }
208
209// Due to platform limitations, we are using two different cursors
210// depending on the platform. Mac and Win have large cursors with two circles
211// for original spot and its magnified projection; Linux gets smaller (64 px)
212// magnified projection only with centered hotspot.
213// Mac Retina requires cursor to be > 120px in order to render smoothly.
214
215#if defined(OS_LINUX)
216 const float kCursorSize = 63;
217 const float kDiameter = 63;
218 const float kHotspotOffset = 32;
219 const float kHotspotRadius = 0;
220 const float kPixelSize = 9;
221#else
222 const float kCursorSize = 150;
223 const float kDiameter = 110;
224 const float kHotspotOffset = 25;
225 const float kHotspotRadius = 5;
226 const float kPixelSize = 10;
227#endif
228
229 content::ScreenInfo screen_info;
230 host_->GetScreenInfo(&screen_info);
231 double device_scale_factor = screen_info.device_scale_factor;
232
233 SkBitmap result;
234 result.allocN32Pixels(kCursorSize * device_scale_factor,
235 kCursorSize * device_scale_factor);
236 result.eraseARGB(0, 0, 0, 0);
237
238 SkCanvas canvas(result);
239 canvas.scale(device_scale_factor, device_scale_factor);
240 canvas.translate(0.5f, 0.5f);
241
242 SkPaint paint;
243
244 // Paint original spot with cross.
245 if (kHotspotRadius > 0) {
246 paint.setStrokeWidth(1);
247 paint.setAntiAlias(false);
248 paint.setColor(SK_ColorDKGRAY);
249 paint.setStyle(SkPaint::kStroke_Style);
250
251 canvas.drawLine(kHotspotOffset, kHotspotOffset - 2 * kHotspotRadius,
252 kHotspotOffset, kHotspotOffset - kHotspotRadius, paint);
253 canvas.drawLine(kHotspotOffset, kHotspotOffset + kHotspotRadius,
254 kHotspotOffset, kHotspotOffset + 2 * kHotspotRadius, paint);
255 canvas.drawLine(kHotspotOffset - 2 * kHotspotRadius, kHotspotOffset,
256 kHotspotOffset - kHotspotRadius, kHotspotOffset, paint);
257 canvas.drawLine(kHotspotOffset + kHotspotRadius, kHotspotOffset,
258 kHotspotOffset + 2 * kHotspotRadius, kHotspotOffset, paint);
259
260 paint.setStrokeWidth(2);
261 paint.setAntiAlias(true);
262 canvas.drawCircle(kHotspotOffset, kHotspotOffset, kHotspotRadius, paint);
263 }
264
265 // Clip circle for magnified projection.
266 float padding = (kCursorSize - kDiameter) / 2;
267 SkPath clip_path;
268 clip_path.addOval(SkRect::MakeXYWH(padding, padding, kDiameter, kDiameter));
269 clip_path.close();
270 canvas.clipPath(clip_path, SkClipOp::kIntersect, true);
271
272 // Project pixels.
273 int pixel_count = kDiameter / kPixelSize;
274 SkRect src_rect = SkRect::MakeXYWH(last_cursor_x_ - pixel_count / 2,
275 last_cursor_y_ - pixel_count / 2,
276 pixel_count, pixel_count);
277 SkRect dst_rect = SkRect::MakeXYWH(padding, padding, kDiameter, kDiameter);
278 canvas.drawBitmapRect(frame_, src_rect, dst_rect, NULL);
279
280 // Paint grid.
281 paint.setStrokeWidth(1);
282 paint.setAntiAlias(false);
283 paint.setColor(SK_ColorGRAY);
284 for (int i = 0; i < pixel_count; ++i) {
285 canvas.drawLine(padding + i * kPixelSize, padding, padding + i * kPixelSize,
286 kCursorSize - padding, paint);
287 canvas.drawLine(padding, padding + i * kPixelSize, kCursorSize - padding,
288 padding + i * kPixelSize, paint);
289 }
290
291 // Paint central pixel in red.
292 SkRect pixel =
293 SkRect::MakeXYWH((kCursorSize - kPixelSize) / 2,
294 (kCursorSize - kPixelSize) / 2, kPixelSize, kPixelSize);
295 paint.setColor(SK_ColorRED);
296 paint.setStyle(SkPaint::kStroke_Style);
297 canvas.drawRect(pixel, paint);
298
299 // Paint outline.
300 paint.setStrokeWidth(2);
301 paint.setColor(SK_ColorDKGRAY);
302 paint.setAntiAlias(true);
303 canvas.drawCircle(kCursorSize / 2, kCursorSize / 2, kDiameter / 2, paint);
304
305 content::CursorInfo cursor_info;
306 cursor_info.type = blink::WebCursorInfo::kTypeCustom;
307 cursor_info.image_scale_factor = device_scale_factor;
308 cursor_info.custom_image = result;
309 cursor_info.hotspot = gfx::Point(kHotspotOffset * device_scale_factor,
310 kHotspotOffset * device_scale_factor);
311 host_->SetCursor(cursor_info);
312}
Saman Sami2acabd12018-03-10 00:51:09313
314void DevToolsEyeDropper::OnFrameCaptured(
315 mojo::ScopedSharedBufferHandle buffer,
316 uint32_t buffer_size,
317 ::media::mojom::VideoFrameInfoPtr info,
318 const gfx::Rect& update_rect,
319 const gfx::Rect& content_rect,
320 viz::mojom::FrameSinkVideoConsumerFrameCallbacksPtr callbacks) {
321 if (!buffer.is_valid()) {
322 callbacks->Done();
323 return;
324 }
325 mojo::ScopedSharedBufferMapping mapping = buffer->Map(buffer_size);
326 if (!mapping) {
327 DLOG(ERROR) << "Shared memory mapping failed.";
328 return;
329 }
Saman Sami2acabd12018-03-10 00:51:09330
Saman Sami00d35442018-03-20 20:22:39331 SkImageInfo image_info = SkImageInfo::MakeN32(
332 content_rect.width(), content_rect.height(), kPremul_SkAlphaType);
333 SkPixmap pixmap(image_info, mapping.get(),
334 media::VideoFrame::RowBytes(media::VideoFrame::kARGBPlane,
335 info->pixel_format,
336 info->coded_size.width()));
337 frame_.installPixels(pixmap);
338 shared_memory_mapping_ = std::move(mapping);
339 shared_memory_releaser_ = std::move(callbacks);
Saman Sami2acabd12018-03-10 00:51:09340
Saman Sami2acabd12018-03-10 00:51:09341 UpdateCursor();
342}
343
344void DevToolsEyeDropper::OnTargetLost(const viz::FrameSinkId& frame_sink_id) {}
345
346void DevToolsEyeDropper::OnStopped() {}