blob: f0d2c1284c8a6fc449521bdde8cea594bd7e47c0 [file] [log] [blame]
/*
* Copyright (c) 2013 The WebRTC project authors. All Rights Reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
#include "webrtc/modules/desktop_capture/window_capturer.h"
#include <assert.h>
#include <string.h>
#include <X11/Xatom.h>
#include <X11/extensions/Xcomposite.h>
#include <X11/extensions/Xrender.h>
#include <X11/Xutil.h>
#include <algorithm>
#include "webrtc/base/scoped_ptr.h"
#include "webrtc/base/scoped_ref_ptr.h"
#include "webrtc/modules/desktop_capture/desktop_capture_options.h"
#include "webrtc/modules/desktop_capture/desktop_frame.h"
#include "webrtc/modules/desktop_capture/x11/shared_x_display.h"
#include "webrtc/modules/desktop_capture/x11/x_error_trap.h"
#include "webrtc/modules/desktop_capture/x11/x_server_pixel_buffer.h"
#include "webrtc/system_wrappers/include/logging.h"
namespace webrtc {
namespace {
// Convenience wrapper for XGetWindowProperty() results.
template <class PropertyType>
class XWindowProperty {
public:
XWindowProperty(Display* display, Window window, Atom property)
: is_valid_(false),
size_(0),
data_(NULL) {
const int kBitsPerByte = 8;
Atom actual_type;
int actual_format;
unsigned long bytes_after; // NOLINT: type required by XGetWindowProperty
int status = XGetWindowProperty(display, window, property, 0L, ~0L, False,
AnyPropertyType, &actual_type,
&actual_format, &size_,
&bytes_after, &data_);
if (status != Success) {
data_ = NULL;
return;
}
if (sizeof(PropertyType) * kBitsPerByte != actual_format) {
size_ = 0;
return;
}
is_valid_ = true;
}
~XWindowProperty() {
if (data_)
XFree(data_);
}
// True if we got properly value successfully.
bool is_valid() const { return is_valid_; }
// Size and value of the property.
size_t size() const { return size_; }
const PropertyType* data() const {
return reinterpret_cast<PropertyType*>(data_);
}
PropertyType* data() {
return reinterpret_cast<PropertyType*>(data_);
}
private:
bool is_valid_;
unsigned long size_; // NOLINT: type required by XGetWindowProperty
unsigned char* data_;
RTC_DISALLOW_COPY_AND_ASSIGN(XWindowProperty);
};
class WindowCapturerLinux : public WindowCapturer,
public SharedXDisplay::XEventHandler {
public:
WindowCapturerLinux(const DesktopCaptureOptions& options);
virtual ~WindowCapturerLinux();
// WindowCapturer interface.
bool GetWindowList(WindowList* windows) override;
bool SelectWindow(WindowId id) override;
bool BringSelectedWindowToFront() override;
// DesktopCapturer interface.
void Start(Callback* callback) override;
void Capture(const DesktopRegion& region) override;
// SharedXDisplay::XEventHandler interface.
bool HandleXEvent(const XEvent& event) override;
private:
Display* display() { return x_display_->display(); }
// Iterates through |window| hierarchy to find first visible window, i.e. one
// that has WM_STATE property set to NormalState.
// See http://tronche.com/gui/x/icccm/sec-4.html#s-4.1.3.1 .
::Window GetApplicationWindow(::Window window);
// Returns true if the |window| is a desktop element.
bool IsDesktopElement(::Window window);
// Returns window title for the specified X |window|.
bool GetWindowTitle(::Window window, std::string* title);
Callback* callback_;
rtc::scoped_refptr<SharedXDisplay> x_display_;
Atom wm_state_atom_;
Atom window_type_atom_;
Atom normal_window_type_atom_;
bool has_composite_extension_;
::Window selected_window_;
XServerPixelBuffer x_server_pixel_buffer_;
RTC_DISALLOW_COPY_AND_ASSIGN(WindowCapturerLinux);
};
WindowCapturerLinux::WindowCapturerLinux(const DesktopCaptureOptions& options)
: callback_(NULL),
x_display_(options.x_display()),
has_composite_extension_(false),
selected_window_(0) {
// Create Atoms so we don't need to do it every time they are used.
wm_state_atom_ = XInternAtom(display(), "WM_STATE", True);
window_type_atom_ = XInternAtom(display(), "_NET_WM_WINDOW_TYPE", True);
normal_window_type_atom_ = XInternAtom(
display(), "_NET_WM_WINDOW_TYPE_NORMAL", True);
int event_base, error_base, major_version, minor_version;
if (XCompositeQueryExtension(display(), &event_base, &error_base) &&
XCompositeQueryVersion(display(), &major_version, &minor_version) &&
// XCompositeNameWindowPixmap() requires version 0.2
(major_version > 0 || minor_version >= 2)) {
has_composite_extension_ = true;
} else {
LOG(LS_INFO) << "Xcomposite extension not available or too old.";
}
x_display_->AddEventHandler(ConfigureNotify, this);
}
WindowCapturerLinux::~WindowCapturerLinux() {
x_display_->RemoveEventHandler(ConfigureNotify, this);
}
bool WindowCapturerLinux::GetWindowList(WindowList* windows) {
WindowList result;
XErrorTrap error_trap(display());
int num_screens = XScreenCount(display());
for (int screen = 0; screen < num_screens; ++screen) {
::Window root_window = XRootWindow(display(), screen);
::Window parent;
::Window *children;
unsigned int num_children;
int status = XQueryTree(display(), root_window, &root_window, &parent,
&children, &num_children);
if (status == 0) {
LOG(LS_ERROR) << "Failed to query for child windows for screen "
<< screen;
continue;
}
for (unsigned int i = 0; i < num_children; ++i) {
// Iterate in reverse order to return windows from front to back.
::Window app_window =
GetApplicationWindow(children[num_children - 1 - i]);
if (app_window && !IsDesktopElement(app_window)) {
Window w;
w.id = app_window;
if (GetWindowTitle(app_window, &w.title))
result.push_back(w);
}
}
if (children)
XFree(children);
}
windows->swap(result);
return true;
}
bool WindowCapturerLinux::SelectWindow(WindowId id) {
if (!x_server_pixel_buffer_.Init(display(), id))
return false;
// Tell the X server to send us window resizing events.
XSelectInput(display(), id, StructureNotifyMask);
selected_window_ = id;
// In addition to needing X11 server-side support for Xcomposite, it actually
// needs to be turned on for the window. If the user has modern
// hardware/drivers but isn't using a compositing window manager, that won't
// be the case. Here we automatically turn it on.
// Redirect drawing to an offscreen buffer (ie, turn on compositing). X11
// remembers who has requested this and will turn it off for us when we exit.
XCompositeRedirectWindow(display(), id, CompositeRedirectAutomatic);
return true;
}
bool WindowCapturerLinux::BringSelectedWindowToFront() {
if (!selected_window_)
return false;
unsigned int num_children;
::Window* children;
::Window parent;
::Window root;
// Find the root window to pass event to.
int status = XQueryTree(
display(), selected_window_, &root, &parent, &children, &num_children);
if (status == 0) {
LOG(LS_ERROR) << "Failed to query for the root window.";
return false;
}
if (children)
XFree(children);
XRaiseWindow(display(), selected_window_);
// Some window managers (e.g., metacity in GNOME) consider it illegal to
// raise a window without also giving it input focus with
// _NET_ACTIVE_WINDOW, so XRaiseWindow() on its own isn't enough.
Atom atom = XInternAtom(display(), "_NET_ACTIVE_WINDOW", True);
if (atom != None) {
XEvent xev;
xev.xclient.type = ClientMessage;
xev.xclient.serial = 0;
xev.xclient.send_event = True;
xev.xclient.window = selected_window_;
xev.xclient.message_type = atom;
// The format member is set to 8, 16, or 32 and specifies whether the
// data should be viewed as a list of bytes, shorts, or longs.
xev.xclient.format = 32;
memset(xev.xclient.data.l, 0, sizeof(xev.xclient.data.l));
XSendEvent(display(),
root,
False,
SubstructureRedirectMask | SubstructureNotifyMask,
&xev);
}
XFlush(display());
return true;
}
void WindowCapturerLinux::Start(Callback* callback) {
assert(!callback_);
assert(callback);
callback_ = callback;
}
void WindowCapturerLinux::Capture(const DesktopRegion& region) {
if (!x_server_pixel_buffer_.IsWindowValid()) {
LOG(LS_INFO) << "The window is no longer valid.";
callback_->OnCaptureCompleted(NULL);
return;
}
x_display_->ProcessPendingXEvents();
if (!has_composite_extension_) {
// Without the Xcomposite extension we capture when the whole window is
// visible on screen and not covered by any other window. This is not
// something we want so instead, just bail out.
LOG(LS_INFO) << "No Xcomposite extension detected.";
callback_->OnCaptureCompleted(NULL);
return;
}
DesktopFrame* frame =
new BasicDesktopFrame(x_server_pixel_buffer_.window_size());
x_server_pixel_buffer_.Synchronize();
x_server_pixel_buffer_.CaptureRect(DesktopRect::MakeSize(frame->size()),
frame);
frame->mutable_updated_region()->SetRect(
DesktopRect::MakeSize(frame->size()));
callback_->OnCaptureCompleted(frame);
}
bool WindowCapturerLinux::HandleXEvent(const XEvent& event) {
if (event.type == ConfigureNotify) {
XConfigureEvent xce = event.xconfigure;
if (!DesktopSize(xce.width, xce.height).equals(
x_server_pixel_buffer_.window_size())) {
if (!x_server_pixel_buffer_.Init(display(), selected_window_)) {
LOG(LS_ERROR) << "Failed to initialize pixel buffer after resizing.";
}
return true;
}
}
return false;
}
::Window WindowCapturerLinux::GetApplicationWindow(::Window window) {
// Get WM_STATE property of the window.
XWindowProperty<uint32_t> window_state(display(), window, wm_state_atom_);
// WM_STATE is considered to be set to WithdrawnState when it missing.
int32_t state = window_state.is_valid() ?
*window_state.data() : WithdrawnState;
if (state == NormalState) {
// Window has WM_STATE==NormalState. Return it.
return window;
} else if (state == IconicState) {
// Window is in minimized. Skip it.
return 0;
}
// If the window is in WithdrawnState then look at all of its children.
::Window root, parent;
::Window *children;
unsigned int num_children;
if (!XQueryTree(display(), window, &root, &parent, &children,
&num_children)) {
LOG(LS_ERROR) << "Failed to query for child windows although window"
<< "does not have a valid WM_STATE.";
return 0;
}
::Window app_window = 0;
for (unsigned int i = 0; i < num_children; ++i) {
app_window = GetApplicationWindow(children[i]);
if (app_window)
break;
}
if (children)
XFree(children);
return app_window;
}
bool WindowCapturerLinux::IsDesktopElement(::Window window) {
if (window == 0)
return false;
// First look for _NET_WM_WINDOW_TYPE. The standard
// (http://standards.freedesktop.org/wm-spec/latest/ar01s05.html#id2760306)
// says this hint *should* be present on all windows, and we use the existence
// of _NET_WM_WINDOW_TYPE_NORMAL in the property to indicate a window is not
// a desktop element (that is, only "normal" windows should be shareable).
XWindowProperty<uint32_t> window_type(display(), window, window_type_atom_);
if (window_type.is_valid() && window_type.size() > 0) {
uint32_t* end = window_type.data() + window_type.size();
bool is_normal = (end != std::find(
window_type.data(), end, normal_window_type_atom_));
return !is_normal;
}
// Fall back on using the hint.
XClassHint class_hint;
Status status = XGetClassHint(display(), window, &class_hint);
bool result = false;
if (status == 0) {
// No hints, assume this is a normal application window.
return result;
}
if (strcmp("gnome-panel", class_hint.res_name) == 0 ||
strcmp("desktop_window", class_hint.res_name) == 0) {
result = true;
}
XFree(class_hint.res_name);
XFree(class_hint.res_class);
return result;
}
bool WindowCapturerLinux::GetWindowTitle(::Window window, std::string* title) {
int status;
bool result = false;
XTextProperty window_name;
window_name.value = NULL;
if (window) {
status = XGetWMName(display(), window, &window_name);
if (status && window_name.value && window_name.nitems) {
int cnt;
char **list = NULL;
status = Xutf8TextPropertyToTextList(display(), &window_name, &list,
&cnt);
if (status >= Success && cnt && *list) {
if (cnt > 1) {
LOG(LS_INFO) << "Window has " << cnt
<< " text properties, only using the first one.";
}
*title = *list;
result = true;
}
if (list)
XFreeStringList(list);
}
if (window_name.value)
XFree(window_name.value);
}
return result;
}
} // namespace
// static
WindowCapturer* WindowCapturer::Create(const DesktopCaptureOptions& options) {
if (!options.x_display())
return NULL;
return new WindowCapturerLinux(options);
}
} // namespace webrtc