electron/shell/browser/api/electron_api_desktop_capturer.cc
Athul Iddya 905e41bbdd
fix: use StartUpdating method for PipeWire capturer (#38833)
* fix: use StartUpdating method for PipeWire capturer

Fixed a crash related to PipeWire capturer by adapting to Chromium's
interface changes. Chromium expects a call to
`NativeDesktopMediaList::StartUpdating` with an implementation of
`DesktopMediaListObserver` for delegated capturers like PipeWire. This
interface allows listening to user permission events and listing
sources only after the user has made a choice on the permission dialog.

The interface has been implemented by an inner class to allow listening
to screen and window capture permissions concurrently using two
instances of the class. A patch that was resetting the capturer on the
first refresh has been changed to exclude PipeWire. PipeWire capturer
object will follow the lifecycle of `NativeDesktopMediaList`, as is the
case in Chromium.

Fixes #37463

* fix: wait for thumbnails from PipeWire when necessary

The PipeWire stream starts after the dialog is dismissed. If the sources
are listed immediately afterwards, the thumbnail may not have been
generated by that time. Explicitly wait for both thumbnail generation
and a selection on the source dialog before listing sources.
2023-07-11 10:21:11 +02:00

422 lines
16 KiB
C++

// Copyright (c) 2015 GitHub, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/browser/api/electron_api_desktop_capturer.h"
#include <map>
#include <memory>
#include <utility>
#include <vector>
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_restrictions.h"
#include "chrome/browser/media/webrtc/desktop_media_list.h"
#include "chrome/browser/media/webrtc/window_icon_util.h"
#include "content/public/browser/desktop_capture.h"
#include "gin/object_template_builder.h"
#include "shell/browser/javascript_environment.h"
#include "shell/common/api/electron_api_native_image.h"
#include "shell/common/gin_converters/gfx_converter.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/event_emitter_caller.h"
#include "shell/common/node_includes.h"
#include "third_party/webrtc/modules/desktop_capture/desktop_capture_options.h"
#include "third_party/webrtc/modules/desktop_capture/desktop_capturer.h"
#if defined(USE_OZONE)
#include "ui/ozone/buildflags.h"
#if BUILDFLAG(OZONE_PLATFORM_X11)
#define USE_OZONE_PLATFORM_X11
#endif
#endif
#if BUILDFLAG(IS_WIN)
#include "third_party/webrtc/modules/desktop_capture/win/dxgi_duplicator_controller.h"
#include "third_party/webrtc/modules/desktop_capture/win/screen_capturer_win_directx.h"
#include "ui/display/win/display_info.h"
#elif BUILDFLAG(IS_LINUX)
#if defined(USE_OZONE_PLATFORM_X11)
#include "base/logging.h"
#include "ui/base/x/x11_display_util.h"
#include "ui/base/x/x11_util.h"
#include "ui/display/util/edid_parser.h" // nogncheck
#include "ui/gfx/x/randr.h"
#include "ui/gfx/x/x11_atom_cache.h"
#include "ui/gfx/x/xproto_util.h"
#endif // defined(USE_OZONE_PLATFORM_X11)
#endif // BUILDFLAG(IS_WIN)
#if BUILDFLAG(IS_LINUX)
// Private function in ui/base/x/x11_display_util.cc
std::map<x11::RandR::Output, int> GetMonitors(int version,
x11::RandR* randr,
x11::Window window) {
std::map<x11::RandR::Output, int> output_to_monitor;
if (version >= 105) {
if (auto reply = randr->GetMonitors({window}).Sync()) {
for (size_t monitor = 0; monitor < reply->monitors.size(); monitor++) {
for (x11::RandR::Output output : reply->monitors[monitor].outputs)
output_to_monitor[output] = monitor;
}
}
}
return output_to_monitor;
}
// Get the EDID data from the |output| and stores to |edid|.
// Private function in ui/base/x/x11_display_util.cc
std::vector<uint8_t> GetEDIDProperty(x11::RandR* randr,
x11::RandR::Output output) {
constexpr const char kRandrEdidProperty[] = "EDID";
auto future = randr->GetOutputProperty(x11::RandR::GetOutputPropertyRequest{
.output = output,
.property = x11::GetAtom(kRandrEdidProperty),
.long_length = 128});
auto response = future.Sync();
std::vector<uint8_t> edid;
if (response && response->format == 8 && response->type != x11::Atom::None)
edid = std::move(response->data);
return edid;
}
// Find the mapping from monitor name atom to the display identifier
// that the screen API uses. Based on the logic in BuildDisplaysFromXRandRInfo
// in ui/base/x/x11_display_util.cc
std::map<int32_t, uint32_t> MonitorAtomIdToDisplayId() {
auto* connection = x11::Connection::Get();
auto& randr = connection->randr();
auto x_root_window = ui::GetX11RootWindow();
int version = ui::GetXrandrVersion();
std::map<int32_t, uint32_t> monitor_atom_to_display;
auto resources = randr.GetScreenResourcesCurrent({x_root_window}).Sync();
if (!resources) {
LOG(ERROR) << "XRandR returned no displays; don't know how to map ids";
return monitor_atom_to_display;
}
std::map<x11::RandR::Output, int> output_to_monitor =
GetMonitors(version, &randr, x_root_window);
auto monitors_reply = randr.GetMonitors({x_root_window}).Sync();
for (size_t i = 0; i < resources->outputs.size(); i++) {
x11::RandR::Output output_id = resources->outputs[i];
auto output_info =
randr.GetOutputInfo({output_id, resources->config_timestamp}).Sync();
if (!output_info)
continue;
if (output_info->connection != x11::RandR::RandRConnection::Connected)
continue;
if (output_info->crtc == static_cast<x11::RandR::Crtc>(0))
continue;
auto crtc =
randr.GetCrtcInfo({output_info->crtc, resources->config_timestamp})
.Sync();
if (!crtc)
continue;
display::EdidParser edid_parser(
GetEDIDProperty(&randr, static_cast<x11::RandR::Output>(output_id)));
auto output_32 = static_cast<uint32_t>(output_id);
int64_t display_id =
output_32 > 0xff ? 0 : edid_parser.GetIndexBasedDisplayId(output_32);
// It isn't ideal, but if we can't parse the EDID data, fall back on the
// display number.
if (!display_id)
display_id = i;
// Find the mapping between output identifier and the monitor name atom
// Note this isn't the atom string, but the numeric atom identifier,
// since this is what the WebRTC system uses as the display identifier
auto output_monitor_iter = output_to_monitor.find(output_id);
if (output_monitor_iter != output_to_monitor.end()) {
x11::Atom atom =
monitors_reply->monitors[output_monitor_iter->second].name;
monitor_atom_to_display[static_cast<int32_t>(atom)] = display_id;
}
}
return monitor_atom_to_display;
}
#endif
namespace gin {
template <>
struct Converter<electron::api::DesktopCapturer::Source> {
static v8::Local<v8::Value> ToV8(
v8::Isolate* isolate,
const electron::api::DesktopCapturer::Source& source) {
gin_helper::Dictionary dict = gin::Dictionary::CreateEmpty(isolate);
content::DesktopMediaID id = source.media_list_source.id;
dict.Set("name", base::UTF16ToUTF8(source.media_list_source.name));
dict.Set("id", id.ToString());
dict.Set("thumbnail",
electron::api::NativeImage::Create(
isolate, gfx::Image(source.media_list_source.thumbnail)));
dict.Set("display_id", source.display_id);
if (source.fetch_icon) {
dict.Set(
"appIcon",
electron::api::NativeImage::Create(
isolate, gfx::Image(GetWindowIcon(source.media_list_source.id))));
} else {
dict.Set("appIcon", nullptr);
}
return ConvertToV8(isolate, dict);
}
};
} // namespace gin
namespace electron::api {
gin::WrapperInfo DesktopCapturer::kWrapperInfo = {gin::kEmbedderNativeGin};
DesktopCapturer::DesktopCapturer(v8::Isolate* isolate) {}
DesktopCapturer::~DesktopCapturer() = default;
DesktopCapturer::DesktopListListener::DesktopListListener(
OnceCallback update_callback,
OnceCallback failure_callback,
bool skip_thumbnails)
: update_callback_(std::move(update_callback)),
failure_callback_(std::move(failure_callback)),
have_thumbnail_(skip_thumbnails) {}
DesktopCapturer::DesktopListListener::~DesktopListListener() = default;
void DesktopCapturer::DesktopListListener::OnDelegatedSourceListSelection() {
if (have_thumbnail_) {
std::move(update_callback_).Run();
} else {
have_selection_ = true;
}
}
void DesktopCapturer::DesktopListListener::OnSourceThumbnailChanged(int index) {
if (have_selection_) {
// This is called every time a thumbnail is refreshed. Reset variable to
// ensure that the callback is not run again.
have_selection_ = false;
// PipeWire returns a single source, so index is not relevant.
std::move(update_callback_).Run();
} else {
have_thumbnail_ = true;
}
}
void DesktopCapturer::DesktopListListener::OnDelegatedSourceListDismissed() {
std::move(failure_callback_).Run();
}
void DesktopCapturer::StartHandling(bool capture_window,
bool capture_screen,
const gfx::Size& thumbnail_size,
bool fetch_window_icons) {
fetch_window_icons_ = fetch_window_icons;
#if BUILDFLAG(IS_WIN)
if (content::desktop_capture::CreateDesktopCaptureOptions()
.allow_directx_capturer()) {
// DxgiDuplicatorController should be alive in this scope according to
// screen_capturer_win.cc.
auto duplicator = webrtc::DxgiDuplicatorController::Instance();
using_directx_capturer_ = webrtc::ScreenCapturerWinDirectx::IsSupported();
}
#endif // BUILDFLAG(IS_WIN)
// clear any existing captured sources.
captured_sources_.clear();
// Start listening for captured sources.
capture_window_ = capture_window;
capture_screen_ = capture_screen;
{
// Initialize the source list.
// Apply the new thumbnail size and restart capture.
if (capture_window) {
if (auto capturer = content::desktop_capture::CreateWindowCapturer();
capturer) {
window_capturer_ = std::make_unique<NativeDesktopMediaList>(
DesktopMediaList::Type::kWindow, std::move(capturer));
window_capturer_->SetThumbnailSize(thumbnail_size);
OnceCallback update_callback = base::BindOnce(
&DesktopCapturer::UpdateSourcesList, weak_ptr_factory_.GetWeakPtr(),
window_capturer_.get());
if (window_capturer_->IsSourceListDelegated()) {
OnceCallback failure_callback = base::BindOnce(
&DesktopCapturer::HandleFailure, weak_ptr_factory_.GetWeakPtr());
window_listener_ = std::make_unique<DesktopListListener>(
std::move(update_callback), std::move(failure_callback),
thumbnail_size.IsEmpty());
window_capturer_->StartUpdating(window_listener_.get());
} else {
window_capturer_->Update(std::move(update_callback),
/* refresh_thumbnails = */ true);
}
}
}
if (capture_screen) {
if (auto capturer = content::desktop_capture::CreateScreenCapturer();
capturer) {
screen_capturer_ = std::make_unique<NativeDesktopMediaList>(
DesktopMediaList::Type::kScreen, std::move(capturer));
screen_capturer_->SetThumbnailSize(thumbnail_size);
OnceCallback update_callback = base::BindOnce(
&DesktopCapturer::UpdateSourcesList, weak_ptr_factory_.GetWeakPtr(),
screen_capturer_.get());
if (screen_capturer_->IsSourceListDelegated()) {
OnceCallback failure_callback = base::BindOnce(
&DesktopCapturer::HandleFailure, weak_ptr_factory_.GetWeakPtr());
screen_listener_ = std::make_unique<DesktopListListener>(
std::move(update_callback), std::move(failure_callback),
thumbnail_size.IsEmpty());
screen_capturer_->StartUpdating(screen_listener_.get());
} else {
screen_capturer_->Update(std::move(update_callback),
/* refresh_thumbnails = */ true);
}
}
}
}
}
void DesktopCapturer::UpdateSourcesList(DesktopMediaList* list) {
if (capture_window_ &&
list->GetMediaListType() == DesktopMediaList::Type::kWindow) {
capture_window_ = false;
std::vector<DesktopCapturer::Source> window_sources;
window_sources.reserve(list->GetSourceCount());
for (int i = 0; i < list->GetSourceCount(); i++) {
window_sources.emplace_back(list->GetSource(i), std::string(),
fetch_window_icons_);
}
std::move(window_sources.begin(), window_sources.end(),
std::back_inserter(captured_sources_));
}
if (capture_screen_ &&
list->GetMediaListType() == DesktopMediaList::Type::kScreen) {
capture_screen_ = false;
std::vector<DesktopCapturer::Source> screen_sources;
screen_sources.reserve(list->GetSourceCount());
for (int i = 0; i < list->GetSourceCount(); i++) {
screen_sources.emplace_back(list->GetSource(i), std::string());
}
#if BUILDFLAG(IS_WIN)
// Gather the same unique screen IDs used by the electron.screen API in
// order to provide an association between it and
// desktopCapturer/getUserMedia. This is only required when using the
// DirectX capturer, otherwise the IDs across the APIs already match.
if (using_directx_capturer_) {
std::vector<std::string> device_names;
// Crucially, this list of device names will be in the same order as
// |media_list_sources|.
if (!webrtc::DxgiDuplicatorController::Instance()->GetDeviceNames(
&device_names)) {
HandleFailure();
return;
}
int device_name_index = 0;
for (auto& source : screen_sources) {
const auto& device_name = device_names[device_name_index++];
std::wstring wide_device_name;
base::UTF8ToWide(device_name.c_str(), device_name.size(),
&wide_device_name);
const int64_t device_id =
display::win::internal::DisplayInfo::DeviceIdFromDeviceName(
wide_device_name.c_str());
source.display_id = base::NumberToString(device_id);
}
}
#elif BUILDFLAG(IS_MAC)
// On Mac, the IDs across the APIs match.
for (auto& source : screen_sources) {
source.display_id = base::NumberToString(source.media_list_source.id.id);
}
#elif BUILDFLAG(IS_LINUX)
#if defined(USE_OZONE_PLATFORM_X11)
// On Linux, with X11, the source id is the numeric value of the
// display name atom and the display id is either the EDID or the
// loop index when that display was found (see
// BuildDisplaysFromXRandRInfo in ui/base/x/x11_display_util.cc)
std::map<int32_t, uint32_t> monitor_atom_to_display_id =
MonitorAtomIdToDisplayId();
for (auto& source : screen_sources) {
auto display_id_iter =
monitor_atom_to_display_id.find(source.media_list_source.id.id);
if (display_id_iter != monitor_atom_to_display_id.end())
source.display_id = base::NumberToString(display_id_iter->second);
}
#endif // defined(USE_OZONE_PLATFORM_X11)
#endif // BUILDFLAG(IS_WIN)
std::move(screen_sources.begin(), screen_sources.end(),
std::back_inserter(captured_sources_));
}
if (!capture_window_ && !capture_screen_) {
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
v8::HandleScope scope(isolate);
gin_helper::CallMethod(this, "_onfinished", captured_sources_);
Unpin();
}
}
void DesktopCapturer::HandleFailure() {
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
v8::HandleScope scope(isolate);
gin_helper::CallMethod(this, "_onerror", "Failed to get sources.");
Unpin();
}
// static
gin::Handle<DesktopCapturer> DesktopCapturer::Create(v8::Isolate* isolate) {
auto handle = gin::CreateHandle(isolate, new DesktopCapturer(isolate));
// Keep reference alive until capturing has finished.
handle->Pin(isolate);
return handle;
}
gin::ObjectTemplateBuilder DesktopCapturer::GetObjectTemplateBuilder(
v8::Isolate* isolate) {
return gin::Wrappable<DesktopCapturer>::GetObjectTemplateBuilder(isolate)
.SetMethod("startHandling", &DesktopCapturer::StartHandling);
}
const char* DesktopCapturer::GetTypeName() {
return "DesktopCapturer";
}
} // namespace electron::api
namespace {
void Initialize(v8::Local<v8::Object> exports,
v8::Local<v8::Value> unused,
v8::Local<v8::Context> context,
void* priv) {
gin_helper::Dictionary dict(context->GetIsolate(), exports);
dict.SetMethod("createDesktopCapturer",
&electron::api::DesktopCapturer::Create);
}
} // namespace
NODE_LINKED_BINDING_CONTEXT_AWARE(electron_browser_desktop_capturer, Initialize)