Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(37)

Side by Side Diff: media/video/capture/screen/screen_capturer_x11.cc

Issue 15692018: Remove screen capturers from media/video/capture/screen. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Created 7 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 // Copyright (c) 2012 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 "media/video/capture/screen/screen_capturer.h"
6
7 #include <X11/extensions/Xdamage.h>
8 #include <X11/extensions/Xfixes.h>
9 #include <X11/Xlib.h>
10 #include <X11/Xutil.h>
11
12 #include <set>
13
14 #include "base/basictypes.h"
15 #include "base/logging.h"
16 #include "base/memory/scoped_ptr.h"
17 #include "media/video/capture/screen/differ.h"
18 #include "media/video/capture/screen/mouse_cursor_shape.h"
19 #include "media/video/capture/screen/screen_capture_frame_queue.h"
20 #include "media/video/capture/screen/screen_capturer_helper.h"
21 #include "media/video/capture/screen/x11/x_server_pixel_buffer.h"
22 #include "third_party/webrtc/modules/desktop_capture/desktop_frame.h"
23
24 namespace media {
25
26 namespace {
27
28 // A class to perform video frame capturing for Linux.
29 class ScreenCapturerLinux : public ScreenCapturer {
30 public:
31 ScreenCapturerLinux();
32 virtual ~ScreenCapturerLinux();
33
34 // TODO(ajwong): Do we really want this to be synchronous?
35 bool Init(bool use_x_damage);
36
37 // DesktopCapturer interface.
38 virtual void Start(Callback* delegate) OVERRIDE;
39 virtual void Capture(const webrtc::DesktopRegion& region) OVERRIDE;
40
41 // ScreenCapturer interface.
42 virtual void SetMouseShapeObserver(
43 MouseShapeObserver* mouse_shape_observer) OVERRIDE;
44
45 private:
46 void InitXDamage();
47
48 // Read and handle all currently-pending XEvents.
49 // In the DAMAGE case, process the XDamage events and store the resulting
50 // damage rectangles in the ScreenCapturerHelper.
51 // In all cases, call ScreenConfigurationChanged() in response to any
52 // ConfigNotify events.
53 void ProcessPendingXEvents();
54
55 // Capture the cursor image and notify the delegate if it was captured.
56 void CaptureCursor();
57
58 // Capture screen pixels to the current buffer in the queue. In the DAMAGE
59 // case, the ScreenCapturerHelper already holds the list of invalid rectangles
60 // from ProcessPendingXEvents(). In the non-DAMAGE case, this captures the
61 // whole screen, then calculates some invalid rectangles that include any
62 // differences between this and the previous capture.
63 webrtc::DesktopFrame* CaptureScreen();
64
65 // Called when the screen configuration is changed. |root_window_size|
66 // specifies the most recent size of the root window.
67 void ScreenConfigurationChanged(const webrtc::DesktopSize& root_window_size);
68
69 // Synchronize the current buffer with |last_buffer_|, by copying pixels from
70 // the area of |last_invalid_rects|.
71 // Note this only works on the assumption that kNumBuffers == 2, as
72 // |last_invalid_rects| holds the differences from the previous buffer and
73 // the one prior to that (which will then be the current buffer).
74 void SynchronizeFrame();
75
76 void DeinitXlib();
77
78 // Capture a rectangle from |x_server_pixel_buffer_|, and copy the data into
79 // |frame|.
80 void CaptureRect(const webrtc::DesktopRect& rect,
81 webrtc::DesktopFrame* frame);
82
83 // We expose two forms of blitting to handle variations in the pixel format.
84 // In FastBlit, the operation is effectively a memcpy.
85 void FastBlit(uint8* image,
86 const webrtc::DesktopRect& rect,
87 webrtc::DesktopFrame* frame);
88 void SlowBlit(uint8* image,
89 const webrtc::DesktopRect& rect,
90 webrtc::DesktopFrame* frame);
91
92 // Returns the number of bits |mask| has to be shifted left so its last
93 // (most-significant) bit set becomes the most-significant bit of the word.
94 // When |mask| is 0 the function returns 31.
95 static uint32 GetRgbShift(uint32 mask);
96
97 Callback* callback_;
98 MouseShapeObserver* mouse_shape_observer_;
99
100 // X11 graphics context.
101 Display* display_;
102 GC gc_;
103 Window root_window_;
104
105 // Last known dimensions of the root window.
106 webrtc::DesktopSize root_window_size_;
107
108 // XFixes.
109 bool has_xfixes_;
110 int xfixes_event_base_;
111 int xfixes_error_base_;
112
113 // XDamage information.
114 bool use_damage_;
115 Damage damage_handle_;
116 int damage_event_base_;
117 int damage_error_base_;
118 XserverRegion damage_region_;
119
120 // Access to the X Server's pixel buffer.
121 XServerPixelBuffer x_server_pixel_buffer_;
122
123 // A thread-safe list of invalid rectangles, and the size of the most
124 // recently captured screen.
125 ScreenCapturerHelper helper_;
126
127 // Queue of the frames buffers.
128 ScreenCaptureFrameQueue queue_;
129
130 // Invalid region from the previous capture. This is used to synchronize the
131 // current with the last buffer used.
132 webrtc::DesktopRegion last_invalid_region_;
133
134 // |Differ| for use when polling for changes.
135 scoped_ptr<Differ> differ_;
136
137 DISALLOW_COPY_AND_ASSIGN(ScreenCapturerLinux);
138 };
139
140 ScreenCapturerLinux::ScreenCapturerLinux()
141 : callback_(NULL),
142 mouse_shape_observer_(NULL),
143 display_(NULL),
144 gc_(NULL),
145 root_window_(BadValue),
146 has_xfixes_(false),
147 xfixes_event_base_(-1),
148 xfixes_error_base_(-1),
149 use_damage_(false),
150 damage_handle_(0),
151 damage_event_base_(-1),
152 damage_error_base_(-1),
153 damage_region_(0) {
154 helper_.SetLogGridSize(4);
155 }
156
157 ScreenCapturerLinux::~ScreenCapturerLinux() {
158 DeinitXlib();
159 }
160
161 bool ScreenCapturerLinux::Init(bool use_x_damage) {
162 // TODO(ajwong): We should specify the display string we are attaching to
163 // in the constructor.
164 display_ = XOpenDisplay(NULL);
165 if (!display_) {
166 LOG(ERROR) << "Unable to open display";
167 return false;
168 }
169
170 root_window_ = RootWindow(display_, DefaultScreen(display_));
171 if (root_window_ == BadValue) {
172 LOG(ERROR) << "Unable to get the root window";
173 DeinitXlib();
174 return false;
175 }
176
177 gc_ = XCreateGC(display_, root_window_, 0, NULL);
178 if (gc_ == NULL) {
179 LOG(ERROR) << "Unable to get graphics context";
180 DeinitXlib();
181 return false;
182 }
183
184 // Check for XFixes extension. This is required for cursor shape
185 // notifications, and for our use of XDamage.
186 if (XFixesQueryExtension(display_, &xfixes_event_base_,
187 &xfixes_error_base_)) {
188 has_xfixes_ = true;
189 } else {
190 LOG(INFO) << "X server does not support XFixes.";
191 }
192
193 // Register for changes to the dimensions of the root window.
194 XSelectInput(display_, root_window_, StructureNotifyMask);
195
196 root_window_size_ = XServerPixelBuffer::GetRootWindowSize(display_);
197 x_server_pixel_buffer_.Init(display_, root_window_size_);
198
199 if (has_xfixes_) {
200 // Register for changes to the cursor shape.
201 XFixesSelectCursorInput(display_, root_window_,
202 XFixesDisplayCursorNotifyMask);
203 }
204
205 if (use_x_damage) {
206 InitXDamage();
207 }
208
209 return true;
210 }
211
212 void ScreenCapturerLinux::InitXDamage() {
213 // Our use of XDamage requires XFixes.
214 if (!has_xfixes_) {
215 return;
216 }
217
218 // Check for XDamage extension.
219 if (!XDamageQueryExtension(display_, &damage_event_base_,
220 &damage_error_base_)) {
221 LOG(INFO) << "X server does not support XDamage.";
222 return;
223 }
224
225 // TODO(lambroslambrou): Disable DAMAGE in situations where it is known
226 // to fail, such as when Desktop Effects are enabled, with graphics
227 // drivers (nVidia, ATI) that fail to report DAMAGE notifications
228 // properly.
229
230 // Request notifications every time the screen becomes damaged.
231 damage_handle_ = XDamageCreate(display_, root_window_,
232 XDamageReportNonEmpty);
233 if (!damage_handle_) {
234 LOG(ERROR) << "Unable to initialize XDamage.";
235 return;
236 }
237
238 // Create an XFixes server-side region to collate damage into.
239 damage_region_ = XFixesCreateRegion(display_, 0, 0);
240 if (!damage_region_) {
241 XDamageDestroy(display_, damage_handle_);
242 LOG(ERROR) << "Unable to create XFixes region.";
243 return;
244 }
245
246 use_damage_ = true;
247 LOG(INFO) << "Using XDamage extension.";
248 }
249
250 void ScreenCapturerLinux::Start(Callback* callback) {
251 DCHECK(!callback_);
252 DCHECK(callback);
253
254 callback_ = callback;
255 }
256
257 void ScreenCapturerLinux::Capture(const webrtc::DesktopRegion& region) {
258 base::Time capture_start_time = base::Time::Now();
259
260 queue_.MoveToNextFrame();
261
262 // Process XEvents for XDamage and cursor shape tracking.
263 ProcessPendingXEvents();
264
265 // If the current frame is from an older generation then allocate a new one.
266 // Note that we can't reallocate other buffers at this point, since the caller
267 // may still be reading from them.
268 if (!queue_.current_frame()) {
269 scoped_ptr<webrtc::DesktopFrame> frame(
270 new webrtc::BasicDesktopFrame(root_window_size_));
271 queue_.ReplaceCurrentFrame(frame.Pass());
272 }
273
274 // Refresh the Differ helper used by CaptureFrame(), if needed.
275 webrtc::DesktopFrame* frame = queue_.current_frame();
276 if (!use_damage_ && (
277 !differ_.get() ||
278 (differ_->width() != frame->size().width()) ||
279 (differ_->height() != frame->size().height()) ||
280 (differ_->bytes_per_row() != frame->stride()))) {
281 differ_.reset(new Differ(frame->size().width(), frame->size().height(),
282 webrtc::DesktopFrame::kBytesPerPixel,
283 frame->stride()));
284 }
285
286 webrtc::DesktopFrame* result = CaptureScreen();
287 last_invalid_region_ = result->updated_region();
288 result->set_capture_time_ms(
289 (base::Time::Now() - capture_start_time).InMillisecondsRoundedUp());
290 callback_->OnCaptureCompleted(result);
291 }
292
293 void ScreenCapturerLinux::SetMouseShapeObserver(
294 MouseShapeObserver* mouse_shape_observer) {
295 DCHECK(!mouse_shape_observer_);
296 DCHECK(mouse_shape_observer);
297
298 mouse_shape_observer_ = mouse_shape_observer;
299 }
300
301 void ScreenCapturerLinux::ProcessPendingXEvents() {
302 // Find the number of events that are outstanding "now." We don't just loop
303 // on XPending because we want to guarantee this terminates.
304 int events_to_process = XPending(display_);
305 XEvent e;
306
307 for (int i = 0; i < events_to_process; i++) {
308 XNextEvent(display_, &e);
309 if (use_damage_ && (e.type == damage_event_base_ + XDamageNotify)) {
310 XDamageNotifyEvent* event = reinterpret_cast<XDamageNotifyEvent*>(&e);
311 DCHECK(event->level == XDamageReportNonEmpty);
312 } else if (e.type == ConfigureNotify) {
313 const XConfigureEvent& event = e.xconfigure;
314 ScreenConfigurationChanged(
315 webrtc::DesktopSize(event.width, event.height));
316 } else if (has_xfixes_ &&
317 e.type == xfixes_event_base_ + XFixesCursorNotify) {
318 XFixesCursorNotifyEvent* cne;
319 cne = reinterpret_cast<XFixesCursorNotifyEvent*>(&e);
320 if (cne->subtype == XFixesDisplayCursorNotify) {
321 CaptureCursor();
322 }
323 } else {
324 LOG(WARNING) << "Got unknown event type: " << e.type;
325 }
326 }
327 }
328
329 void ScreenCapturerLinux::CaptureCursor() {
330 DCHECK(has_xfixes_);
331
332 XFixesCursorImage* img = XFixesGetCursorImage(display_);
333 if (!img) {
334 return;
335 }
336
337 scoped_ptr<MouseCursorShape> cursor(new MouseCursorShape());
338 cursor->size = webrtc::DesktopSize(img->width, img->height);
339 cursor->hotspot = webrtc::DesktopVector(img->xhot, img->yhot);
340
341 int total_bytes = cursor->size.width ()* cursor->size.height() *
342 webrtc::DesktopFrame::kBytesPerPixel;
343 cursor->data.resize(total_bytes);
344
345 // Xlib stores 32-bit data in longs, even if longs are 64-bits long.
346 unsigned long* src = img->pixels;
347 uint32* dst = reinterpret_cast<uint32*>(&*(cursor->data.begin()));
348 uint32* dst_end = dst + (img->width * img->height);
349 while (dst < dst_end) {
350 *dst++ = static_cast<uint32>(*src++);
351 }
352 XFree(img);
353
354 if (mouse_shape_observer_)
355 mouse_shape_observer_->OnCursorShapeChanged(cursor.Pass());
356 }
357
358 webrtc::DesktopFrame* ScreenCapturerLinux::CaptureScreen() {
359 webrtc::DesktopFrame* frame = queue_.current_frame()->Share();
360
361 // Pass the screen size to the helper, so it can clip the invalid region if it
362 // expands that region to a grid.
363 helper_.set_size_most_recent(frame->size());
364
365 // In the DAMAGE case, ensure the frame is up-to-date with the previous frame
366 // if any. If there isn't a previous frame, that means a screen-resolution
367 // change occurred, and |invalid_rects| will be updated to include the whole
368 // screen.
369 if (use_damage_ && queue_.previous_frame())
370 SynchronizeFrame();
371
372 webrtc::DesktopRegion* updated_region = frame->mutable_updated_region();
373
374 x_server_pixel_buffer_.Synchronize();
375 if (use_damage_ && queue_.previous_frame()) {
376 // Atomically fetch and clear the damage region.
377 XDamageSubtract(display_, damage_handle_, None, damage_region_);
378 int rects_num = 0;
379 XRectangle bounds;
380 XRectangle* rects = XFixesFetchRegionAndBounds(display_, damage_region_,
381 &rects_num, &bounds);
382 for (int i = 0; i < rects_num; ++i) {
383 updated_region->AddRect(webrtc::DesktopRect::MakeXYWH(
384 rects[i].x, rects[i].y, rects[i].width, rects[i].height));
385 }
386 XFree(rects);
387 helper_.InvalidateRegion(*updated_region);
388
389 // Capture the damaged portions of the desktop.
390 helper_.TakeInvalidRegion(updated_region);
391
392 // Clip the damaged portions to the current screen size, just in case some
393 // spurious XDamage notifications were received for a previous (larger)
394 // screen size.
395 updated_region->IntersectWith(
396 webrtc::DesktopRect::MakeSize(root_window_size_));
397 for (webrtc::DesktopRegion::Iterator it(*updated_region);
398 !it.IsAtEnd(); it.Advance()) {
399 CaptureRect(it.rect(), frame);
400 }
401 } else {
402 // Doing full-screen polling, or this is the first capture after a
403 // screen-resolution change. In either case, need a full-screen capture.
404 webrtc::DesktopRect screen_rect =
405 webrtc::DesktopRect::MakeSize(frame->size());
406 CaptureRect(screen_rect, frame);
407
408 if (queue_.previous_frame()) {
409 // Full-screen polling, so calculate the invalid rects here, based on the
410 // changed pixels between current and previous buffers.
411 DCHECK(differ_ != NULL);
412 DCHECK(queue_.previous_frame()->data());
413 differ_->CalcDirtyRegion(queue_.previous_frame()->data(),
414 frame->data(), updated_region);
415 } else {
416 // No previous buffer, so always invalidate the whole screen, whether
417 // or not DAMAGE is being used. DAMAGE doesn't necessarily send a
418 // full-screen notification after a screen-resolution change, so
419 // this is done here.
420 updated_region->SetRect(screen_rect);
421 }
422 }
423
424 return frame;
425 }
426
427 void ScreenCapturerLinux::ScreenConfigurationChanged(
428 const webrtc::DesktopSize& root_window_size) {
429 root_window_size_ = root_window_size;
430
431 // Make sure the frame buffers will be reallocated.
432 queue_.Reset();
433
434 helper_.ClearInvalidRegion();
435 x_server_pixel_buffer_.Init(display_, root_window_size_);
436 }
437
438 void ScreenCapturerLinux::SynchronizeFrame() {
439 // Synchronize the current buffer with the previous one since we do not
440 // capture the entire desktop. Note that encoder may be reading from the
441 // previous buffer at this time so thread access complaints are false
442 // positives.
443
444 // TODO(hclam): We can reduce the amount of copying here by subtracting
445 // |capturer_helper_|s region from |last_invalid_region_|.
446 // http://crbug.com/92354
447 DCHECK(queue_.previous_frame());
448
449 webrtc::DesktopFrame* current = queue_.current_frame();
450 webrtc::DesktopFrame* last = queue_.previous_frame();
451 DCHECK_NE(current, last);
452 for (webrtc::DesktopRegion::Iterator it(last_invalid_region_);
453 !it.IsAtEnd(); it.Advance()) {
454 const webrtc::DesktopRect& r = it.rect();
455 int offset = r.top() * current->stride() +
456 r.left() * webrtc::DesktopFrame::kBytesPerPixel;
457 for (int i = 0; i < r.height(); ++i) {
458 memcpy(current->data() + offset, last->data() + offset,
459 r.width() * webrtc::DesktopFrame::kBytesPerPixel);
460 offset += current->size().width() * webrtc::DesktopFrame::kBytesPerPixel;
461 }
462 }
463 }
464
465 void ScreenCapturerLinux::DeinitXlib() {
466 if (gc_) {
467 XFreeGC(display_, gc_);
468 gc_ = NULL;
469 }
470
471 x_server_pixel_buffer_.Release();
472
473 if (display_) {
474 if (damage_handle_)
475 XDamageDestroy(display_, damage_handle_);
476 if (damage_region_)
477 XFixesDestroyRegion(display_, damage_region_);
478 XCloseDisplay(display_);
479 display_ = NULL;
480 damage_handle_ = 0;
481 damage_region_ = 0;
482 }
483 }
484
485 void ScreenCapturerLinux::CaptureRect(const webrtc::DesktopRect& rect,
486 webrtc::DesktopFrame* frame) {
487 uint8* image = x_server_pixel_buffer_.CaptureRect(rect);
488 int depth = x_server_pixel_buffer_.GetDepth();
489 if ((depth == 24 || depth == 32) &&
490 x_server_pixel_buffer_.GetBitsPerPixel() == 32 &&
491 x_server_pixel_buffer_.GetRedMask() == 0xff0000 &&
492 x_server_pixel_buffer_.GetGreenMask() == 0xff00 &&
493 x_server_pixel_buffer_.GetBlueMask() == 0xff) {
494 DVLOG(3) << "Fast blitting";
495 FastBlit(image, rect, frame);
496 } else {
497 DVLOG(3) << "Slow blitting";
498 SlowBlit(image, rect, frame);
499 }
500 }
501
502 void ScreenCapturerLinux::FastBlit(uint8* image,
503 const webrtc::DesktopRect& rect,
504 webrtc::DesktopFrame* frame) {
505 uint8* src_pos = image;
506 int src_stride = x_server_pixel_buffer_.GetStride();
507 int dst_x = rect.left(), dst_y = rect.top();
508
509 uint8* dst_pos = frame->data() + frame->stride() * dst_y;
510 dst_pos += dst_x * webrtc::DesktopFrame::kBytesPerPixel;
511
512 int height = rect.height();
513 int row_bytes = rect.width() * webrtc::DesktopFrame::kBytesPerPixel;
514 for (int y = 0; y < height; ++y) {
515 memcpy(dst_pos, src_pos, row_bytes);
516 src_pos += src_stride;
517 dst_pos += frame->stride();
518 }
519 }
520
521 void ScreenCapturerLinux::SlowBlit(uint8* image,
522 const webrtc::DesktopRect& rect,
523 webrtc::DesktopFrame* frame) {
524 int src_stride = x_server_pixel_buffer_.GetStride();
525 int dst_x = rect.left(), dst_y = rect.top();
526 int width = rect.width(), height = rect.height();
527
528 uint32 red_mask = x_server_pixel_buffer_.GetRedMask();
529 uint32 green_mask = x_server_pixel_buffer_.GetGreenMask();
530 uint32 blue_mask = x_server_pixel_buffer_.GetBlueMask();
531
532 uint32 red_shift = GetRgbShift(red_mask);
533 uint32 green_shift = GetRgbShift(green_mask);
534 uint32 blue_shift = GetRgbShift(blue_mask);
535
536 unsigned int bits_per_pixel = x_server_pixel_buffer_.GetBitsPerPixel();
537
538 uint8* dst_pos = frame->data() + frame->stride() * dst_y;
539 uint8* src_pos = image;
540 dst_pos += dst_x * webrtc::DesktopFrame::kBytesPerPixel;
541 // TODO(hclam): Optimize, perhaps using MMX code or by converting to
542 // YUV directly
543 for (int y = 0; y < height; y++) {
544 uint32* dst_pos_32 = reinterpret_cast<uint32*>(dst_pos);
545 uint32* src_pos_32 = reinterpret_cast<uint32*>(src_pos);
546 uint16* src_pos_16 = reinterpret_cast<uint16*>(src_pos);
547 for (int x = 0; x < width; x++) {
548 // Dereference through an appropriately-aligned pointer.
549 uint32 pixel;
550 if (bits_per_pixel == 32)
551 pixel = src_pos_32[x];
552 else if (bits_per_pixel == 16)
553 pixel = src_pos_16[x];
554 else
555 pixel = src_pos[x];
556 uint32 r = (pixel & red_mask) << red_shift;
557 uint32 g = (pixel & green_mask) << green_shift;
558 uint32 b = (pixel & blue_mask) << blue_shift;
559
560 // Write as 32-bit RGB.
561 dst_pos_32[x] = ((r >> 8) & 0xff0000) | ((g >> 16) & 0xff00) |
562 ((b >> 24) & 0xff);
563 }
564 dst_pos += frame->stride();
565 src_pos += src_stride;
566 }
567 }
568
569 // static
570 uint32 ScreenCapturerLinux::GetRgbShift(uint32 mask) {
571 int shift = 0;
572 if ((mask & 0xffff0000u) == 0) {
573 mask <<= 16;
574 shift += 16;
575 }
576 if ((mask & 0xff000000u) == 0) {
577 mask <<= 8;
578 shift += 8;
579 }
580 if ((mask & 0xf0000000u) == 0) {
581 mask <<= 4;
582 shift += 4;
583 }
584 if ((mask & 0xc0000000u) == 0) {
585 mask <<= 2;
586 shift += 2;
587 }
588 if ((mask & 0x80000000u) == 0)
589 shift += 1;
590
591 return shift;
592 }
593
594 } // namespace
595
596 // static
597 scoped_ptr<ScreenCapturer> ScreenCapturer::Create() {
598 scoped_ptr<ScreenCapturerLinux> capturer(new ScreenCapturerLinux());
599 if (!capturer->Init(false))
600 capturer.reset();
601 return capturer.PassAs<ScreenCapturer>();
602 }
603
604 // static
605 scoped_ptr<ScreenCapturer> ScreenCapturer::CreateWithXDamage(
606 bool use_x_damage) {
607 scoped_ptr<ScreenCapturerLinux> capturer(new ScreenCapturerLinux());
608 if (!capturer->Init(use_x_damage))
609 capturer.reset();
610 return capturer.PassAs<ScreenCapturer>();
611 }
612
613 } // namespace media
OLDNEW
« no previous file with comments | « media/video/capture/screen/screen_capturer_win.cc ('k') | media/video/capture/screen/shared_desktop_frame.h » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698