1 /*
2  *  Copyright (c) 2014 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 "modules/desktop_capture/cropping_window_capturer.h"
12 #include "modules/desktop_capture/desktop_capturer_differ_wrapper.h"
13 #include "modules/desktop_capture/win/screen_capture_utils.h"
14 #include "modules/desktop_capture/win/selected_window_context.h"
15 #include "modules/desktop_capture/win/window_capture_utils.h"
16 #include "rtc_base/logging.h"
17 #include "rtc_base/trace_event.h"
18 #include "rtc_base/win32.h"
19 
20 namespace webrtc {
21 
22 namespace {
23 
24 // Used to pass input data for verifying the selected window is on top.
25 struct TopWindowVerifierContext : public SelectedWindowContext {
TopWindowVerifierContextwebrtc::__anona23449c80111::TopWindowVerifierContext26   TopWindowVerifierContext(HWND selected_window,
27                            HWND excluded_window,
28                            DesktopRect selected_window_rect,
29                            WindowCaptureHelperWin* window_capture_helper)
30       : SelectedWindowContext(selected_window,
31                               selected_window_rect,
32                               window_capture_helper),
33         excluded_window(excluded_window) {
34     RTC_DCHECK_NE(selected_window, excluded_window);
35   }
36 
37   // Determines whether the selected window is on top (not occluded by any
38   // windows except for those it owns or any excluded window).
IsTopWindowwebrtc::__anona23449c80111::TopWindowVerifierContext39   bool IsTopWindow() {
40     if (!IsSelectedWindowValid()) {
41       return false;
42     }
43 
44     // Enumerate all top-level windows above the selected window in Z-order,
45     // checking whether any overlaps it. This uses FindWindowEx rather than
46     // EnumWindows because the latter excludes certain system windows (e.g. the
47     // Start menu & other taskbar menus) that should be detected here to avoid
48     // inadvertent capture.
49     int num_retries = 0;
50     while (true) {
51       HWND hwnd = nullptr;
52       while ((hwnd = FindWindowEx(nullptr, hwnd, nullptr, nullptr))) {
53         if (hwnd == selected_window()) {
54           // Windows are enumerated in top-down Z-order, so we can stop
55           // enumerating upon reaching the selected window & report it's on top.
56           return true;
57         }
58 
59         // Ignore the excluded window.
60         if (hwnd == excluded_window) {
61           continue;
62         }
63 
64         // Ignore windows that aren't visible on the current desktop.
65         if (!window_capture_helper()->IsWindowVisibleOnCurrentDesktop(hwnd)) {
66           continue;
67         }
68 
69         // Ignore Chrome notification windows, especially the notification for
70         // the ongoing window sharing. Notes:
71         // - This only works with notifications from Chrome, not other Apps.
72         // - All notifications from Chrome will be ignored.
73         // - This may cause part or whole of notification window being cropped
74         // into the capturing of the target window if there is overlapping.
75         if (window_capture_helper()->IsWindowChromeNotification(hwnd)) {
76           continue;
77         }
78 
79         // Ignore windows owned by the selected window since we want to capture
80         // them.
81         if (IsWindowOwnedBySelectedWindow(hwnd)) {
82           continue;
83         }
84 
85         // Check whether this window intersects with the selected window.
86         if (IsWindowOverlappingSelectedWindow(hwnd)) {
87           // If intersection is not empty, the selected window is not on top.
88           return false;
89         }
90       }
91 
92       DWORD lastError = GetLastError();
93       if (lastError == ERROR_SUCCESS) {
94         // The enumeration completed successfully without finding the selected
95         // window (which may have been closed).
96         RTC_LOG(LS_WARNING) << "Failed to find selected window (only expected "
97                                "if it was closed)";
98         RTC_DCHECK(!IsWindow(selected_window()));
99         return false;
100       } else if (lastError == ERROR_INVALID_WINDOW_HANDLE) {
101         // This error may occur if a window is closed around the time it's
102         // enumerated; retry the enumeration in this case up to 10 times
103         // (this should be a rare race & unlikely to recur).
104         if (++num_retries <= 10) {
105           RTC_LOG(LS_WARNING) << "Enumeration failed due to race with a window "
106                                  "closing; retrying - retry #"
107                               << num_retries;
108           continue;
109         } else {
110           RTC_LOG(LS_ERROR)
111               << "Exhausted retry allowance around window enumeration failures "
112                  "due to races with windows closing";
113         }
114       }
115 
116       // The enumeration failed with an unexpected error (or more repeats of
117       // an infrequently-expected error than anticipated). After logging this &
118       // firing an assert when enabled, report that the selected window isn't
119       // topmost to avoid inadvertent capture of other windows.
120       RTC_LOG(LS_ERROR) << "Failed to enumerate windows: " << lastError;
121       RTC_DCHECK(false);
122       return false;
123     }
124   }
125 
126   const HWND excluded_window;
127 };
128 
129 class CroppingWindowCapturerWin : public CroppingWindowCapturer {
130  public:
CroppingWindowCapturerWin(const DesktopCaptureOptions & options)131   explicit CroppingWindowCapturerWin(const DesktopCaptureOptions& options)
132       : CroppingWindowCapturer(options),
133         full_screen_window_detector_(options.full_screen_window_detector()) {}
134 
135   void CaptureFrame() override;
136 
137  private:
138   bool ShouldUseScreenCapturer() override;
139   DesktopRect GetWindowRectInVirtualScreen() override;
140 
141   // Returns either selected by user sourceId or sourceId provided by
142   // FullScreenWindowDetector
143   WindowId GetWindowToCapture() const;
144 
145   // The region from GetWindowRgn in the desktop coordinate if the region is
146   // rectangular, or the rect from GetWindowRect if the region is not set.
147   DesktopRect window_region_rect_;
148 
149   WindowCaptureHelperWin window_capture_helper_;
150 
151   rtc::scoped_refptr<FullScreenWindowDetector> full_screen_window_detector_;
152 };
153 
CaptureFrame()154 void CroppingWindowCapturerWin::CaptureFrame() {
155   DesktopCapturer* win_capturer = window_capturer();
156   if (win_capturer) {
157     // Feed the actual list of windows into full screen window detector.
158     if (full_screen_window_detector_) {
159       full_screen_window_detector_->UpdateWindowListIfNeeded(
160           selected_window(), [this](DesktopCapturer::SourceList* sources) {
161             // Get the list of top level windows, including ones with empty
162             // title. win_capturer_->GetSourceList can't be used here
163             // cause it filters out the windows with empty titles and
164             // it uses responsiveness check which could lead to performance
165             // issues.
166             SourceList result;
167             if (!webrtc::GetWindowList(GetWindowListFlags::kNone, &result))
168               return false;
169 
170             // Filter out windows not visible on current desktop
171             auto it = std::remove_if(
172                 result.begin(), result.end(), [this](const auto& source) {
173                   HWND hwnd = reinterpret_cast<HWND>(source.id);
174                   return !window_capture_helper_
175                               .IsWindowVisibleOnCurrentDesktop(hwnd);
176                 });
177             result.erase(it, result.end());
178 
179             sources->swap(result);
180             return true;
181           });
182     }
183     win_capturer->SelectSource(GetWindowToCapture());
184   }
185 
186   CroppingWindowCapturer::CaptureFrame();
187 }
188 
ShouldUseScreenCapturer()189 bool CroppingWindowCapturerWin::ShouldUseScreenCapturer() {
190   if (!rtc::IsWindows8OrLater() && window_capture_helper_.IsAeroEnabled()) {
191     return false;
192   }
193 
194   const HWND selected = reinterpret_cast<HWND>(GetWindowToCapture());
195   // Check if the window is visible on current desktop.
196   if (!window_capture_helper_.IsWindowVisibleOnCurrentDesktop(selected)) {
197     return false;
198   }
199 
200   // Check if the window is a translucent layered window.
201   const LONG window_ex_style = GetWindowLong(selected, GWL_EXSTYLE);
202   if (window_ex_style & WS_EX_LAYERED) {
203     COLORREF color_ref_key = 0;
204     BYTE alpha = 0;
205     DWORD flags = 0;
206 
207     // GetLayeredWindowAttributes fails if the window was setup with
208     // UpdateLayeredWindow. We have no way to know the opacity of the window in
209     // that case. This happens for Stiky Note (crbug/412726).
210     if (!GetLayeredWindowAttributes(selected, &color_ref_key, &alpha, &flags))
211       return false;
212 
213     // UpdateLayeredWindow is the only way to set per-pixel alpha and will cause
214     // the previous GetLayeredWindowAttributes to fail. So we only need to check
215     // the window wide color key or alpha.
216     if ((flags & LWA_COLORKEY) || ((flags & LWA_ALPHA) && (alpha < 255))) {
217       return false;
218     }
219   }
220 
221   if (!GetWindowRect(selected, &window_region_rect_)) {
222     return false;
223   }
224 
225   DesktopRect content_rect;
226   if (!GetWindowContentRect(selected, &content_rect)) {
227     return false;
228   }
229 
230   DesktopRect region_rect;
231   // Get the window region and check if it is rectangular.
232   const int region_type =
233       GetWindowRegionTypeWithBoundary(selected, &region_rect);
234 
235   // Do not use the screen capturer if the region is empty or not rectangular.
236   if (region_type == COMPLEXREGION || region_type == NULLREGION) {
237     return false;
238   }
239 
240   if (region_type == SIMPLEREGION) {
241     // The |region_rect| returned from GetRgnBox() is always in window
242     // coordinate.
243     region_rect.Translate(window_region_rect_.left(),
244                           window_region_rect_.top());
245     // MSDN: The window region determines the area *within* the window where the
246     // system permits drawing.
247     // https://msdn.microsoft.com/en-us/library/windows/desktop/dd144950(v=vs.85).aspx.
248     //
249     // |region_rect| should always be inside of |window_region_rect_|. So after
250     // the intersection, |window_region_rect_| == |region_rect|. If so, what's
251     // the point of the intersecting operations? Why cannot we directly retrieve
252     // |window_region_rect_| from GetWindowRegionTypeWithBoundary() function?
253     // TODO(zijiehe): Figure out the purpose of these intersections.
254     window_region_rect_.IntersectWith(region_rect);
255     content_rect.IntersectWith(region_rect);
256   }
257 
258   // Check if the client area is out of the screen area. When the window is
259   // maximized, only its client area is visible in the screen, the border will
260   // be hidden. So we are using |content_rect| here.
261   if (!GetFullscreenRect().ContainsRect(content_rect)) {
262     return false;
263   }
264 
265   // Check if the window is occluded by any other window, excluding the child
266   // windows, context menus, and |excluded_window_|.
267   // |content_rect| is preferred, see the comments on
268   // IsWindowIntersectWithSelectedWindow().
269   TopWindowVerifierContext context(selected,
270                                    reinterpret_cast<HWND>(excluded_window()),
271                                    content_rect, &window_capture_helper_);
272   return context.IsTopWindow();
273 }
274 
GetWindowRectInVirtualScreen()275 DesktopRect CroppingWindowCapturerWin::GetWindowRectInVirtualScreen() {
276   TRACE_EVENT0("webrtc",
277                "CroppingWindowCapturerWin::GetWindowRectInVirtualScreen");
278   DesktopRect window_rect;
279   HWND hwnd = reinterpret_cast<HWND>(GetWindowToCapture());
280   if (!GetCroppedWindowRect(hwnd, /*avoid_cropping_border*/ false, &window_rect,
281                             /*original_rect*/ nullptr)) {
282     RTC_LOG(LS_WARNING) << "Failed to get window info: " << GetLastError();
283     return window_rect;
284   }
285   window_rect.IntersectWith(window_region_rect_);
286 
287   // Convert |window_rect| to be relative to the top-left of the virtual screen.
288   DesktopRect screen_rect(GetFullscreenRect());
289   window_rect.IntersectWith(screen_rect);
290   window_rect.Translate(-screen_rect.left(), -screen_rect.top());
291   return window_rect;
292 }
293 
GetWindowToCapture() const294 WindowId CroppingWindowCapturerWin::GetWindowToCapture() const {
295   const auto selected_source = selected_window();
296   const auto full_screen_source =
297       full_screen_window_detector_
298           ? full_screen_window_detector_->FindFullScreenWindow(selected_source)
299           : 0;
300   return full_screen_source ? full_screen_source : selected_source;
301 }
302 
303 }  // namespace
304 
305 // static
CreateCapturer(const DesktopCaptureOptions & options)306 std::unique_ptr<DesktopCapturer> CroppingWindowCapturer::CreateCapturer(
307     const DesktopCaptureOptions& options) {
308   std::unique_ptr<DesktopCapturer> capturer(
309       new CroppingWindowCapturerWin(options));
310   if (capturer && options.detect_updated_region()) {
311     capturer.reset(new DesktopCapturerDifferWrapper(std::move(capturer)));
312   }
313 
314   return capturer;
315 }
316 
317 }  // namespace webrtc
318