blob: b7a00c1536b111ceeb98feb3d005f1ee874a4a73 [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 Sami2acabd12018-03-10 00:51:0973 video_capturer_->SetMinCapturePeriod(base::TimeDelta::FromSeconds(1) /
74 kMaxFrameRate);
75
76 // Start video capture.
77 viz::mojom::FrameSinkVideoConsumerPtr consumer;
78 video_consumer_binding_.Bind(mojo::MakeRequest(&consumer));
79 video_capturer_->Start(std::move(consumer));
dgozman1137e622017-04-17 19:49:1280}
81
82void DevToolsEyeDropper::DetachFromHost() {
83 if (!host_)
84 return;
85 host_->RemoveMouseEventCallback(mouse_event_callback_);
86 content::CursorInfo cursor_info;
87 cursor_info.type = blink::WebCursorInfo::kTypePointer;
88 host_->SetCursor(cursor_info);
Saman Sami2acabd12018-03-10 00:51:0989 video_consumer_binding_.Close();
90 video_capturer_.reset();
dgozman1137e622017-04-17 19:49:1291 host_ = nullptr;
92}
93
94void DevToolsEyeDropper::RenderViewCreated(content::RenderViewHost* host) {
95 if (!host_) {
96 AttachToHost(host->GetWidget());
97 UpdateFrame();
98 }
99}
100
101void DevToolsEyeDropper::RenderViewDeleted(content::RenderViewHost* host) {
102 if (host->GetWidget() == host_) {
103 DetachFromHost();
104 ResetFrame();
105 }
106}
107
108void DevToolsEyeDropper::RenderViewHostChanged(
109 content::RenderViewHost* old_host,
110 content::RenderViewHost* new_host) {
111 if ((old_host && old_host->GetWidget() == host_) || (!old_host && !host_)) {
112 DetachFromHost();
113 AttachToHost(new_host->GetWidget());
114 UpdateFrame();
115 }
116}
117
118void DevToolsEyeDropper::DidReceiveCompositorFrame() {
119 UpdateFrame();
120}
121
122void DevToolsEyeDropper::UpdateFrame() {
Saman Samid81a0e642018-03-19 04:51:15123 if (use_video_capture_api_ || !host_ || !host_->GetView())
dgozman1137e622017-04-17 19:49:12124 return;
125
126 // TODO(miu): This is the wrong size. It's the size of the view on-screen, and
127 // not the rendering size of the view. The latter is what is wanted here, so
128 // that the resulting bitmap's pixel coordinates line-up with the
129 // blink::WebMouseEvent coordinates. http://crbug.com/73362
130 gfx::Size should_be_rendering_size = host_->GetView()->GetViewBounds().size();
131 host_->GetView()->CopyFromSurface(
132 gfx::Rect(), should_be_rendering_size,
Yuri Wiitala6a4443f02018-02-27 22:29:27133 base::BindOnce(&DevToolsEyeDropper::FrameUpdated,
134 weak_factory_.GetWeakPtr()));
dgozman1137e622017-04-17 19:49:12135}
136
137void DevToolsEyeDropper::ResetFrame() {
138 frame_.reset();
139 last_cursor_x_ = -1;
140 last_cursor_y_ = -1;
141}
142
Yuri Wiitala6a4443f02018-02-27 22:29:27143void DevToolsEyeDropper::FrameUpdated(const SkBitmap& bitmap) {
Saman Samid81a0e642018-03-19 04:51:15144 DCHECK(!use_video_capture_api_);
Yuri Wiitala6a4443f02018-02-27 22:29:27145 if (bitmap.drawsNothing())
146 return;
147 frame_ = bitmap;
148 UpdateCursor();
dgozman1137e622017-04-17 19:49:12149}
150
151bool DevToolsEyeDropper::HandleMouseEvent(const blink::WebMouseEvent& event) {
152 last_cursor_x_ = event.PositionInWidget().x;
153 last_cursor_y_ = event.PositionInWidget().y;
154 if (frame_.drawsNothing())
155 return true;
156
157 if (event.button == blink::WebMouseEvent::Button::kLeft &&
158 (event.GetType() == blink::WebInputEvent::kMouseDown ||
159 event.GetType() == blink::WebInputEvent::kMouseMove)) {
160 if (last_cursor_x_ < 0 || last_cursor_x_ >= frame_.width() ||
161 last_cursor_y_ < 0 || last_cursor_y_ >= frame_.height()) {
162 return true;
163 }
164
dgozman1137e622017-04-17 19:49:12165 SkColor sk_color = frame_.getColor(last_cursor_x_, last_cursor_y_);
Christopher Cameron95fb5ee62017-08-23 12:50:58166 uint8_t rgba_color[4] = {
167 SkColorGetR(sk_color), SkColorGetG(sk_color), SkColorGetB(sk_color),
168 SkColorGetA(sk_color),
169 };
170
171 // The picked colors are expected to be sRGB. Create a color transform from
172 // |frame_|'s color space to sRGB.
173 // TODO(ccameron): We don't actually know |frame_|'s color space, so just
174 // use |host_|'s current display's color space. This will almost always be
175 // the right color space, but is sloppy.
176 // http://crbug.com/758057
177 content::ScreenInfo screen_info;
178 host_->GetScreenInfo(&screen_info);
179 gfx::ColorSpace frame_color_space = screen_info.color_space;
180 std::unique_ptr<SkColorSpaceXform> frame_color_space_to_srgb_xform =
181 SkColorSpaceXform::New(frame_color_space.ToSkColorSpace().get(),
182 SkColorSpace::MakeSRGB().get());
183 if (frame_color_space_to_srgb_xform) {
184 bool xform_apply_result = frame_color_space_to_srgb_xform->apply(
185 SkColorSpaceXform::kRGBA_8888_ColorFormat, rgba_color,
186 SkColorSpaceXform::kRGBA_8888_ColorFormat, rgba_color, 1,
187 kUnpremul_SkAlphaType);
188 DCHECK(xform_apply_result);
189 }
190
191 callback_.Run(rgba_color[0], rgba_color[1], rgba_color[2], rgba_color[3]);
dgozman1137e622017-04-17 19:49:12192 }
193 UpdateCursor();
194 return true;
195}
196
197void DevToolsEyeDropper::UpdateCursor() {
198 if (!host_ || frame_.drawsNothing())
199 return;
200
201 if (last_cursor_x_ < 0 || last_cursor_x_ >= frame_.width() ||
202 last_cursor_y_ < 0 || last_cursor_y_ >= frame_.height()) {
203 return;
204 }
205
206// Due to platform limitations, we are using two different cursors
207// depending on the platform. Mac and Win have large cursors with two circles
208// for original spot and its magnified projection; Linux gets smaller (64 px)
209// magnified projection only with centered hotspot.
210// Mac Retina requires cursor to be > 120px in order to render smoothly.
211
212#if defined(OS_LINUX)
213 const float kCursorSize = 63;
214 const float kDiameter = 63;
215 const float kHotspotOffset = 32;
216 const float kHotspotRadius = 0;
217 const float kPixelSize = 9;
218#else
219 const float kCursorSize = 150;
220 const float kDiameter = 110;
221 const float kHotspotOffset = 25;
222 const float kHotspotRadius = 5;
223 const float kPixelSize = 10;
224#endif
225
226 content::ScreenInfo screen_info;
227 host_->GetScreenInfo(&screen_info);
228 double device_scale_factor = screen_info.device_scale_factor;
229
230 SkBitmap result;
231 result.allocN32Pixels(kCursorSize * device_scale_factor,
232 kCursorSize * device_scale_factor);
233 result.eraseARGB(0, 0, 0, 0);
234
235 SkCanvas canvas(result);
236 canvas.scale(device_scale_factor, device_scale_factor);
237 canvas.translate(0.5f, 0.5f);
238
239 SkPaint paint;
240
241 // Paint original spot with cross.
242 if (kHotspotRadius > 0) {
243 paint.setStrokeWidth(1);
244 paint.setAntiAlias(false);
245 paint.setColor(SK_ColorDKGRAY);
246 paint.setStyle(SkPaint::kStroke_Style);
247
248 canvas.drawLine(kHotspotOffset, kHotspotOffset - 2 * kHotspotRadius,
249 kHotspotOffset, kHotspotOffset - kHotspotRadius, paint);
250 canvas.drawLine(kHotspotOffset, kHotspotOffset + kHotspotRadius,
251 kHotspotOffset, kHotspotOffset + 2 * kHotspotRadius, paint);
252 canvas.drawLine(kHotspotOffset - 2 * kHotspotRadius, kHotspotOffset,
253 kHotspotOffset - kHotspotRadius, kHotspotOffset, paint);
254 canvas.drawLine(kHotspotOffset + kHotspotRadius, kHotspotOffset,
255 kHotspotOffset + 2 * kHotspotRadius, kHotspotOffset, paint);
256
257 paint.setStrokeWidth(2);
258 paint.setAntiAlias(true);
259 canvas.drawCircle(kHotspotOffset, kHotspotOffset, kHotspotRadius, paint);
260 }
261
262 // Clip circle for magnified projection.
263 float padding = (kCursorSize - kDiameter) / 2;
264 SkPath clip_path;
265 clip_path.addOval(SkRect::MakeXYWH(padding, padding, kDiameter, kDiameter));
266 clip_path.close();
267 canvas.clipPath(clip_path, SkClipOp::kIntersect, true);
268
269 // Project pixels.
270 int pixel_count = kDiameter / kPixelSize;
271 SkRect src_rect = SkRect::MakeXYWH(last_cursor_x_ - pixel_count / 2,
272 last_cursor_y_ - pixel_count / 2,
273 pixel_count, pixel_count);
274 SkRect dst_rect = SkRect::MakeXYWH(padding, padding, kDiameter, kDiameter);
275 canvas.drawBitmapRect(frame_, src_rect, dst_rect, NULL);
276
277 // Paint grid.
278 paint.setStrokeWidth(1);
279 paint.setAntiAlias(false);
280 paint.setColor(SK_ColorGRAY);
281 for (int i = 0; i < pixel_count; ++i) {
282 canvas.drawLine(padding + i * kPixelSize, padding, padding + i * kPixelSize,
283 kCursorSize - padding, paint);
284 canvas.drawLine(padding, padding + i * kPixelSize, kCursorSize - padding,
285 padding + i * kPixelSize, paint);
286 }
287
288 // Paint central pixel in red.
289 SkRect pixel =
290 SkRect::MakeXYWH((kCursorSize - kPixelSize) / 2,
291 (kCursorSize - kPixelSize) / 2, kPixelSize, kPixelSize);
292 paint.setColor(SK_ColorRED);
293 paint.setStyle(SkPaint::kStroke_Style);
294 canvas.drawRect(pixel, paint);
295
296 // Paint outline.
297 paint.setStrokeWidth(2);
298 paint.setColor(SK_ColorDKGRAY);
299 paint.setAntiAlias(true);
300 canvas.drawCircle(kCursorSize / 2, kCursorSize / 2, kDiameter / 2, paint);
301
302 content::CursorInfo cursor_info;
303 cursor_info.type = blink::WebCursorInfo::kTypeCustom;
304 cursor_info.image_scale_factor = device_scale_factor;
305 cursor_info.custom_image = result;
306 cursor_info.hotspot = gfx::Point(kHotspotOffset * device_scale_factor,
307 kHotspotOffset * device_scale_factor);
308 host_->SetCursor(cursor_info);
309}
Saman Sami2acabd12018-03-10 00:51:09310
311void DevToolsEyeDropper::OnFrameCaptured(
312 mojo::ScopedSharedBufferHandle buffer,
313 uint32_t buffer_size,
314 ::media::mojom::VideoFrameInfoPtr info,
315 const gfx::Rect& update_rect,
316 const gfx::Rect& content_rect,
317 viz::mojom::FrameSinkVideoConsumerFrameCallbacksPtr callbacks) {
318 if (!buffer.is_valid()) {
319 callbacks->Done();
320 return;
321 }
322 mojo::ScopedSharedBufferMapping mapping = buffer->Map(buffer_size);
323 if (!mapping) {
324 DLOG(ERROR) << "Shared memory mapping failed.";
325 return;
326 }
327 scoped_refptr<media::VideoFrame> frame = media::VideoFrame::WrapExternalData(
328 info->pixel_format, info->coded_size, info->visible_rect,
329 info->visible_rect.size(), static_cast<uint8_t*>(mapping.get()),
330 buffer_size, info->timestamp);
331 if (!frame)
332 return;
333 frame->AddDestructionObserver(base::BindOnce(
334 [](mojo::ScopedSharedBufferMapping mapping) {}, std::move(mapping)));
335
336 SkBitmap skbitmap;
337 skbitmap.allocN32Pixels(info->visible_rect.width(),
338 info->visible_rect.height());
339 cc::SkiaPaintCanvas canvas(skbitmap);
340 video_renderer_.Copy(frame, &canvas, media::Context3D());
341
342 frame_ = skbitmap;
343 UpdateCursor();
344}
345
346void DevToolsEyeDropper::OnTargetLost(const viz::FrameSinkId& frame_sink_id) {}
347
348void DevToolsEyeDropper::OnStopped() {}