blob: 3bb3aa80cc8769d0fffa869fff3659ff22342e3f [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"
Christopher Cameron95fb5ee62017-08-23 12:50:5826#include "third_party/skia/include/core/SkColorSpaceXform.h"
dgozman1137e622017-04-17 19:49:1227#include "third_party/skia/include/core/SkPaint.h"
28#include "third_party/skia/include/core/SkPath.h"
29#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,
72 media::COLOR_SPACE_UNSPECIFIED);
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 uint8_t rgba_color[4] = {
132 SkColorGetR(sk_color), SkColorGetG(sk_color), SkColorGetB(sk_color),
133 SkColorGetA(sk_color),
134 };
135
136 // The picked colors are expected to be sRGB. Create a color transform from
137 // |frame_|'s color space to sRGB.
138 // TODO(ccameron): We don't actually know |frame_|'s color space, so just
139 // use |host_|'s current display's color space. This will almost always be
140 // the right color space, but is sloppy.
141 // http://crbug.com/758057
142 content::ScreenInfo screen_info;
143 host_->GetScreenInfo(&screen_info);
144 gfx::ColorSpace frame_color_space = screen_info.color_space;
145 std::unique_ptr<SkColorSpaceXform> frame_color_space_to_srgb_xform =
146 SkColorSpaceXform::New(frame_color_space.ToSkColorSpace().get(),
147 SkColorSpace::MakeSRGB().get());
148 if (frame_color_space_to_srgb_xform) {
149 bool xform_apply_result = frame_color_space_to_srgb_xform->apply(
150 SkColorSpaceXform::kRGBA_8888_ColorFormat, rgba_color,
151 SkColorSpaceXform::kRGBA_8888_ColorFormat, rgba_color, 1,
152 kUnpremul_SkAlphaType);
153 DCHECK(xform_apply_result);
154 }
155
156 callback_.Run(rgba_color[0], rgba_color[1], rgba_color[2], rgba_color[3]);
dgozman1137e622017-04-17 19:49:12157 }
158 UpdateCursor();
159 return true;
160}
161
162void DevToolsEyeDropper::UpdateCursor() {
163 if (!host_ || frame_.drawsNothing())
164 return;
165
166 if (last_cursor_x_ < 0 || last_cursor_x_ >= frame_.width() ||
167 last_cursor_y_ < 0 || last_cursor_y_ >= frame_.height()) {
168 return;
169 }
170
171// Due to platform limitations, we are using two different cursors
172// depending on the platform. Mac and Win have large cursors with two circles
173// for original spot and its magnified projection; Linux gets smaller (64 px)
174// magnified projection only with centered hotspot.
175// Mac Retina requires cursor to be > 120px in order to render smoothly.
176
177#if defined(OS_LINUX)
178 const float kCursorSize = 63;
179 const float kDiameter = 63;
180 const float kHotspotOffset = 32;
181 const float kHotspotRadius = 0;
182 const float kPixelSize = 9;
183#else
184 const float kCursorSize = 150;
185 const float kDiameter = 110;
186 const float kHotspotOffset = 25;
187 const float kHotspotRadius = 5;
188 const float kPixelSize = 10;
189#endif
190
191 content::ScreenInfo screen_info;
192 host_->GetScreenInfo(&screen_info);
193 double device_scale_factor = screen_info.device_scale_factor;
194
195 SkBitmap result;
196 result.allocN32Pixels(kCursorSize * device_scale_factor,
197 kCursorSize * device_scale_factor);
198 result.eraseARGB(0, 0, 0, 0);
199
200 SkCanvas canvas(result);
201 canvas.scale(device_scale_factor, device_scale_factor);
202 canvas.translate(0.5f, 0.5f);
203
204 SkPaint paint;
205
206 // Paint original spot with cross.
207 if (kHotspotRadius > 0) {
208 paint.setStrokeWidth(1);
209 paint.setAntiAlias(false);
210 paint.setColor(SK_ColorDKGRAY);
211 paint.setStyle(SkPaint::kStroke_Style);
212
213 canvas.drawLine(kHotspotOffset, kHotspotOffset - 2 * kHotspotRadius,
214 kHotspotOffset, kHotspotOffset - kHotspotRadius, paint);
215 canvas.drawLine(kHotspotOffset, kHotspotOffset + kHotspotRadius,
216 kHotspotOffset, kHotspotOffset + 2 * kHotspotRadius, paint);
217 canvas.drawLine(kHotspotOffset - 2 * kHotspotRadius, kHotspotOffset,
218 kHotspotOffset - kHotspotRadius, kHotspotOffset, paint);
219 canvas.drawLine(kHotspotOffset + kHotspotRadius, kHotspotOffset,
220 kHotspotOffset + 2 * kHotspotRadius, kHotspotOffset, paint);
221
222 paint.setStrokeWidth(2);
223 paint.setAntiAlias(true);
224 canvas.drawCircle(kHotspotOffset, kHotspotOffset, kHotspotRadius, paint);
225 }
226
227 // Clip circle for magnified projection.
228 float padding = (kCursorSize - kDiameter) / 2;
229 SkPath clip_path;
230 clip_path.addOval(SkRect::MakeXYWH(padding, padding, kDiameter, kDiameter));
231 clip_path.close();
232 canvas.clipPath(clip_path, SkClipOp::kIntersect, true);
233
234 // Project pixels.
235 int pixel_count = kDiameter / kPixelSize;
236 SkRect src_rect = SkRect::MakeXYWH(last_cursor_x_ - pixel_count / 2,
237 last_cursor_y_ - pixel_count / 2,
238 pixel_count, pixel_count);
239 SkRect dst_rect = SkRect::MakeXYWH(padding, padding, kDiameter, kDiameter);
240 canvas.drawBitmapRect(frame_, src_rect, dst_rect, NULL);
241
242 // Paint grid.
243 paint.setStrokeWidth(1);
244 paint.setAntiAlias(false);
245 paint.setColor(SK_ColorGRAY);
246 for (int i = 0; i < pixel_count; ++i) {
247 canvas.drawLine(padding + i * kPixelSize, padding, padding + i * kPixelSize,
248 kCursorSize - padding, paint);
249 canvas.drawLine(padding, padding + i * kPixelSize, kCursorSize - padding,
250 padding + i * kPixelSize, paint);
251 }
252
253 // Paint central pixel in red.
254 SkRect pixel =
255 SkRect::MakeXYWH((kCursorSize - kPixelSize) / 2,
256 (kCursorSize - kPixelSize) / 2, kPixelSize, kPixelSize);
257 paint.setColor(SK_ColorRED);
258 paint.setStyle(SkPaint::kStroke_Style);
259 canvas.drawRect(pixel, paint);
260
261 // Paint outline.
262 paint.setStrokeWidth(2);
263 paint.setColor(SK_ColorDKGRAY);
264 paint.setAntiAlias(true);
265 canvas.drawCircle(kCursorSize / 2, kCursorSize / 2, kDiameter / 2, paint);
266
267 content::CursorInfo cursor_info;
268 cursor_info.type = blink::WebCursorInfo::kTypeCustom;
269 cursor_info.image_scale_factor = device_scale_factor;
270 cursor_info.custom_image = result;
271 cursor_info.hotspot = gfx::Point(kHotspotOffset * device_scale_factor,
272 kHotspotOffset * device_scale_factor);
273 host_->SetCursor(cursor_info);
274}
Saman Sami2acabd12018-03-10 00:51:09275
276void DevToolsEyeDropper::OnFrameCaptured(
Yuri Wiitala4a74fb02018-08-29 06:09:35277 base::ReadOnlySharedMemoryRegion data,
Saman Sami2acabd12018-03-10 00:51:09278 ::media::mojom::VideoFrameInfoPtr info,
279 const gfx::Rect& update_rect,
280 const gfx::Rect& content_rect,
281 viz::mojom::FrameSinkVideoConsumerFrameCallbacksPtr callbacks) {
Saman Sami493222f2018-04-05 03:41:17282 gfx::Size view_size = host_->GetView()->GetViewBounds().size();
283 if (view_size != content_rect.size()) {
284 video_capturer_->SetResolutionConstraints(view_size, view_size, true);
285 video_capturer_->RequestRefreshFrame();
286 return;
287 }
288
Yuri Wiitala4a74fb02018-08-29 06:09:35289 if (!data.IsValid()) {
Saman Sami2acabd12018-03-10 00:51:09290 callbacks->Done();
291 return;
292 }
Yuri Wiitala4a74fb02018-08-29 06:09:35293 base::ReadOnlySharedMemoryMapping mapping = data.Map();
294 if (!mapping.IsValid()) {
Saman Sami2acabd12018-03-10 00:51:09295 DLOG(ERROR) << "Shared memory mapping failed.";
296 return;
297 }
Yuri Wiitala4a74fb02018-08-29 06:09:35298 if (mapping.size() <
299 media::VideoFrame::AllocationSize(info->pixel_format, info->coded_size)) {
300 DLOG(ERROR) << "Shared memory size was less than expected.";
301 return;
302 }
Saman Sami2acabd12018-03-10 00:51:09303
Yuri Wiitala4a74fb02018-08-29 06:09:35304 // The SkBitmap's pixels will be marked as immutable, but the installPixels()
305 // API requires a non-const pointer. So, cast away the const.
306 void* const pixels = const_cast<void*>(mapping.memory());
307
308 // Call installPixels() with a |releaseProc| that: 1) notifies the capturer
309 // that this consumer has finished with the frame, and 2) releases the shared
310 // memory mapping.
311 struct FramePinner {
312 // Keeps the shared memory that backs |frame_| mapped.
313 base::ReadOnlySharedMemoryMapping mapping;
314 // Prevents FrameSinkVideoCapturer from recycling the shared memory that
315 // backs |frame_|.
316 viz::mojom::FrameSinkVideoConsumerFrameCallbacksPtr releaser;
317 };
318 frame_.installPixels(
319 SkImageInfo::MakeN32(content_rect.width(), content_rect.height(),
320 kPremul_SkAlphaType),
321 pixels,
322 media::VideoFrame::RowBytes(media::VideoFrame::kARGBPlane,
323 info->pixel_format, info->coded_size.width()),
324 [](void* addr, void* context) {
325 delete static_cast<FramePinner*>(context);
326 },
327 new FramePinner{std::move(mapping), std::move(callbacks)});
328 frame_.setImmutable();
Saman Sami2acabd12018-03-10 00:51:09329
Saman Sami2acabd12018-03-10 00:51:09330 UpdateCursor();
331}
332
Saman Sami2acabd12018-03-10 00:51:09333void DevToolsEyeDropper::OnStopped() {}