1/*
2 *  Copyright (c) 2013 The WebRTC project authors. All Rights Reserved.
3 *
4 *  Use of this source code is governed by a BSD-style license
5 *  that can be found in the LICENSE file in the root of the source
6 *  tree. An additional intellectual property rights grant can be found
7 *  in the file PATENTS.  All contributing project authors may
8 *  be found in the AUTHORS file in the root of the source tree.
9 */
10
11#include "webrtc/modules/desktop_capture/window_capturer.h"
12
13#include <assert.h>
14#include <string.h>
15#include <X11/Xatom.h>
16#include <X11/extensions/Xcomposite.h>
17#include <X11/extensions/Xrender.h>
18#include <X11/Xutil.h>
19
20#include <algorithm>
21
22#include "webrtc/modules/desktop_capture/desktop_capture_options.h"
23#include "webrtc/modules/desktop_capture/desktop_frame.h"
24#include "webrtc/modules/desktop_capture/x11/shared_x_display.h"
25#include "webrtc/modules/desktop_capture/x11/x_error_trap.h"
26#include "webrtc/modules/desktop_capture/x11/x_server_pixel_buffer.h"
27#include "webrtc/system_wrappers/interface/logging.h"
28#include "webrtc/system_wrappers/interface/scoped_ptr.h"
29#include "webrtc/system_wrappers/interface/scoped_refptr.h"
30
31namespace webrtc {
32
33namespace {
34
35// Convenience wrapper for XGetWindowProperty() results.
36template <class PropertyType>
37class XWindowProperty {
38 public:
39  XWindowProperty(Display* display, Window window, Atom property)
40      : is_valid_(false),
41        size_(0),
42        data_(NULL) {
43    const int kBitsPerByte = 8;
44    Atom actual_type;
45    int actual_format;
46    unsigned long bytes_after;  // NOLINT: type required by XGetWindowProperty
47    int status = XGetWindowProperty(display, window, property, 0L, ~0L, False,
48                                    AnyPropertyType, &actual_type,
49                                    &actual_format, &size_,
50                                    &bytes_after, &data_);
51    if (status != Success) {
52      data_ = NULL;
53      return;
54    }
55    if (sizeof(PropertyType) * kBitsPerByte != actual_format) {
56      size_ = 0;
57      return;
58    }
59
60    is_valid_ = true;
61  }
62
63  ~XWindowProperty() {
64    if (data_)
65      XFree(data_);
66  }
67
68  // True if we got properly value successfully.
69  bool is_valid() const { return is_valid_; }
70
71  // Size and value of the property.
72  size_t size() const { return size_; }
73  const PropertyType* data() const {
74    return reinterpret_cast<PropertyType*>(data_);
75  }
76  PropertyType* data() {
77    return reinterpret_cast<PropertyType*>(data_);
78  }
79
80 private:
81  bool is_valid_;
82  unsigned long size_;  // NOLINT: type required by XGetWindowProperty
83  unsigned char* data_;
84
85  DISALLOW_COPY_AND_ASSIGN(XWindowProperty);
86};
87
88class WindowCapturerLinux : public WindowCapturer,
89                            public SharedXDisplay::XEventHandler {
90 public:
91  WindowCapturerLinux(const DesktopCaptureOptions& options);
92  virtual ~WindowCapturerLinux();
93
94  // WindowCapturer interface.
95  virtual bool GetWindowList(WindowList* windows) OVERRIDE;
96  virtual bool SelectWindow(WindowId id) OVERRIDE;
97  virtual bool BringSelectedWindowToFront() OVERRIDE;
98
99  // DesktopCapturer interface.
100  virtual void Start(Callback* callback) OVERRIDE;
101  virtual void Capture(const DesktopRegion& region) OVERRIDE;
102
103  // SharedXDisplay::XEventHandler interface.
104  virtual bool HandleXEvent(const XEvent& event) OVERRIDE;
105
106 private:
107  Display* display() { return x_display_->display(); }
108
109  // Iterates through |window| hierarchy to find first visible window, i.e. one
110  // that has WM_STATE property set to NormalState.
111  // See http://tronche.com/gui/x/icccm/sec-4.html#s-4.1.3.1 .
112  ::Window GetApplicationWindow(::Window window);
113
114  // Returns true if the |window| is a desktop element.
115  bool IsDesktopElement(::Window window);
116
117  // Returns window title for the specified X |window|.
118  bool GetWindowTitle(::Window window, std::string* title);
119
120  Callback* callback_;
121
122  scoped_refptr<SharedXDisplay> x_display_;
123
124  Atom wm_state_atom_;
125  Atom window_type_atom_;
126  Atom normal_window_type_atom_;
127  bool has_composite_extension_;
128
129  ::Window selected_window_;
130  XServerPixelBuffer x_server_pixel_buffer_;
131
132  DISALLOW_COPY_AND_ASSIGN(WindowCapturerLinux);
133};
134
135WindowCapturerLinux::WindowCapturerLinux(const DesktopCaptureOptions& options)
136    : callback_(NULL),
137      x_display_(options.x_display()),
138      has_composite_extension_(false),
139      selected_window_(0) {
140  // Create Atoms so we don't need to do it every time they are used.
141  wm_state_atom_ = XInternAtom(display(), "WM_STATE", True);
142  window_type_atom_ = XInternAtom(display(), "_NET_WM_WINDOW_TYPE", True);
143  normal_window_type_atom_ = XInternAtom(
144      display(), "_NET_WM_WINDOW_TYPE_NORMAL", True);
145
146  int event_base, error_base, major_version, minor_version;
147  if (XCompositeQueryExtension(display(), &event_base, &error_base) &&
148      XCompositeQueryVersion(display(), &major_version, &minor_version) &&
149      // XCompositeNameWindowPixmap() requires version 0.2
150      (major_version > 0 || minor_version >= 2)) {
151    has_composite_extension_ = true;
152  } else {
153    LOG(LS_INFO) << "Xcomposite extension not available or too old.";
154  }
155
156  x_display_->AddEventHandler(ConfigureNotify, this);
157}
158
159WindowCapturerLinux::~WindowCapturerLinux() {
160  x_display_->RemoveEventHandler(ConfigureNotify, this);
161}
162
163bool WindowCapturerLinux::GetWindowList(WindowList* windows) {
164  WindowList result;
165
166  XErrorTrap error_trap(display());
167
168  int num_screens = XScreenCount(display());
169  for (int screen = 0; screen < num_screens; ++screen) {
170    ::Window root_window = XRootWindow(display(), screen);
171    ::Window parent;
172    ::Window *children;
173    unsigned int num_children;
174    int status = XQueryTree(display(), root_window, &root_window, &parent,
175                            &children, &num_children);
176    if (status == 0) {
177      LOG(LS_ERROR) << "Failed to query for child windows for screen "
178                    << screen;
179      continue;
180    }
181
182    for (unsigned int i = 0; i < num_children; ++i) {
183      // Iterate in reverse order to return windows from front to back.
184      ::Window app_window =
185          GetApplicationWindow(children[num_children - 1 - i]);
186      if (app_window && !IsDesktopElement(app_window)) {
187        Window w;
188        w.id = app_window;
189        if (GetWindowTitle(app_window, &w.title))
190          result.push_back(w);
191      }
192    }
193
194    if (children)
195      XFree(children);
196  }
197
198  windows->swap(result);
199
200  return true;
201}
202
203bool WindowCapturerLinux::SelectWindow(WindowId id) {
204  if (!x_server_pixel_buffer_.Init(display(), id))
205    return false;
206
207  // Tell the X server to send us window resizing events.
208  XSelectInput(display(), id, StructureNotifyMask);
209
210  selected_window_ = id;
211
212  // In addition to needing X11 server-side support for Xcomposite, it actually
213  // needs to be turned on for the window. If the user has modern
214  // hardware/drivers but isn't using a compositing window manager, that won't
215  // be the case. Here we automatically turn it on.
216
217  // Redirect drawing to an offscreen buffer (ie, turn on compositing). X11
218  // remembers who has requested this and will turn it off for us when we exit.
219  XCompositeRedirectWindow(display(), id, CompositeRedirectAutomatic);
220
221  return true;
222}
223
224bool WindowCapturerLinux::BringSelectedWindowToFront() {
225  if (!selected_window_)
226    return false;
227
228  unsigned int num_children;
229  ::Window* children;
230  ::Window parent;
231  ::Window root;
232  // Find the root window to pass event to.
233  int status = XQueryTree(
234      display(), selected_window_, &root, &parent, &children, &num_children);
235  if (status == 0) {
236    LOG(LS_ERROR) << "Failed to query for the root window.";
237    return false;
238  }
239
240  if (children)
241    XFree(children);
242
243  XRaiseWindow(display(), selected_window_);
244
245  // Some window managers (e.g., metacity in GNOME) consider it illegal to
246  // raise a window without also giving it input focus with
247  // _NET_ACTIVE_WINDOW, so XRaiseWindow() on its own isn't enough.
248  Atom atom = XInternAtom(display(), "_NET_ACTIVE_WINDOW", True);
249  if (atom != None) {
250    XEvent xev;
251    xev.xclient.type = ClientMessage;
252    xev.xclient.serial = 0;
253    xev.xclient.send_event = True;
254    xev.xclient.window = selected_window_;
255    xev.xclient.message_type = atom;
256
257    // The format member is set to 8, 16, or 32 and specifies whether the
258    // data should be viewed as a list of bytes, shorts, or longs.
259    xev.xclient.format = 32;
260
261    memset(xev.xclient.data.l, 0, sizeof(xev.xclient.data.l));
262
263    XSendEvent(display(),
264               root,
265               False,
266               SubstructureRedirectMask | SubstructureNotifyMask,
267               &xev);
268  }
269  XFlush(display());
270  return true;
271}
272
273void WindowCapturerLinux::Start(Callback* callback) {
274  assert(!callback_);
275  assert(callback);
276
277  callback_ = callback;
278}
279
280void WindowCapturerLinux::Capture(const DesktopRegion& region) {
281  if (!x_server_pixel_buffer_.IsWindowValid()) {
282    LOG(LS_INFO) << "The window is no longer valid.";
283    callback_->OnCaptureCompleted(NULL);
284    return;
285  }
286
287  x_display_->ProcessPendingXEvents();
288
289  if (!has_composite_extension_) {
290    // Without the Xcomposite extension we capture when the whole window is
291    // visible on screen and not covered by any other window. This is not
292    // something we want so instead, just bail out.
293    LOG(LS_INFO) << "No Xcomposite extension detected.";
294    callback_->OnCaptureCompleted(NULL);
295    return;
296  }
297
298  DesktopFrame* frame =
299      new BasicDesktopFrame(x_server_pixel_buffer_.window_size());
300
301  x_server_pixel_buffer_.Synchronize();
302  x_server_pixel_buffer_.CaptureRect(DesktopRect::MakeSize(frame->size()),
303                                     frame);
304
305  frame->mutable_updated_region()->SetRect(
306      DesktopRect::MakeSize(frame->size()));
307
308  callback_->OnCaptureCompleted(frame);
309}
310
311bool WindowCapturerLinux::HandleXEvent(const XEvent& event) {
312  if (event.type == ConfigureNotify) {
313    XConfigureEvent xce = event.xconfigure;
314    if (!DesktopSize(xce.width, xce.height).equals(
315            x_server_pixel_buffer_.window_size())) {
316      if (!x_server_pixel_buffer_.Init(display(), selected_window_)) {
317        LOG(LS_ERROR) << "Failed to initialize pixel buffer after resizing.";
318      }
319      return true;
320    }
321  }
322  return false;
323}
324
325::Window WindowCapturerLinux::GetApplicationWindow(::Window window) {
326  // Get WM_STATE property of the window.
327  XWindowProperty<uint32_t> window_state(display(), window, wm_state_atom_);
328
329  // WM_STATE is considered to be set to WithdrawnState when it missing.
330  int32_t state = window_state.is_valid() ?
331      *window_state.data() : WithdrawnState;
332
333  if (state == NormalState) {
334    // Window has WM_STATE==NormalState. Return it.
335    return window;
336  } else if (state == IconicState) {
337    // Window is in minimized. Skip it.
338    return 0;
339  }
340
341  // If the window is in WithdrawnState then look at all of its children.
342  ::Window root, parent;
343  ::Window *children;
344  unsigned int num_children;
345  if (!XQueryTree(display(), window, &root, &parent, &children,
346                  &num_children)) {
347    LOG(LS_ERROR) << "Failed to query for child windows although window"
348                  << "does not have a valid WM_STATE.";
349    return 0;
350  }
351  ::Window app_window = 0;
352  for (unsigned int i = 0; i < num_children; ++i) {
353    app_window = GetApplicationWindow(children[i]);
354    if (app_window)
355      break;
356  }
357
358  if (children)
359    XFree(children);
360  return app_window;
361}
362
363bool WindowCapturerLinux::IsDesktopElement(::Window window) {
364  if (window == 0)
365    return false;
366
367  // First look for _NET_WM_WINDOW_TYPE. The standard
368  // (http://standards.freedesktop.org/wm-spec/latest/ar01s05.html#id2760306)
369  // says this hint *should* be present on all windows, and we use the existence
370  // of _NET_WM_WINDOW_TYPE_NORMAL in the property to indicate a window is not
371  // a desktop element (that is, only "normal" windows should be shareable).
372  XWindowProperty<uint32_t> window_type(display(), window, window_type_atom_);
373  if (window_type.is_valid() && window_type.size() > 0) {
374    uint32_t* end = window_type.data() + window_type.size();
375    bool is_normal = (end != std::find(
376        window_type.data(), end, normal_window_type_atom_));
377    return !is_normal;
378  }
379
380  // Fall back on using the hint.
381  XClassHint class_hint;
382  Status status = XGetClassHint(display(), window, &class_hint);
383  bool result = false;
384  if (status == 0) {
385    // No hints, assume this is a normal application window.
386    return result;
387  }
388
389  if (strcmp("gnome-panel", class_hint.res_name) == 0 ||
390      strcmp("desktop_window", class_hint.res_name) == 0) {
391    result = true;
392  }
393  XFree(class_hint.res_name);
394  XFree(class_hint.res_class);
395  return result;
396}
397
398bool WindowCapturerLinux::GetWindowTitle(::Window window, std::string* title) {
399  int status;
400  bool result = false;
401  XTextProperty window_name;
402  window_name.value = NULL;
403  if (window) {
404    status = XGetWMName(display(), window, &window_name);
405    if (status && window_name.value && window_name.nitems) {
406      int cnt;
407      char **list = NULL;
408      status = Xutf8TextPropertyToTextList(display(), &window_name, &list,
409                                           &cnt);
410      if (status >= Success && cnt && *list) {
411        if (cnt > 1) {
412          LOG(LS_INFO) << "Window has " << cnt
413                       << " text properties, only using the first one.";
414        }
415        *title = *list;
416        result = true;
417      }
418      if (list)
419        XFreeStringList(list);
420    }
421    if (window_name.value)
422      XFree(window_name.value);
423  }
424  return result;
425}
426
427}  // namespace
428
429// static
430WindowCapturer* WindowCapturer::Create(const DesktopCaptureOptions& options) {
431  if (!options.x_display())
432    return NULL;
433  return new WindowCapturerLinux(options);
434}
435
436}  // namespace webrtc
437