* Introduce a FullScreenWindowDetector to manage routines for updating the list of sources being application agnostic, inspired by FullScreenChromeWindowDetector. * Introduce a FullScreenApplicationHandler to make a decision about changing window to share in application specific way, inspired by FullScreenChromeWindowDetector. * Remove FullScreenChromeWindowDetector as redundant. * Add FullScreenApplicationHandler for MS PowerPoint and Apple Keynote on MacOS. * Add FullScreenApplicationHandler for MS PowerPoint on Windows. Bug: webrtc:3852 Change-Id: I06507d929308e85b882b2f8210a025afef7f26a9 Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/156020 Commit-Queue: Mirko Bonadei <mbonadei@webrtc.org> Reviewed-by: Jamie Walch <jamiewalch@chromium.org> Reviewed-by: Justin Uberti <juberti@webrtc.org> Reviewed-by: Wez <wez@google.com> Cr-Commit-Position: refs/heads/master@{#29993}
415 lines
13 KiB
C++
415 lines
13 KiB
C++
/*
|
|
* Copyright (c) 2014 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 "modules/desktop_capture/mac/window_list_utils.h"
|
|
|
|
#include <ApplicationServices/ApplicationServices.h>
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <iterator>
|
|
#include <limits>
|
|
#include <list>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <utility>
|
|
|
|
#include "rtc_base/checks.h"
|
|
|
|
static_assert(static_cast<webrtc::WindowId>(kCGNullWindowID) ==
|
|
webrtc::kNullWindowId,
|
|
"kNullWindowId needs to equal to kCGNullWindowID.");
|
|
|
|
namespace webrtc {
|
|
|
|
namespace {
|
|
|
|
bool ToUtf8(const CFStringRef str16, std::string* str8) {
|
|
size_t maxlen = CFStringGetMaximumSizeForEncoding(CFStringGetLength(str16),
|
|
kCFStringEncodingUTF8) +
|
|
1;
|
|
std::unique_ptr<char[]> buffer(new char[maxlen]);
|
|
if (!buffer ||
|
|
!CFStringGetCString(str16, buffer.get(), maxlen, kCFStringEncodingUTF8)) {
|
|
return false;
|
|
}
|
|
str8->assign(buffer.get());
|
|
return true;
|
|
}
|
|
|
|
// Get CFDictionaryRef from |id| and call |on_window| against it. This function
|
|
// returns false if native APIs fail, typically it indicates that the |id| does
|
|
// not represent a window. |on_window| will not be called if false is returned
|
|
// from this function.
|
|
bool GetWindowRef(CGWindowID id,
|
|
rtc::FunctionView<void(CFDictionaryRef)> on_window) {
|
|
RTC_DCHECK(on_window);
|
|
|
|
// TODO(zijiehe): |id| is a 32-bit integer, casting it to an array seems not
|
|
// safe enough. Maybe we should create a new
|
|
// const void* arr[] = {
|
|
// reinterpret_cast<void*>(id) }
|
|
// };
|
|
CFArrayRef window_id_array =
|
|
CFArrayCreate(NULL, reinterpret_cast<const void**>(&id), 1, NULL);
|
|
CFArrayRef window_array =
|
|
CGWindowListCreateDescriptionFromArray(window_id_array);
|
|
|
|
bool result = false;
|
|
// TODO(zijiehe): CFArrayGetCount(window_array) should always return 1.
|
|
// Otherwise, we should treat it as failure.
|
|
if (window_array && CFArrayGetCount(window_array)) {
|
|
on_window(reinterpret_cast<CFDictionaryRef>(
|
|
CFArrayGetValueAtIndex(window_array, 0)));
|
|
result = true;
|
|
}
|
|
|
|
if (window_array) {
|
|
CFRelease(window_array);
|
|
}
|
|
CFRelease(window_id_array);
|
|
return result;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool GetWindowList(rtc::FunctionView<bool(CFDictionaryRef)> on_window,
|
|
bool ignore_minimized,
|
|
bool only_zero_layer) {
|
|
RTC_DCHECK(on_window);
|
|
|
|
// Only get on screen, non-desktop windows.
|
|
// According to
|
|
// https://developer.apple.com/documentation/coregraphics/cgwindowlistoption/1454105-optiononscreenonly
|
|
// , when kCGWindowListOptionOnScreenOnly is used, the order of windows are in
|
|
// decreasing z-order.
|
|
CFArrayRef window_array = CGWindowListCopyWindowInfo(
|
|
kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements,
|
|
kCGNullWindowID);
|
|
if (!window_array)
|
|
return false;
|
|
|
|
MacDesktopConfiguration desktop_config = MacDesktopConfiguration::GetCurrent(
|
|
MacDesktopConfiguration::TopLeftOrigin);
|
|
|
|
// Check windows to make sure they have an id, title, and use window layer
|
|
// other than 0.
|
|
CFIndex count = CFArrayGetCount(window_array);
|
|
for (CFIndex i = 0; i < count; i++) {
|
|
CFDictionaryRef window = reinterpret_cast<CFDictionaryRef>(
|
|
CFArrayGetValueAtIndex(window_array, i));
|
|
if (!window) {
|
|
continue;
|
|
}
|
|
|
|
CFNumberRef window_id = reinterpret_cast<CFNumberRef>(
|
|
CFDictionaryGetValue(window, kCGWindowNumber));
|
|
if (!window_id) {
|
|
continue;
|
|
}
|
|
|
|
CFNumberRef window_layer = reinterpret_cast<CFNumberRef>(
|
|
CFDictionaryGetValue(window, kCGWindowLayer));
|
|
if (!window_layer) {
|
|
continue;
|
|
}
|
|
|
|
// Skip windows with layer!=0 (menu, dock).
|
|
int layer;
|
|
if (!CFNumberGetValue(window_layer, kCFNumberIntType, &layer)) {
|
|
continue;
|
|
}
|
|
if (only_zero_layer && layer != 0) {
|
|
continue;
|
|
}
|
|
|
|
// Skip windows that are minimized and not full screen.
|
|
if (ignore_minimized && !IsWindowOnScreen(window) &&
|
|
!IsWindowFullScreen(desktop_config, window)) {
|
|
continue;
|
|
}
|
|
|
|
// If window title is empty, only consider it if it is either on screen or
|
|
// fullscreen.
|
|
CFStringRef window_title = reinterpret_cast<CFStringRef>(
|
|
CFDictionaryGetValue(window, kCGWindowName));
|
|
if (!window_title && !IsWindowOnScreen(window) &&
|
|
!IsWindowFullScreen(desktop_config, window)) {
|
|
continue;
|
|
}
|
|
|
|
if (!on_window(window)) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
CFRelease(window_array);
|
|
return true;
|
|
}
|
|
|
|
bool GetWindowList(DesktopCapturer::SourceList* windows,
|
|
bool ignore_minimized,
|
|
bool only_zero_layer) {
|
|
// Use a std::list so that iterators are preversed upon insertion and
|
|
// deletion.
|
|
std::list<DesktopCapturer::Source> sources;
|
|
std::map<int, std::list<DesktopCapturer::Source>::const_iterator> pid_itr_map;
|
|
const bool ret = GetWindowList(
|
|
[&sources, &pid_itr_map](CFDictionaryRef window) {
|
|
WindowId window_id = GetWindowId(window);
|
|
if (window_id != kNullWindowId) {
|
|
const std::string title = GetWindowTitle(window);
|
|
const int pid = GetWindowOwnerPid(window);
|
|
// Check if window for the same pid have been already inserted.
|
|
std::map<int,
|
|
std::list<DesktopCapturer::Source>::const_iterator>::iterator
|
|
itr = pid_itr_map.find(pid);
|
|
|
|
// Only consider empty titles if the app has no other window with a
|
|
// proper title.
|
|
if (title.empty()) {
|
|
std::string owner_name = GetWindowOwnerName(window);
|
|
|
|
// At this time we do not know if there will be other windows
|
|
// for the same pid unless they have been already inserted, hence
|
|
// the check in the map. Also skip the window if owner name is
|
|
// empty too.
|
|
if (!owner_name.empty() && (itr == pid_itr_map.end())) {
|
|
sources.push_back(DesktopCapturer::Source{window_id, owner_name});
|
|
RTC_DCHECK(!sources.empty());
|
|
// Get an iterator on the last valid element in the source list.
|
|
std::list<DesktopCapturer::Source>::const_iterator last_source =
|
|
--sources.end();
|
|
pid_itr_map.insert(
|
|
std::pair<int,
|
|
std::list<DesktopCapturer::Source>::const_iterator>(
|
|
pid, last_source));
|
|
}
|
|
} else {
|
|
sources.push_back(DesktopCapturer::Source{window_id, title});
|
|
// Once the window with empty title has been removed no other empty
|
|
// windows are allowed for the same pid.
|
|
if (itr != pid_itr_map.end() && (itr->second != sources.end())) {
|
|
sources.erase(itr->second);
|
|
// sdt::list::end() never changes during the lifetime of that
|
|
// list.
|
|
itr->second = sources.end();
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
},
|
|
ignore_minimized, only_zero_layer);
|
|
|
|
if (!ret)
|
|
return false;
|
|
|
|
RTC_DCHECK(windows);
|
|
windows->reserve(windows->size() + sources.size());
|
|
std::copy(std::begin(sources), std::end(sources),
|
|
std::back_inserter(*windows));
|
|
|
|
return true;
|
|
}
|
|
|
|
// Returns true if the window is occupying a full screen.
|
|
bool IsWindowFullScreen(const MacDesktopConfiguration& desktop_config,
|
|
CFDictionaryRef window) {
|
|
bool fullscreen = false;
|
|
CFDictionaryRef bounds_ref = reinterpret_cast<CFDictionaryRef>(
|
|
CFDictionaryGetValue(window, kCGWindowBounds));
|
|
|
|
CGRect bounds;
|
|
if (bounds_ref &&
|
|
CGRectMakeWithDictionaryRepresentation(bounds_ref, &bounds)) {
|
|
for (MacDisplayConfigurations::const_iterator it =
|
|
desktop_config.displays.begin();
|
|
it != desktop_config.displays.end(); it++) {
|
|
if (it->bounds.equals(
|
|
DesktopRect::MakeXYWH(bounds.origin.x, bounds.origin.y,
|
|
bounds.size.width, bounds.size.height))) {
|
|
fullscreen = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return fullscreen;
|
|
}
|
|
|
|
bool IsWindowFullScreen(const MacDesktopConfiguration& desktop_config,
|
|
CGWindowID id) {
|
|
bool fullscreen = false;
|
|
GetWindowRef(id, [&](CFDictionaryRef window) {
|
|
fullscreen = IsWindowFullScreen(desktop_config, window);
|
|
});
|
|
return fullscreen;
|
|
}
|
|
|
|
bool IsWindowOnScreen(CFDictionaryRef window) {
|
|
CFBooleanRef on_screen = reinterpret_cast<CFBooleanRef>(
|
|
CFDictionaryGetValue(window, kCGWindowIsOnscreen));
|
|
return on_screen != NULL && CFBooleanGetValue(on_screen);
|
|
}
|
|
|
|
bool IsWindowOnScreen(CGWindowID id) {
|
|
bool on_screen;
|
|
if (GetWindowRef(id, [&on_screen](CFDictionaryRef window) {
|
|
on_screen = IsWindowOnScreen(window);
|
|
})) {
|
|
return on_screen;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
std::string GetWindowTitle(CFDictionaryRef window) {
|
|
CFStringRef title = reinterpret_cast<CFStringRef>(
|
|
CFDictionaryGetValue(window, kCGWindowName));
|
|
std::string result;
|
|
if (title && ToUtf8(title, &result)) {
|
|
return result;
|
|
}
|
|
|
|
return std::string();
|
|
}
|
|
|
|
std::string GetWindowTitle(CGWindowID id) {
|
|
std::string title;
|
|
if (GetWindowRef(id, [&title](CFDictionaryRef window) {
|
|
title = GetWindowTitle(window);
|
|
})) {
|
|
return title;
|
|
}
|
|
return std::string();
|
|
}
|
|
|
|
std::string GetWindowOwnerName(CFDictionaryRef window) {
|
|
CFStringRef owner_name = reinterpret_cast<CFStringRef>(
|
|
CFDictionaryGetValue(window, kCGWindowOwnerName));
|
|
std::string result;
|
|
if (owner_name && ToUtf8(owner_name, &result)) {
|
|
return result;
|
|
}
|
|
return std::string();
|
|
}
|
|
|
|
std::string GetWindowOwnerName(CGWindowID id) {
|
|
std::string owner_name;
|
|
if (GetWindowRef(id, [&owner_name](CFDictionaryRef window) {
|
|
owner_name = GetWindowOwnerPid(window);
|
|
})) {
|
|
return owner_name;
|
|
}
|
|
return std::string();
|
|
}
|
|
|
|
WindowId GetWindowId(CFDictionaryRef window) {
|
|
CFNumberRef window_id = reinterpret_cast<CFNumberRef>(
|
|
CFDictionaryGetValue(window, kCGWindowNumber));
|
|
if (!window_id) {
|
|
return kNullWindowId;
|
|
}
|
|
|
|
// Note: WindowId is 64-bit on 64-bit system, but CGWindowID is always 32-bit.
|
|
// CFNumberGetValue() fills only top 32 bits, so we should use CGWindowID to
|
|
// receive the window id.
|
|
CGWindowID id;
|
|
if (!CFNumberGetValue(window_id, kCFNumberIntType, &id)) {
|
|
return kNullWindowId;
|
|
}
|
|
|
|
return id;
|
|
}
|
|
|
|
int GetWindowOwnerPid(CFDictionaryRef window) {
|
|
CFNumberRef window_pid = reinterpret_cast<CFNumberRef>(
|
|
CFDictionaryGetValue(window, kCGWindowOwnerPID));
|
|
if (!window_pid) {
|
|
return 0;
|
|
}
|
|
|
|
int pid;
|
|
if (!CFNumberGetValue(window_pid, kCFNumberIntType, &pid)) {
|
|
return 0;
|
|
}
|
|
|
|
return pid;
|
|
}
|
|
|
|
int GetWindowOwnerPid(CGWindowID id) {
|
|
int pid;
|
|
if (GetWindowRef(id, [&pid](CFDictionaryRef window) {
|
|
pid = GetWindowOwnerPid(window);
|
|
})) {
|
|
return pid;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
float GetScaleFactorAtPosition(const MacDesktopConfiguration& desktop_config,
|
|
DesktopVector position) {
|
|
// Find the dpi to physical pixel scale for the screen where the mouse cursor
|
|
// is.
|
|
for (auto it = desktop_config.displays.begin();
|
|
it != desktop_config.displays.end(); ++it) {
|
|
if (it->bounds.Contains(position)) {
|
|
return it->dip_to_pixel_scale;
|
|
}
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
float GetWindowScaleFactor(CGWindowID id, DesktopSize size) {
|
|
DesktopRect window_bounds = GetWindowBounds(id);
|
|
float scale = 1.0f;
|
|
|
|
if (!window_bounds.is_empty() && !size.is_empty()) {
|
|
float scale_x = size.width() / window_bounds.width();
|
|
float scale_y = size.height() / window_bounds.height();
|
|
// Currently the scale in X and Y directions must be same.
|
|
if ((std::fabs(scale_x - scale_y) <=
|
|
std::numeric_limits<float>::epsilon() * std::max(scale_x, scale_y)) &&
|
|
scale_x > 0.0f) {
|
|
scale = scale_x;
|
|
}
|
|
}
|
|
|
|
return scale;
|
|
}
|
|
|
|
DesktopRect GetWindowBounds(CFDictionaryRef window) {
|
|
CFDictionaryRef window_bounds = reinterpret_cast<CFDictionaryRef>(
|
|
CFDictionaryGetValue(window, kCGWindowBounds));
|
|
if (!window_bounds) {
|
|
return DesktopRect();
|
|
}
|
|
|
|
CGRect gc_window_rect;
|
|
if (!CGRectMakeWithDictionaryRepresentation(window_bounds, &gc_window_rect)) {
|
|
return DesktopRect();
|
|
}
|
|
|
|
return DesktopRect::MakeXYWH(gc_window_rect.origin.x, gc_window_rect.origin.y,
|
|
gc_window_rect.size.width,
|
|
gc_window_rect.size.height);
|
|
}
|
|
|
|
DesktopRect GetWindowBounds(CGWindowID id) {
|
|
DesktopRect result;
|
|
if (GetWindowRef(id, [&result](CFDictionaryRef window) {
|
|
result = GetWindowBounds(window);
|
|
})) {
|
|
return result;
|
|
}
|
|
return DesktopRect();
|
|
}
|
|
|
|
} // namespace webrtc
|