camera-portal.c 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602
  1. /* camera-portal.c
  2. *
  3. * Copyright 2021 Georges Basile Stavracas Neto <[email protected]>
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation, either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License
  16. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. *
  18. * SPDX-License-Identifier: GPL-2.0-or-later
  19. */
  20. #include "pipewire.h"
  21. #include "portal.h"
  22. #include <util/dstr.h>
  23. #include <fcntl.h>
  24. #include <unistd.h>
  25. #include <gio/gio.h>
  26. #include <gio/gunixfdlist.h>
  27. #include <spa/debug/dict.h>
  28. #include <spa/node/keys.h>
  29. #include <spa/pod/iter.h>
  30. #include <spa/utils/defs.h>
  31. #include <spa/utils/keys.h>
  32. struct camera_portal_source {
  33. obs_source_t *source;
  34. obs_data_t *settings;
  35. obs_pipewire_stream *obs_pw_stream;
  36. char *device_id;
  37. };
  38. /* ------------------------------------------------- */
  39. struct pw_portal_connection {
  40. obs_pipewire *obs_pw;
  41. GHashTable *devices;
  42. GCancellable *cancellable;
  43. GPtrArray *sources;
  44. bool initializing;
  45. };
  46. struct pw_portal_connection *connection = NULL;
  47. static void pw_portal_connection_free(struct pw_portal_connection *connection)
  48. {
  49. if (!connection)
  50. return;
  51. g_cancellable_cancel(connection->cancellable);
  52. g_clear_pointer(&connection->devices, g_hash_table_destroy);
  53. g_clear_pointer(&connection->obs_pw, obs_pipewire_destroy);
  54. g_clear_pointer(&connection->sources, g_ptr_array_unref);
  55. g_clear_object(&connection->cancellable);
  56. bfree(connection);
  57. }
  58. static GDBusProxy *camera_proxy = NULL;
  59. static void ensure_camera_portal_proxy(void)
  60. {
  61. g_autoptr(GError) error = NULL;
  62. if (!camera_proxy) {
  63. camera_proxy = g_dbus_proxy_new_sync(
  64. portal_get_dbus_connection(), G_DBUS_PROXY_FLAGS_NONE,
  65. NULL, "org.freedesktop.portal.Desktop",
  66. "/org/freedesktop/portal/desktop",
  67. "org.freedesktop.portal.Camera", NULL, &error);
  68. if (error) {
  69. blog(LOG_WARNING,
  70. "[portals] Error retrieving D-Bus proxy: %s",
  71. error->message);
  72. return;
  73. }
  74. }
  75. }
  76. static GDBusProxy *get_camera_portal_proxy(void)
  77. {
  78. ensure_camera_portal_proxy();
  79. return camera_proxy;
  80. }
  81. static uint32_t get_camera_version(void)
  82. {
  83. g_autoptr(GVariant) cached_version = NULL;
  84. uint32_t version;
  85. ensure_camera_portal_proxy();
  86. if (!camera_proxy)
  87. return 0;
  88. cached_version =
  89. g_dbus_proxy_get_cached_property(camera_proxy, "version");
  90. version = cached_version ? g_variant_get_uint32(cached_version) : 0;
  91. return version;
  92. }
  93. /* ------------------------------------------------- */
  94. struct camera_device {
  95. uint32_t id;
  96. struct pw_properties *properties;
  97. struct pw_proxy *proxy;
  98. struct spa_hook proxy_listener;
  99. };
  100. static struct camera_device *
  101. camera_device_new(uint32_t id, const struct spa_dict *properties)
  102. {
  103. struct camera_device *device = bzalloc(sizeof(struct camera_device));
  104. device->id = id;
  105. device->properties = properties ? pw_properties_new_dict(properties)
  106. : NULL;
  107. return device;
  108. }
  109. static void camera_device_free(struct camera_device *device)
  110. {
  111. if (!device)
  112. return;
  113. g_clear_pointer(&device->proxy, pw_proxy_destroy);
  114. g_clear_pointer(&device->properties, pw_properties_free);
  115. bfree(device);
  116. }
  117. /* ------------------------------------------------- */
  118. static bool update_device_id(struct camera_portal_source *camera_source,
  119. const char *new_device_id)
  120. {
  121. if (strcmp(camera_source->device_id, new_device_id) == 0)
  122. return false;
  123. g_clear_pointer(&camera_source->device_id, bfree);
  124. camera_source->device_id = bstrdup(new_device_id);
  125. return true;
  126. }
  127. static void stream_camera(struct camera_portal_source *camera_source)
  128. {
  129. struct obs_pipwire_connect_stream_info connect_info;
  130. struct camera_device *device;
  131. g_clear_pointer(&camera_source->obs_pw_stream,
  132. obs_pipewire_stream_destroy);
  133. device = g_hash_table_lookup(connection->devices,
  134. camera_source->device_id);
  135. if (!device)
  136. return;
  137. blog(LOG_INFO, "[camera-portal] streaming camera '%s'",
  138. camera_source->device_id);
  139. connect_info = (struct obs_pipwire_connect_stream_info){
  140. .stream_name = "OBS PipeWire Camera",
  141. .stream_properties = pw_properties_new(
  142. PW_KEY_MEDIA_TYPE, "Video", PW_KEY_MEDIA_CATEGORY,
  143. "Capture", PW_KEY_MEDIA_ROLE, "Camera", NULL),
  144. };
  145. camera_source->obs_pw_stream = obs_pipewire_connect_stream(
  146. connection->obs_pw, camera_source->source, device->id,
  147. &connect_info);
  148. }
  149. static bool device_selected(void *data, obs_properties_t *props,
  150. obs_property_t *property, obs_data_t *settings)
  151. {
  152. UNUSED_PARAMETER(props);
  153. UNUSED_PARAMETER(property);
  154. struct camera_portal_source *camera_source = data;
  155. const char *device_id;
  156. device_id = obs_data_get_string(settings, "device_id");
  157. if (update_device_id(camera_source, device_id))
  158. stream_camera(camera_source);
  159. return true;
  160. }
  161. static void populate_cameras_list(struct camera_portal_source *camera_source,
  162. obs_property_t *device_list)
  163. {
  164. struct camera_device *device;
  165. GHashTableIter iter;
  166. const char *device_id;
  167. bool device_found;
  168. if (!connection)
  169. return;
  170. obs_property_list_clear(device_list);
  171. device_found = false;
  172. g_hash_table_iter_init(&iter, connection->devices);
  173. while (g_hash_table_iter_next(&iter, (gpointer *)&device_id,
  174. (gpointer *)&device)) {
  175. const char *device_name;
  176. device_name = pw_properties_get(device->properties,
  177. PW_KEY_NODE_DESCRIPTION);
  178. obs_property_list_add_string(device_list, device_name,
  179. device_id);
  180. device_found |= strcmp(device_id, camera_source->device_id) ==
  181. 0;
  182. }
  183. if (!device_found && camera_source->device_id) {
  184. size_t device_index;
  185. device_index = obs_property_list_add_string(
  186. device_list, camera_source->device_id,
  187. camera_source->device_id);
  188. obs_property_list_item_disable(device_list, device_index, true);
  189. }
  190. }
  191. /* ------------------------------------------------- */
  192. static void on_proxy_removed_cb(void *data)
  193. {
  194. struct camera_device *device = data;
  195. pw_proxy_destroy(device->proxy);
  196. }
  197. static void on_destroy_proxy_cb(void *data)
  198. {
  199. struct camera_device *device = data;
  200. spa_hook_remove(&device->proxy_listener);
  201. device->proxy = NULL;
  202. }
  203. static const struct pw_proxy_events proxy_events = {
  204. PW_VERSION_PROXY_EVENTS,
  205. .removed = on_proxy_removed_cb,
  206. .destroy = on_destroy_proxy_cb,
  207. };
  208. static void on_registry_global_cb(void *user_data, uint32_t id,
  209. uint32_t permissions, const char *type,
  210. uint32_t version,
  211. const struct spa_dict *props)
  212. {
  213. UNUSED_PARAMETER(user_data);
  214. UNUSED_PARAMETER(permissions);
  215. UNUSED_PARAMETER(version);
  216. struct camera_device *device;
  217. struct pw_registry *registry;
  218. const char *device_id;
  219. if (strcmp(type, PW_TYPE_INTERFACE_Node) != 0)
  220. return;
  221. registry = obs_pipewire_get_registry(connection->obs_pw);
  222. device_id = spa_dict_lookup(props, SPA_KEY_NODE_NAME);
  223. device = camera_device_new(id, props);
  224. device->proxy = pw_registry_bind(registry, id, type, version, 0);
  225. if (!device->proxy) {
  226. blog(LOG_WARNING, "[camera-portal] Failed to bind device %s",
  227. device_id);
  228. bfree(device);
  229. return;
  230. }
  231. pw_proxy_add_listener(device->proxy, &device->proxy_listener,
  232. &proxy_events, device);
  233. g_hash_table_insert(connection->devices, bstrdup(device_id), device);
  234. }
  235. static void on_registry_global_remove_cb(void *user_data, uint32_t id)
  236. {
  237. UNUSED_PARAMETER(user_data);
  238. struct camera_device *device;
  239. const char *device_id;
  240. GHashTableIter iter;
  241. g_hash_table_iter_init(&iter, connection->devices);
  242. while (g_hash_table_iter_next(&iter, (gpointer *)&device_id,
  243. (gpointer *)&device)) {
  244. if (device->id != id)
  245. continue;
  246. g_hash_table_iter_remove(&iter);
  247. }
  248. }
  249. static const struct pw_registry_events registry_events = {
  250. PW_VERSION_REGISTRY_EVENTS,
  251. .global = on_registry_global_cb,
  252. .global_remove = on_registry_global_remove_cb,
  253. };
  254. /* ------------------------------------------------- */
  255. static void on_pipewire_remote_opened_cb(GObject *source, GAsyncResult *res,
  256. void *user_data)
  257. {
  258. UNUSED_PARAMETER(user_data);
  259. g_autoptr(GUnixFDList) fd_list = NULL;
  260. g_autoptr(GVariant) result = NULL;
  261. g_autoptr(GError) error = NULL;
  262. int pipewire_fd;
  263. int fd_index;
  264. result = g_dbus_proxy_call_with_unix_fd_list_finish(
  265. G_DBUS_PROXY(source), &fd_list, res, &error);
  266. if (error) {
  267. if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
  268. blog(LOG_ERROR,
  269. "[camera-portal] Error retrieving PipeWire fd: %s",
  270. error->message);
  271. return;
  272. }
  273. g_variant_get(result, "(h)", &fd_index, &error);
  274. pipewire_fd = g_unix_fd_list_get(fd_list, fd_index, &error);
  275. if (error) {
  276. if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
  277. blog(LOG_ERROR,
  278. "[camera-portal] Error retrieving PipeWire fd: %s",
  279. error->message);
  280. return;
  281. }
  282. connection->obs_pw = obs_pipewire_connect_fd(
  283. pipewire_fd, &registry_events, connection);
  284. obs_pipewire_roundtrip(connection->obs_pw);
  285. for (size_t i = 0; i < connection->sources->len; i++) {
  286. struct camera_portal_source *camera_source =
  287. g_ptr_array_index(connection->sources, i);
  288. stream_camera(camera_source);
  289. }
  290. }
  291. static void open_pipewire_remote(void)
  292. {
  293. GVariantBuilder builder;
  294. g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
  295. g_dbus_proxy_call_with_unix_fd_list(get_camera_portal_proxy(),
  296. "OpenPipeWireRemote",
  297. g_variant_new("(a{sv})", &builder),
  298. G_DBUS_CALL_FLAGS_NONE, -1, NULL,
  299. connection->cancellable,
  300. on_pipewire_remote_opened_cb, NULL);
  301. }
  302. /* ------------------------------------------------- */
  303. static void on_access_camera_response_received_cb(GVariant *parameters,
  304. void *user_data)
  305. {
  306. UNUSED_PARAMETER(user_data);
  307. g_autoptr(GVariant) result = NULL;
  308. uint32_t response;
  309. g_variant_get(parameters, "(u@a{sv})", &response, &result);
  310. if (response != 0) {
  311. blog(LOG_WARNING,
  312. "[camera-portal] Failed to create session, denied or cancelled by user");
  313. return;
  314. }
  315. blog(LOG_INFO, "[camera-portal] Successfully accessed cameras");
  316. open_pipewire_remote();
  317. }
  318. static void on_access_camera_finished_cb(GObject *source, GAsyncResult *res,
  319. void *user_data)
  320. {
  321. UNUSED_PARAMETER(user_data);
  322. g_autoptr(GVariant) result = NULL;
  323. g_autoptr(GError) error = NULL;
  324. result = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error);
  325. if (error) {
  326. if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
  327. blog(LOG_ERROR,
  328. "[camera-portal] Error accessing camera: %s",
  329. error->message);
  330. return;
  331. }
  332. }
  333. static void access_camera(struct camera_portal_source *camera_source)
  334. {
  335. GVariantBuilder builder;
  336. char *request_token;
  337. char *request_path;
  338. if (connection && connection->obs_pw) {
  339. stream_camera(camera_source);
  340. return;
  341. }
  342. if (!connection) {
  343. connection = bzalloc(sizeof(struct pw_portal_connection));
  344. connection->devices = g_hash_table_new_full(
  345. g_str_hash, g_str_equal, bfree,
  346. (GDestroyNotify)camera_device_free);
  347. connection->cancellable = g_cancellable_new();
  348. connection->sources = g_ptr_array_new();
  349. connection->initializing = false;
  350. }
  351. g_ptr_array_add(connection->sources, camera_source);
  352. if (connection->initializing)
  353. return;
  354. portal_create_request_path(&request_path, &request_token);
  355. portal_signal_subscribe(request_path, NULL,
  356. on_access_camera_response_received_cb, NULL);
  357. g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
  358. g_variant_builder_add(&builder, "{sv}", "handle_token",
  359. g_variant_new_string(request_token));
  360. g_dbus_proxy_call(get_camera_portal_proxy(), "AccessCamera",
  361. g_variant_new("(a{sv})", &builder),
  362. G_DBUS_CALL_FLAGS_NONE, -1, connection->cancellable,
  363. on_access_camera_finished_cb, NULL);
  364. connection->initializing = true;
  365. bfree(request_token);
  366. bfree(request_path);
  367. }
  368. /* obs_source_info methods */
  369. static const char *pipewire_camera_get_name(void *data)
  370. {
  371. UNUSED_PARAMETER(data);
  372. return obs_module_text("PipeWireCamera");
  373. }
  374. static void *pipewire_camera_create(obs_data_t *settings, obs_source_t *source)
  375. {
  376. struct camera_portal_source *camera_source;
  377. camera_source = bzalloc(sizeof(struct camera_portal_source));
  378. camera_source->source = source;
  379. camera_source->device_id =
  380. bstrdup(obs_data_get_string(settings, "device_id"));
  381. access_camera(camera_source);
  382. return camera_source;
  383. }
  384. static void pipewire_camera_destroy(void *data)
  385. {
  386. struct camera_portal_source *camera_source = data;
  387. if (connection)
  388. g_ptr_array_remove(connection->sources, camera_source);
  389. g_clear_pointer(&camera_source->obs_pw_stream,
  390. obs_pipewire_stream_destroy);
  391. g_clear_pointer(&camera_source->device_id, bfree);
  392. bfree(camera_source);
  393. }
  394. static void pipewire_camera_get_defaults(obs_data_t *settings)
  395. {
  396. obs_data_set_default_string(settings, "device_id", NULL);
  397. }
  398. static obs_properties_t *pipewire_camera_get_properties(void *data)
  399. {
  400. struct camera_portal_source *camera_source = data;
  401. obs_properties_t *properties;
  402. obs_property_t *device_list;
  403. properties = obs_properties_create();
  404. device_list = obs_properties_add_list(
  405. properties, "device_id",
  406. obs_module_text("PipeWireCameraDevice"), OBS_COMBO_TYPE_LIST,
  407. OBS_COMBO_FORMAT_STRING);
  408. populate_cameras_list(camera_source, device_list);
  409. obs_property_set_modified_callback2(device_list, device_selected,
  410. camera_source);
  411. return properties;
  412. }
  413. static void pipewire_camera_update(void *data, obs_data_t *settings)
  414. {
  415. struct camera_portal_source *camera_source = data;
  416. const char *device_id;
  417. device_id = obs_data_get_string(settings, "device_id");
  418. if (update_device_id(camera_source, device_id))
  419. stream_camera(camera_source);
  420. }
  421. static void pipewire_camera_show(void *data)
  422. {
  423. struct camera_portal_source *camera_source = data;
  424. if (camera_source->obs_pw_stream)
  425. obs_pipewire_stream_show(camera_source->obs_pw_stream);
  426. }
  427. static void pipewire_camera_hide(void *data)
  428. {
  429. struct camera_portal_source *camera_source = data;
  430. if (camera_source->obs_pw_stream)
  431. obs_pipewire_stream_hide(camera_source->obs_pw_stream);
  432. }
  433. static uint32_t pipewire_camera_get_width(void *data)
  434. {
  435. struct camera_portal_source *camera_source = data;
  436. if (camera_source->obs_pw_stream)
  437. return obs_pipewire_stream_get_width(
  438. camera_source->obs_pw_stream);
  439. else
  440. return 0;
  441. }
  442. static uint32_t pipewire_camera_get_height(void *data)
  443. {
  444. struct camera_portal_source *camera_source = data;
  445. if (camera_source->obs_pw_stream)
  446. return obs_pipewire_stream_get_height(
  447. camera_source->obs_pw_stream);
  448. else
  449. return 0;
  450. }
  451. void camera_portal_load(void)
  452. {
  453. const struct obs_source_info pipewire_camera_info = {
  454. .id = "pipewire-camera-source",
  455. .type = OBS_SOURCE_TYPE_INPUT,
  456. .output_flags = OBS_SOURCE_ASYNC_VIDEO,
  457. .get_name = pipewire_camera_get_name,
  458. .create = pipewire_camera_create,
  459. .destroy = pipewire_camera_destroy,
  460. .get_defaults = pipewire_camera_get_defaults,
  461. .get_properties = pipewire_camera_get_properties,
  462. .update = pipewire_camera_update,
  463. .show = pipewire_camera_show,
  464. .hide = pipewire_camera_hide,
  465. .get_width = pipewire_camera_get_width,
  466. .get_height = pipewire_camera_get_height,
  467. .icon_type = OBS_ICON_TYPE_CAMERA,
  468. };
  469. obs_register_source(&pipewire_camera_info);
  470. }
  471. void camera_portal_unload(void)
  472. {
  473. g_clear_pointer(&connection, pw_portal_connection_free);
  474. }