PipeWire capturer: add initial test for SharedScreenCastStream
This test created another PipeWire stream we can connect to with
SharedScreenCastStream and recieve frames from there. This is an
initial version, where I test whether we can successfuly connect
and disconnect, receive frames and it also tests DesktopFrameQueue.
In the future I will add tests to test mouse cursor and try to
come up with some corner cases and possible scenarios.
Bug: webrtc:13429
Change-Id: Ib2a749207085c6324ffe3d5cc8f2f9c631fa6459
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/256267
Reviewed-by: Christoffer Jansson <jansson@webrtc.org>
Reviewed-by: Mark Foltz <mfoltz@chromium.org>
Reviewed-by: Harald Alvestrand <hta@webrtc.org>
Commit-Queue: Jan Grulich <grulja@gmail.com>
Reviewed-by: Jeremy Leconte <jleconte@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#38431}
diff --git a/BUILD.gn b/BUILD.gn
index 3caa4c5..f962484 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -106,6 +106,9 @@
"tools_webrtc/perf:webrtc_dashboard_upload",
]
}
+ if ((is_linux || is_chromeos) && rtc_use_pipewire) {
+ deps += [ "modules/desktop_capture:shared_screencast_stream_test" ]
+ }
}
if (target_os == "android") {
deps += [ "tools_webrtc:binary_version_check" ]
diff --git a/DEPS b/DEPS
index 6cd918e..0ae460a 100644
--- a/DEPS
+++ b/DEPS
@@ -490,6 +490,21 @@
],
'dep_type': 'cipd',
},
+ 'src/third_party/pipewire/linux-amd64': {
+ 'packages': [
+ {
+ 'package': 'chromium/third_party/pipewire/linux-amd64',
+ 'version': 'BaVKmAmwpjdS6O0pnjSaMNSKhO1nmk5mRnyPVAJ2-HEC',
+ },
+ {
+ 'package': 'chromium/third_party/pipewire-media-session/linux-amd64',
+ 'version': 'Y6wUeITvAA0QD1vt8_a7eQdzbp0gkI1B02qfZUMJdowC',
+ },
+ ],
+
+ 'condition': 'checkout_linux',
+ 'dep_type': 'cipd',
+ },
# Everything coming after this is automatically updated by the auto-roller.
# === ANDROID_DEPS Generated Code Start ===
diff --git a/infra/specs/client.webrtc.json b/infra/specs/client.webrtc.json
index 4f01793..15a55cc 100644
--- a/infra/specs/client.webrtc.json
+++ b/infra/specs/client.webrtc.json
@@ -3325,6 +3325,32 @@
"test_id_prefix": "ninja://pc:peerconnection_unittests/"
},
{
+ "args": [
+ ".",
+ "--gtest_output=json:${ISOLATED_OUTDIR}/gtest_output.json"
+ ],
+ "isolate_name": "pipewire_shared_screencast_stream_test",
+ "merge": {
+ "args": [],
+ "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+ },
+ "name": "pipewire_shared_screencast_stream_test",
+ "resultdb": {
+ "result_file": "${ISOLATED_OUTDIR}/gtest_output.json",
+ "result_format": "gtest_json"
+ },
+ "swarming": {
+ "can_use_on_swarming_builders": true,
+ "dimension_sets": [
+ {
+ "cpu": "x86-64",
+ "os": "Ubuntu-18.04"
+ }
+ ]
+ },
+ "test_id_prefix": "ninja://modules/desktop_capture:pipewire_shared_screencast_stream_test/"
+ },
+ {
"isolate_name": "rtc_media_unittests",
"merge": {
"args": [],
@@ -4624,6 +4650,32 @@
"test_id_prefix": "ninja://pc:peerconnection_unittests/"
},
{
+ "args": [
+ ".",
+ "--gtest_output=json:${ISOLATED_OUTDIR}/gtest_output.json"
+ ],
+ "isolate_name": "pipewire_shared_screencast_stream_test",
+ "merge": {
+ "args": [],
+ "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+ },
+ "name": "pipewire_shared_screencast_stream_test",
+ "resultdb": {
+ "result_file": "${ISOLATED_OUTDIR}/gtest_output.json",
+ "result_format": "gtest_json"
+ },
+ "swarming": {
+ "can_use_on_swarming_builders": true,
+ "dimension_sets": [
+ {
+ "cpu": "x86-64",
+ "os": "Ubuntu-18.04"
+ }
+ ]
+ },
+ "test_id_prefix": "ninja://modules/desktop_capture:pipewire_shared_screencast_stream_test/"
+ },
+ {
"isolate_name": "rtc_media_unittests",
"merge": {
"args": [],
@@ -5057,6 +5109,32 @@
"test_id_prefix": "ninja://pc:peerconnection_unittests/"
},
{
+ "args": [
+ ".",
+ "--gtest_output=json:${ISOLATED_OUTDIR}/gtest_output.json"
+ ],
+ "isolate_name": "pipewire_shared_screencast_stream_test",
+ "merge": {
+ "args": [],
+ "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+ },
+ "name": "pipewire_shared_screencast_stream_test",
+ "resultdb": {
+ "result_file": "${ISOLATED_OUTDIR}/gtest_output.json",
+ "result_format": "gtest_json"
+ },
+ "swarming": {
+ "can_use_on_swarming_builders": true,
+ "dimension_sets": [
+ {
+ "cpu": "x86-64",
+ "os": "Ubuntu-18.04"
+ }
+ ]
+ },
+ "test_id_prefix": "ninja://modules/desktop_capture:pipewire_shared_screencast_stream_test/"
+ },
+ {
"isolate_name": "rtc_media_unittests",
"merge": {
"args": [],
@@ -6359,6 +6437,32 @@
"test_id_prefix": "ninja://pc:peerconnection_unittests/"
},
{
+ "args": [
+ ".",
+ "--gtest_output=json:${ISOLATED_OUTDIR}/gtest_output.json"
+ ],
+ "isolate_name": "pipewire_shared_screencast_stream_test",
+ "merge": {
+ "args": [],
+ "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+ },
+ "name": "pipewire_shared_screencast_stream_test",
+ "resultdb": {
+ "result_file": "${ISOLATED_OUTDIR}/gtest_output.json",
+ "result_format": "gtest_json"
+ },
+ "swarming": {
+ "can_use_on_swarming_builders": true,
+ "dimension_sets": [
+ {
+ "cpu": "x86-64",
+ "os": "Ubuntu-18.04"
+ }
+ ]
+ },
+ "test_id_prefix": "ninja://modules/desktop_capture:pipewire_shared_screencast_stream_test/"
+ },
+ {
"isolate_name": "rtc_media_unittests",
"merge": {
"args": [],
@@ -6793,6 +6897,32 @@
"test_id_prefix": "ninja://pc:peerconnection_unittests/"
},
{
+ "args": [
+ ".",
+ "--gtest_output=json:${ISOLATED_OUTDIR}/gtest_output.json"
+ ],
+ "isolate_name": "pipewire_shared_screencast_stream_test",
+ "merge": {
+ "args": [],
+ "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+ },
+ "name": "pipewire_shared_screencast_stream_test",
+ "resultdb": {
+ "result_file": "${ISOLATED_OUTDIR}/gtest_output.json",
+ "result_format": "gtest_json"
+ },
+ "swarming": {
+ "can_use_on_swarming_builders": true,
+ "dimension_sets": [
+ {
+ "cpu": "x86-64",
+ "os": "Ubuntu-18.04"
+ }
+ ]
+ },
+ "test_id_prefix": "ninja://modules/desktop_capture:pipewire_shared_screencast_stream_test/"
+ },
+ {
"isolate_name": "rtc_media_unittests",
"merge": {
"args": [],
diff --git a/infra/specs/gn_isolate_map.pyl b/infra/specs/gn_isolate_map.pyl
index d35a46c..598b15f 100644
--- a/infra/specs/gn_isolate_map.pyl
+++ b/infra/specs/gn_isolate_map.pyl
@@ -80,6 +80,14 @@
"label": "//pc:peerconnection_unittests",
"type": "console_test_launcher",
},
+ "pipewire_shared_screencast_stream_test": {
+ "label":
+ "//modules/desktop_capture:pipewire_shared_screencast_stream_test",
+ "type":
+ "script",
+ "script":
+ "//modules/desktop_capture/linux/wayland/test/shared_screencast_stream_test.py",
+ },
"rtc_media_unittests": {
"label": "//media:rtc_media_unittests",
"type": "console_test_launcher",
diff --git a/infra/specs/test_suites.pyl b/infra/specs/test_suites.pyl
index d4b8cf9..f207c3d 100644
--- a/infra/specs/test_suites.pyl
+++ b/infra/specs/test_suites.pyl
@@ -183,6 +183,12 @@
'voip_unittests': {},
'webrtc_nonparallel_tests': {},
},
+ 'linux_desktop_specific_tests': {
+ 'pipewire_shared_screencast_stream_test': {
+ 'args': ['.'],
+ 'mixins': ['resultdb-gtest-json-format'],
+ },
+ },
'more_configs_tests': {
'peerconnection_unittests': {
'swarming': {
@@ -225,5 +231,20 @@
'desktop_tests',
'video_capture_tests',
],
+ 'linux_desktop_tests_tryserver': [
+ 'desktop_tests',
+ 'linux_desktop_specific_tests',
+ 'video_capture_tests_tryserver',
+ 'webrtc_perf_tests_tryserver',
+ ],
+ 'linux_desktop_tests_with_video_capture': [
+ 'desktop_tests',
+ 'linux_desktop_specific_tests',
+ 'video_capture_tests',
+ ],
+ 'linux_tests': [
+ 'desktop_tests',
+ 'linux_desktop_specific_tests',
+ ],
},
}
diff --git a/infra/specs/tryserver.webrtc.json b/infra/specs/tryserver.webrtc.json
index 6db19d0..666bbc7 100644
--- a/infra/specs/tryserver.webrtc.json
+++ b/infra/specs/tryserver.webrtc.json
@@ -5942,6 +5942,32 @@
"test_id_prefix": "ninja://pc:peerconnection_unittests/"
},
{
+ "args": [
+ ".",
+ "--gtest_output=json:${ISOLATED_OUTDIR}/gtest_output.json"
+ ],
+ "isolate_name": "pipewire_shared_screencast_stream_test",
+ "merge": {
+ "args": [],
+ "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+ },
+ "name": "pipewire_shared_screencast_stream_test",
+ "resultdb": {
+ "result_file": "${ISOLATED_OUTDIR}/gtest_output.json",
+ "result_format": "gtest_json"
+ },
+ "swarming": {
+ "can_use_on_swarming_builders": true,
+ "dimension_sets": [
+ {
+ "cpu": "x86-64",
+ "os": "Ubuntu-18.04"
+ }
+ ]
+ },
+ "test_id_prefix": "ninja://modules/desktop_capture:pipewire_shared_screencast_stream_test/"
+ },
+ {
"isolate_name": "rtc_media_unittests",
"merge": {
"args": [],
@@ -6381,6 +6407,32 @@
"test_id_prefix": "ninja://pc:peerconnection_unittests/"
},
{
+ "args": [
+ ".",
+ "--gtest_output=json:${ISOLATED_OUTDIR}/gtest_output.json"
+ ],
+ "isolate_name": "pipewire_shared_screencast_stream_test",
+ "merge": {
+ "args": [],
+ "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+ },
+ "name": "pipewire_shared_screencast_stream_test",
+ "resultdb": {
+ "result_file": "${ISOLATED_OUTDIR}/gtest_output.json",
+ "result_format": "gtest_json"
+ },
+ "swarming": {
+ "can_use_on_swarming_builders": true,
+ "dimension_sets": [
+ {
+ "cpu": "x86-64",
+ "os": "Ubuntu-18.04"
+ }
+ ]
+ },
+ "test_id_prefix": "ninja://modules/desktop_capture:pipewire_shared_screencast_stream_test/"
+ },
+ {
"isolate_name": "rtc_media_unittests",
"merge": {
"args": [],
@@ -6815,6 +6867,32 @@
"test_id_prefix": "ninja://pc:peerconnection_unittests/"
},
{
+ "args": [
+ ".",
+ "--gtest_output=json:${ISOLATED_OUTDIR}/gtest_output.json"
+ ],
+ "isolate_name": "pipewire_shared_screencast_stream_test",
+ "merge": {
+ "args": [],
+ "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+ },
+ "name": "pipewire_shared_screencast_stream_test",
+ "resultdb": {
+ "result_file": "${ISOLATED_OUTDIR}/gtest_output.json",
+ "result_format": "gtest_json"
+ },
+ "swarming": {
+ "can_use_on_swarming_builders": true,
+ "dimension_sets": [
+ {
+ "cpu": "x86-64",
+ "os": "Ubuntu-18.04"
+ }
+ ]
+ },
+ "test_id_prefix": "ninja://modules/desktop_capture:pipewire_shared_screencast_stream_test/"
+ },
+ {
"isolate_name": "rtc_media_unittests",
"merge": {
"args": [],
@@ -7707,6 +7785,32 @@
"test_id_prefix": "ninja://pc:peerconnection_unittests/"
},
{
+ "args": [
+ ".",
+ "--gtest_output=json:${ISOLATED_OUTDIR}/gtest_output.json"
+ ],
+ "isolate_name": "pipewire_shared_screencast_stream_test",
+ "merge": {
+ "args": [],
+ "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+ },
+ "name": "pipewire_shared_screencast_stream_test",
+ "resultdb": {
+ "result_file": "${ISOLATED_OUTDIR}/gtest_output.json",
+ "result_format": "gtest_json"
+ },
+ "swarming": {
+ "can_use_on_swarming_builders": true,
+ "dimension_sets": [
+ {
+ "cpu": "x86-64",
+ "os": "Ubuntu-18.04"
+ }
+ ]
+ },
+ "test_id_prefix": "ninja://modules/desktop_capture:pipewire_shared_screencast_stream_test/"
+ },
+ {
"isolate_name": "rtc_media_unittests",
"merge": {
"args": [],
@@ -8622,6 +8726,32 @@
"test_id_prefix": "ninja://pc:peerconnection_unittests/"
},
{
+ "args": [
+ ".",
+ "--gtest_output=json:${ISOLATED_OUTDIR}/gtest_output.json"
+ ],
+ "isolate_name": "pipewire_shared_screencast_stream_test",
+ "merge": {
+ "args": [],
+ "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+ },
+ "name": "pipewire_shared_screencast_stream_test",
+ "resultdb": {
+ "result_file": "${ISOLATED_OUTDIR}/gtest_output.json",
+ "result_format": "gtest_json"
+ },
+ "swarming": {
+ "can_use_on_swarming_builders": true,
+ "dimension_sets": [
+ {
+ "cpu": "x86-64",
+ "os": "Ubuntu-18.04"
+ }
+ ]
+ },
+ "test_id_prefix": "ninja://modules/desktop_capture:pipewire_shared_screencast_stream_test/"
+ },
+ {
"isolate_name": "rtc_media_unittests",
"merge": {
"args": [],
@@ -9055,6 +9185,32 @@
"test_id_prefix": "ninja://pc:peerconnection_unittests/"
},
{
+ "args": [
+ ".",
+ "--gtest_output=json:${ISOLATED_OUTDIR}/gtest_output.json"
+ ],
+ "isolate_name": "pipewire_shared_screencast_stream_test",
+ "merge": {
+ "args": [],
+ "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+ },
+ "name": "pipewire_shared_screencast_stream_test",
+ "resultdb": {
+ "result_file": "${ISOLATED_OUTDIR}/gtest_output.json",
+ "result_format": "gtest_json"
+ },
+ "swarming": {
+ "can_use_on_swarming_builders": true,
+ "dimension_sets": [
+ {
+ "cpu": "x86-64",
+ "os": "Ubuntu-18.04"
+ }
+ ]
+ },
+ "test_id_prefix": "ninja://modules/desktop_capture:pipewire_shared_screencast_stream_test/"
+ },
+ {
"isolate_name": "rtc_media_unittests",
"merge": {
"args": [],
diff --git a/infra/specs/waterfalls.pyl b/infra/specs/waterfalls.pyl
index bad6afa..bf7ea4c 100644
--- a/infra/specs/waterfalls.pyl
+++ b/infra/specs/waterfalls.pyl
@@ -83,7 +83,7 @@
'os_type': 'linux',
'mixins': ['linux-bionic', 'x86-64', 'resultdb-json-format'],
'test_suites': {
- 'isolated_scripts': 'desktop_tests',
+ 'isolated_scripts': 'linux_tests',
},
},
'Linux MSan': {
@@ -91,6 +91,9 @@
'mixins': ['linux-bionic', 'x86-64', 'resultdb-json-format'],
'test_suites': {
'isolated_scripts': 'desktop_tests',
+ # TODO(crbug.com/webrtc/14568): use 'linux_tests'
+ # Fails on "MemorySanitizer: use-of-uninitialized-value in
+ # libpipewire-0.3.so"
},
},
'Linux Tsan v2': {
@@ -98,20 +101,23 @@
'mixins': ['linux-bionic', 'x86-64', 'resultdb-json-format'],
'test_suites': {
'isolated_scripts': 'desktop_tests',
+ # TODO(crbug.com/webrtc/14568): use 'linux_tests'
+ # Fails on "ThreadSanitizer: data race on vptr (ctor/dtor vs
+ # virtual call) in shared_screencast_stream_test"
},
},
'Linux UBSan': {
'os_type': 'linux',
'mixins': ['linux-bionic', 'x86-64', 'resultdb-json-format'],
'test_suites': {
- 'isolated_scripts': 'desktop_tests',
+ 'isolated_scripts': 'linux_tests',
},
},
'Linux UBSan vptr': {
'os_type': 'linux',
'mixins': ['linux-bionic', 'x86-64', 'resultdb-json-format'],
'test_suites': {
- 'isolated_scripts': 'desktop_tests',
+ 'isolated_scripts': 'linux_tests',
},
},
'Linux32 Debug': {
@@ -135,7 +141,7 @@
'os_type': 'linux',
'mixins': ['linux-bionic', 'x86-64', 'resultdb-json-format'],
'test_suites': {
- 'isolated_scripts': 'desktop_tests',
+ 'isolated_scripts': 'linux_tests',
},
},
'Linux64 Debug (ARM)': {},
@@ -143,7 +149,7 @@
'os_type': 'linux',
'mixins': ['linux-bionic', 'x86-64', 'resultdb-json-format'],
'test_suites': {
- 'isolated_scripts': 'desktop_tests_with_video_capture',
+ 'isolated_scripts': 'linux_desktop_tests_with_video_capture',
},
},
'Linux64 Release (ARM)': {},
@@ -466,7 +472,7 @@
'os_type': 'linux',
'mixins': ['linux-bionic', 'x86-64', 'resultdb-json-format'],
'test_suites': {
- 'isolated_scripts': 'desktop_tests',
+ 'isolated_scripts': 'linux_tests',
},
},
'linux_compile_arm64_dbg': {},
@@ -479,7 +485,7 @@
'os_type': 'linux',
'mixins': ['linux-bionic', 'x86-64', 'resultdb-json-format'],
'test_suites': {
- 'isolated_scripts': 'desktop_tests',
+ 'isolated_scripts': 'linux_tests',
},
},
'linux_libfuzzer_rel': {},
@@ -487,7 +493,7 @@
'os_type': 'linux',
'mixins': ['linux-bionic', 'x86-64', 'resultdb-json-format'],
'test_suites': {
- 'isolated_scripts': 'desktop_tests',
+ 'isolated_scripts': 'linux_tests',
},
},
'linux_more_configs': {
@@ -502,13 +508,16 @@
'mixins': ['linux-bionic', 'x86-64', 'resultdb-json-format'],
'test_suites': {
'isolated_scripts': 'desktop_tests',
+ # TODO(crbug.com/webrtc/14568): use 'linux_tests'
+ # Fails on "MemorySanitizer: use-of-uninitialized-value in
+ # libpipewire-0.3.so"
},
},
'linux_rel': {
'os_type': 'linux',
'mixins': ['linux-bionic', 'x86-64', 'resultdb-json-format'],
'test_suites': {
- 'isolated_scripts': 'desktop_tests_tryserver',
+ 'isolated_scripts': 'linux_desktop_tests_tryserver',
},
},
'linux_tsan2': {
@@ -516,20 +525,23 @@
'mixins': ['linux-bionic', 'x86-64', 'resultdb-json-format'],
'test_suites': {
'isolated_scripts': 'desktop_tests',
+ # TODO(crbug.com/webrtc/14568): use 'linux_tests'
+ # Fails on "ThreadSanitizer: data race on vptr (ctor/dtor vs
+ # virtual call) in shared_screencast_stream_test"
},
},
'linux_ubsan': {
'os_type': 'linux',
'mixins': ['linux-bionic', 'x86-64', 'resultdb-json-format'],
'test_suites': {
- 'isolated_scripts': 'desktop_tests',
+ 'isolated_scripts': 'linux_tests',
},
},
'linux_ubsan_vptr': {
'os_type': 'linux',
'mixins': ['linux-bionic', 'x86-64', 'resultdb-json-format'],
'test_suites': {
- 'isolated_scripts': 'desktop_tests',
+ 'isolated_scripts': 'linux_tests',
},
},
'linux_x86_dbg': {
diff --git a/modules/desktop_capture/BUILD.gn b/modules/desktop_capture/BUILD.gn
index dd5b37b..db70df2 100644
--- a/modules/desktop_capture/BUILD.gn
+++ b/modules/desktop_capture/BUILD.gn
@@ -95,6 +95,66 @@
}
}
+ if ((is_linux || is_chromeos) && rtc_use_pipewire) {
+ rtc_test("shared_screencast_stream_test") {
+ testonly = true
+
+ sources = [
+ "linux/wayland/shared_screencast_stream_unittest.cc",
+ "linux/wayland/test/fake_screencast_stream.cc",
+ "linux/wayland/test/fake_screencast_stream.h",
+ ]
+
+ configs += [
+ ":gio",
+ ":pipewire",
+ ":gbm",
+ ":egl",
+ ":epoxy",
+ ":libdrm",
+ ]
+
+ deps = [
+ ":desktop_capture",
+ ":desktop_capture_mock",
+ ":primitives",
+ "../../rtc_base:checks",
+ "../../rtc_base:logging",
+ "../../rtc_base:random",
+ "../../rtc_base:timeutils",
+
+ # TODO(bugs.webrtc.org/9987): Remove this dep on rtc_base:rtc_base once
+ # rtc_base:threading is fully defined.
+ "../../rtc_base:rtc_base",
+ "../../rtc_base:task_queue_for_test",
+ "../../rtc_base:threading",
+ "../../system_wrappers",
+ "../../test:test_main",
+ "../../test:test_support",
+ "//api/units:time_delta",
+ "//rtc_base:rtc_event",
+ ]
+
+ if (!rtc_link_pipewire) {
+ deps += [ ":pipewire_stubs" ]
+ }
+
+ public_configs = [ ":pipewire_config" ]
+ }
+
+ group("pipewire_shared_screencast_stream_test") {
+ testonly = true
+
+ deps = [ ":shared_screencast_stream_test" ]
+
+ data = [
+ "../../third_party/pipewire",
+ "linux/wayland/test/shared_screencast_stream_test.py",
+ "${root_out_dir}/shared_screencast_stream_test",
+ ]
+ }
+ }
+
rtc_library("desktop_capture_unittests") {
testonly = true
diff --git a/modules/desktop_capture/linux/wayland/pipewire.sigs b/modules/desktop_capture/linux/wayland/pipewire.sigs
index 06a97b8..139a8c3 100644
--- a/modules/desktop_capture/linux/wayland/pipewire.sigs
+++ b/modules/desktop_capture/linux/wayland/pipewire.sigs
@@ -31,6 +31,8 @@
int pw_stream_queue_buffer(pw_stream *stream, pw_buffer *buffer);
int pw_stream_set_active(pw_stream *stream, bool active);
int pw_stream_update_params(pw_stream *stream, const spa_pod **params, uint32_t n_params);
+uint32_t pw_stream_get_node_id(pw_stream *stream);
+pw_stream_state pw_stream_get_state(pw_stream *stream, const char **error);
// thread-loop.h
void pw_thread_loop_destroy(pw_thread_loop *loop);
diff --git a/modules/desktop_capture/linux/wayland/shared_screencast_stream.cc b/modules/desktop_capture/linux/wayland/shared_screencast_stream.cc
index 48a15c1..aacf68e 100644
--- a/modules/desktop_capture/linux/wayland/shared_screencast_stream.cc
+++ b/modules/desktop_capture/linux/wayland/shared_screencast_stream.cc
@@ -21,8 +21,6 @@
#include "absl/memory/memory.h"
#include "modules/desktop_capture/linux/wayland/egl_dmabuf.h"
#include "modules/desktop_capture/linux/wayland/screencast_stream_utils.h"
-#include "modules/desktop_capture/screen_capture_frame_queue.h"
-#include "modules/desktop_capture/shared_desktop_frame.h"
#include "rtc_base/checks.h"
#include "rtc_base/logging.h"
#include "rtc_base/sanitizer.h"
@@ -90,8 +88,11 @@
uint32_t width = 0,
uint32_t height = 0);
void UpdateScreenCastStreamResolution(uint32_t width, uint32_t height);
+ void SetObserver(SharedScreenCastStream::Observer* observer) {
+ observer_ = observer;
+ }
void StopScreenCastStream();
- std::unique_ptr<DesktopFrame> CaptureFrame();
+ std::unique_ptr<SharedDesktopFrame> CaptureFrame();
std::unique_ptr<MouseCursor> CaptureCursor();
DesktopVector CaptureCursorPosition();
@@ -99,6 +100,7 @@
// Stops the streams and cleans up any in-use elements.
void StopAndCleanupStream();
+ SharedScreenCastStream::Observer* observer_ = nullptr;
uint32_t pw_stream_node_id_ = 0;
DesktopSize stream_size_ = {};
@@ -575,15 +577,15 @@
pw_main_loop_ = nullptr;
}
-std::unique_ptr<DesktopFrame> SharedScreenCastStreamPrivate::CaptureFrame() {
+std::unique_ptr<SharedDesktopFrame>
+SharedScreenCastStreamPrivate::CaptureFrame() {
webrtc::MutexLock lock(&queue_lock_);
if (!pw_stream_ || !queue_.current_frame()) {
- return std::unique_ptr<DesktopFrame>{};
+ return std::unique_ptr<SharedDesktopFrame>{};
}
- std::unique_ptr<SharedDesktopFrame> frame = queue_.current_frame()->Share();
- return std::move(frame);
+ return queue_.current_frame()->Share();
}
std::unique_ptr<MouseCursor> SharedScreenCastStreamPrivate::CaptureCursor() {
@@ -628,8 +630,18 @@
DesktopRect::MakeWH(bitmap->size.width, bitmap->size.height));
mouse_cursor_ = std::make_unique<MouseCursor>(
mouse_frame, DesktopVector(cursor->hotspot.x, cursor->hotspot.y));
+
+ // For testing purpose
+ if (observer_) {
+ observer_->OnCursorShapeChanged();
+ }
}
mouse_cursor_position_.set(cursor->position.x, cursor->position.y);
+
+ // For testing purpose
+ if (observer_) {
+ observer_->OnCursorPositionChanged();
+ }
}
}
@@ -704,6 +716,10 @@
}
if (!src) {
+ // For testing purpose
+ if (observer_) {
+ observer_->OnFailedToProcessBuffer();
+ }
return;
}
@@ -730,6 +746,11 @@
videocrop_metadata->region.size.height >
static_cast<uint32_t>(stream_size_.height()))) {
RTC_LOG(LS_ERROR) << "Stream metadata sizes are wrong!";
+
+ if (observer_) {
+ observer_->OnFailedToProcessBuffer();
+ }
+
return;
}
@@ -795,6 +816,10 @@
queue_.MoveToNextFrame();
if (queue_.current_frame() && queue_.current_frame()->IsShared()) {
RTC_DLOG(LS_WARNING) << "Overwriting frame that is still shared";
+
+ if (observer_) {
+ observer_->OnFailedToProcessBuffer();
+ }
}
if (!queue_.current_frame() ||
@@ -821,6 +846,11 @@
queue_.current_frame()->mutable_updated_region()->SetRect(
DesktopRect::MakeSize(queue_.current_frame()->size()));
+
+ // For testing purpose
+ if (observer_) {
+ observer_->OnDesktopFrameChanged();
+ }
}
void SharedScreenCastStreamPrivate::ConvertRGBxToBGRx(uint8_t* frame,
@@ -861,11 +891,16 @@
private_->UpdateScreenCastStreamResolution(width, height);
}
+void SharedScreenCastStream::SetObserver(
+ SharedScreenCastStream::Observer* observer) {
+ private_->SetObserver(observer);
+}
+
void SharedScreenCastStream::StopScreenCastStream() {
private_->StopScreenCastStream();
}
-std::unique_ptr<DesktopFrame> SharedScreenCastStream::CaptureFrame() {
+std::unique_ptr<SharedDesktopFrame> SharedScreenCastStream::CaptureFrame() {
return private_->CaptureFrame();
}
diff --git a/modules/desktop_capture/linux/wayland/shared_screencast_stream.h b/modules/desktop_capture/linux/wayland/shared_screencast_stream.h
index 66a3f45..c58d840 100644
--- a/modules/desktop_capture/linux/wayland/shared_screencast_stream.h
+++ b/modules/desktop_capture/linux/wayland/shared_screencast_stream.h
@@ -16,8 +16,9 @@
#include "absl/types/optional.h"
#include "api/ref_counted_base.h"
#include "api/scoped_refptr.h"
-#include "modules/desktop_capture/desktop_frame.h"
#include "modules/desktop_capture/mouse_cursor.h"
+#include "modules/desktop_capture/screen_capture_frame_queue.h"
+#include "modules/desktop_capture/shared_desktop_frame.h"
#include "rtc_base/system/rtc_export.h"
namespace webrtc {
@@ -27,6 +28,18 @@
class RTC_EXPORT SharedScreenCastStream
: public rtc::RefCountedNonVirtual<SharedScreenCastStream> {
public:
+ class Observer {
+ public:
+ virtual void OnCursorPositionChanged() = 0;
+ virtual void OnCursorShapeChanged() = 0;
+ virtual void OnDesktopFrameChanged() = 0;
+ virtual void OnFailedToProcessBuffer() = 0;
+
+ protected:
+ Observer() = default;
+ virtual ~Observer() = default;
+ };
+
static rtc::scoped_refptr<SharedScreenCastStream> CreateDefault();
bool StartScreenCastStream(uint32_t stream_node_id);
@@ -35,6 +48,7 @@
uint32_t width = 0,
uint32_t height = 0);
void UpdateScreenCastStreamResolution(uint32_t width, uint32_t height);
+ void SetObserver(SharedScreenCastStream::Observer* observer);
void StopScreenCastStream();
// Below functions return the most recent information we get from a
@@ -47,7 +61,7 @@
// Returns the most recent screen/window frame we obtained from PipeWire
// buffer. Will return an empty frame in case we didn't manage to get a frame
// from PipeWire buffer.
- std::unique_ptr<DesktopFrame> CaptureFrame();
+ std::unique_ptr<SharedDesktopFrame> CaptureFrame();
// Returns the most recent mouse cursor image. Will return an nullptr cursor
// in case we didn't manage to get a cursor from PipeWire buffer. NOTE: the
@@ -65,6 +79,13 @@
SharedScreenCastStream();
private:
+ friend class SharedScreenCastStreamPrivate;
+ // Allows test cases to use private functionality
+ friend class PipeWireStreamTest;
+
+ // FIXME: is this a useful thing to be public?
+ explicit SharedScreenCastStream(Observer* notifier);
+
SharedScreenCastStream(const SharedScreenCastStream&) = delete;
SharedScreenCastStream& operator=(const SharedScreenCastStream&) = delete;
diff --git a/modules/desktop_capture/linux/wayland/shared_screencast_stream_unittest.cc b/modules/desktop_capture/linux/wayland/shared_screencast_stream_unittest.cc
new file mode 100644
index 0000000..23296f5
--- /dev/null
+++ b/modules/desktop_capture/linux/wayland/shared_screencast_stream_unittest.cc
@@ -0,0 +1,143 @@
+/*
+ * Copyright (c) 2022 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/linux/wayland/shared_screencast_stream.h"
+
+#include <memory>
+#include <utility>
+
+#include "api/units/time_delta.h"
+#include "modules/desktop_capture/desktop_capturer.h"
+#include "modules/desktop_capture/desktop_frame.h"
+#include "modules/desktop_capture/linux/wayland/test/fake_screencast_stream.h"
+#include "modules/desktop_capture/rgba_color.h"
+#include "rtc_base/event.h"
+#include "test/gmock.h"
+#include "test/gtest.h"
+
+using ::testing::_;
+using ::testing::Ge;
+using ::testing::Invoke;
+
+namespace webrtc {
+
+constexpr TimeDelta kShortWait = TimeDelta::Seconds(2);
+constexpr TimeDelta kLongWait = TimeDelta::Seconds(10);
+
+constexpr int kBytesPerPixel = 4;
+constexpr int32_t kWidth = 800;
+constexpr int32_t kHeight = 640;
+
+class PipeWireStreamTest : public ::testing::Test,
+ public FakeScreenCastStream::Observer,
+ public SharedScreenCastStream::Observer {
+ public:
+ PipeWireStreamTest()
+ : fake_screencast_stream_(
+ std::make_unique<FakeScreenCastStream>(this, kWidth, kHeight)),
+ shared_screencast_stream_(new SharedScreenCastStream()) {
+ shared_screencast_stream_->SetObserver(this);
+ }
+
+ ~PipeWireStreamTest() override {}
+
+ // FakeScreenCastPortal::Observer
+ MOCK_METHOD(void, OnFrameRecorded, (), (override));
+ MOCK_METHOD(void, OnStreamReady, (uint32_t stream_node_id), (override));
+ MOCK_METHOD(void, OnStartStreaming, (), (override));
+ MOCK_METHOD(void, OnStopStreaming, (), (override));
+
+ // SharedScreenCastStream::Observer
+ MOCK_METHOD(void, OnCursorPositionChanged, (), (override));
+ MOCK_METHOD(void, OnCursorShapeChanged, (), (override));
+ MOCK_METHOD(void, OnDesktopFrameChanged, (), (override));
+ MOCK_METHOD(void, OnFailedToProcessBuffer, (), (override));
+
+ void StartScreenCastStream(uint32_t stream_node_id) {
+ shared_screencast_stream_->StartScreenCastStream(stream_node_id);
+ }
+
+ protected:
+ uint recorded_frames_ = 0;
+ bool streaming_ = false;
+ std::unique_ptr<FakeScreenCastStream> fake_screencast_stream_;
+ rtc::scoped_refptr<SharedScreenCastStream> shared_screencast_stream_;
+};
+
+TEST_F(PipeWireStreamTest, TestPipeWire) {
+ // Set expectations for PipeWire to successfully connect both streams
+ rtc::Event waitConnectEvent;
+ EXPECT_CALL(*this, OnStreamReady(_))
+ .WillOnce(Invoke(this, &PipeWireStreamTest::StartScreenCastStream));
+ EXPECT_CALL(*this, OnStartStreaming).WillOnce([&waitConnectEvent] {
+ waitConnectEvent.Set();
+ });
+
+ // Give it some time to connect, the order between these shouldn't matter, but
+ // we need to be sure we are connected before we proceed to work with frames.
+ waitConnectEvent.Wait(kLongWait);
+
+ rtc::Event frameRetrievedEvent;
+ EXPECT_CALL(*this, OnFrameRecorded).Times(3);
+ EXPECT_CALL(*this, OnDesktopFrameChanged)
+ .WillRepeatedly([&frameRetrievedEvent] { frameRetrievedEvent.Set(); });
+
+ // Record a frame in FakePipeWireStream
+ RgbaColor red_color(255, 0, 0);
+ fake_screencast_stream_->RecordFrame(red_color);
+ frameRetrievedEvent.Wait(kShortWait);
+
+ // Retrieve a frame from SharedScreenCastStream
+ frameRetrievedEvent.Wait(kShortWait);
+ std::unique_ptr<SharedDesktopFrame> frame =
+ shared_screencast_stream_->CaptureFrame();
+
+ // Check frame parameters
+ ASSERT_NE(frame, nullptr);
+ ASSERT_NE(frame->data(), nullptr);
+ EXPECT_EQ(frame->rect().width(), kWidth);
+ EXPECT_EQ(frame->rect().height(), kHeight);
+ EXPECT_EQ(frame->stride(), frame->rect().width() * kBytesPerPixel);
+ EXPECT_EQ(frame->data()[0], static_cast<uint8_t>(red_color.ToUInt32()));
+
+ // Test DesktopFrameQueue
+ RgbaColor green_color(0, 255, 0);
+ fake_screencast_stream_->RecordFrame(green_color);
+ frameRetrievedEvent.Wait(kShortWait);
+ std::unique_ptr<SharedDesktopFrame> frame2 =
+ shared_screencast_stream_->CaptureFrame();
+ ASSERT_NE(frame2, nullptr);
+ ASSERT_NE(frame2->data(), nullptr);
+ EXPECT_EQ(frame2->rect().width(), kWidth);
+ EXPECT_EQ(frame2->rect().height(), kHeight);
+ EXPECT_EQ(frame2->stride(), frame->rect().width() * kBytesPerPixel);
+ EXPECT_EQ(frame2->data()[0], static_cast<uint8_t>(green_color.ToUInt32()));
+
+ // Thanks to DesktopFrameQueue we should be able to have two frames shared
+ EXPECT_EQ(frame->IsShared(), true);
+ EXPECT_EQ(frame2->IsShared(), true);
+ EXPECT_NE(frame->data(), frame2->data());
+
+ // This should result into overwriting a frame in use
+ rtc::Event frameRecordedEvent;
+ RgbaColor blue_color(0, 0, 255);
+ EXPECT_CALL(*this, OnFailedToProcessBuffer).WillOnce([&frameRecordedEvent] {
+ frameRecordedEvent.Set();
+ });
+
+ fake_screencast_stream_->RecordFrame(blue_color);
+ frameRecordedEvent.Wait(kShortWait);
+
+ // Test disconnection from stream
+ EXPECT_CALL(*this, OnStopStreaming);
+ shared_screencast_stream_->StopScreenCastStream();
+}
+
+} // namespace webrtc
diff --git a/modules/desktop_capture/linux/wayland/test/fake_screencast_stream.cc b/modules/desktop_capture/linux/wayland/test/fake_screencast_stream.cc
new file mode 100644
index 0000000..8f973da
--- /dev/null
+++ b/modules/desktop_capture/linux/wayland/test/fake_screencast_stream.cc
@@ -0,0 +1,370 @@
+
+/*
+ * Copyright 2022 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/linux/wayland/test/fake_screencast_stream.h"
+
+#include <fcntl.h>
+#include <sys/mman.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "rtc_base/logging.h"
+
+#if defined(WEBRTC_DLOPEN_PIPEWIRE)
+#include "modules/desktop_capture/linux/wayland/pipewire_stubs.h"
+using modules_desktop_capture_linux_wayland::InitializeStubs;
+using modules_desktop_capture_linux_wayland::kModulePipewire;
+using modules_desktop_capture_linux_wayland::StubPathMap;
+#endif // defined(WEBRTC_DLOPEN_PIPEWIRE)
+
+namespace webrtc {
+
+#if defined(WEBRTC_DLOPEN_PIPEWIRE)
+const char kPipeWireLib[] = "libpipewire-0.3.so.0";
+#endif
+
+constexpr int kBytesPerPixel = 4;
+
+FakeScreenCastStream::FakeScreenCastStream(Observer* observer,
+ uint32_t width,
+ uint32_t height)
+ : observer_(observer), width_(width), height_(height) {
+#if defined(WEBRTC_DLOPEN_PIPEWIRE)
+ StubPathMap paths;
+
+ // Check if the PipeWire library is available.
+ paths[kModulePipewire].push_back(kPipeWireLib);
+
+ if (!InitializeStubs(paths)) {
+ RTC_LOG(LS_ERROR)
+ << "One of following libraries is missing on your system:\n"
+ << " - PipeWire (" << kPipeWireLib << ")\n";
+ return;
+ }
+#endif // defined(WEBRTC_DLOPEN_PIPEWIRE)
+
+ pw_init(/*argc=*/nullptr, /*argc=*/nullptr);
+
+ pw_main_loop_ = pw_thread_loop_new("pipewire-test-main-loop", nullptr);
+
+ pw_context_ =
+ pw_context_new(pw_thread_loop_get_loop(pw_main_loop_), nullptr, 0);
+ if (!pw_context_) {
+ RTC_LOG(LS_ERROR) << "PipeWire test: Failed to create PipeWire context";
+ return;
+ }
+
+ if (pw_thread_loop_start(pw_main_loop_) < 0) {
+ RTC_LOG(LS_ERROR) << "PipeWire test: Failed to start main PipeWire loop";
+ return;
+ }
+
+ // Initialize event handlers, remote end and stream-related.
+ pw_core_events_.version = PW_VERSION_CORE_EVENTS;
+ pw_core_events_.error = &OnCoreError;
+
+ pw_stream_events_.version = PW_VERSION_STREAM_EVENTS;
+ pw_stream_events_.add_buffer = &OnStreamAddBuffer;
+ pw_stream_events_.remove_buffer = &OnStreamRemoveBuffer;
+ pw_stream_events_.state_changed = &OnStreamStateChanged;
+ pw_stream_events_.param_changed = &OnStreamParamChanged;
+
+ {
+ PipeWireThreadLoopLock thread_loop_lock(pw_main_loop_);
+
+ pw_core_ = pw_context_connect(pw_context_, nullptr, 0);
+ if (!pw_core_) {
+ RTC_LOG(LS_ERROR) << "PipeWire test: Failed to connect PipeWire context";
+ return;
+ }
+
+ pw_core_add_listener(pw_core_, &spa_core_listener_, &pw_core_events_, this);
+
+ pw_stream_ = pw_stream_new(pw_core_, "webrtc-test-stream", nullptr);
+
+ if (!pw_stream_) {
+ RTC_LOG(LS_ERROR) << "PipeWire test: Failed to create PipeWire stream";
+ return;
+ }
+
+ pw_stream_add_listener(pw_stream_, &spa_stream_listener_,
+ &pw_stream_events_, this);
+ uint8_t buffer[2048] = {};
+
+ spa_pod_builder builder = spa_pod_builder{buffer, sizeof(buffer)};
+
+ std::vector<const spa_pod*> params;
+
+ spa_rectangle resolution =
+ SPA_RECTANGLE(uint32_t(width_), uint32_t(height_));
+ params.push_back(BuildFormat(&builder, SPA_VIDEO_FORMAT_BGRx,
+ /*modifiers=*/{}, &resolution));
+
+ auto flags =
+ pw_stream_flags(PW_STREAM_FLAG_DRIVER | PW_STREAM_FLAG_ALLOC_BUFFERS);
+ if (pw_stream_connect(pw_stream_, PW_DIRECTION_OUTPUT, SPA_ID_INVALID,
+ flags, params.data(), params.size()) != 0) {
+ RTC_LOG(LS_ERROR) << "PipeWire test: Could not connect receiving stream.";
+ pw_stream_destroy(pw_stream_);
+ pw_stream_ = nullptr;
+ return;
+ }
+ }
+
+ return;
+}
+
+FakeScreenCastStream::~FakeScreenCastStream() {
+ if (pw_main_loop_) {
+ pw_thread_loop_stop(pw_main_loop_);
+ }
+
+ if (pw_stream_) {
+ pw_stream_destroy(pw_stream_);
+ }
+
+ if (pw_core_) {
+ pw_core_disconnect(pw_core_);
+ }
+
+ if (pw_context_) {
+ pw_context_destroy(pw_context_);
+ }
+
+ if (pw_main_loop_) {
+ pw_thread_loop_destroy(pw_main_loop_);
+ }
+}
+
+void FakeScreenCastStream::RecordFrame(RgbaColor rgba_color) {
+ const char* error;
+ if (pw_stream_get_state(pw_stream_, &error) != PW_STREAM_STATE_STREAMING) {
+ if (error) {
+ RTC_LOG(LS_ERROR)
+ << "PipeWire test: Failed to record frame: stream is not active: "
+ << error;
+ }
+ }
+
+ struct pw_buffer* buffer = pw_stream_dequeue_buffer(pw_stream_);
+ if (!buffer) {
+ RTC_LOG(LS_ERROR) << "PipeWire test: No available buffer";
+ return;
+ }
+
+ struct spa_buffer* spa_buffer = buffer->buffer;
+ struct spa_data* spa_data = spa_buffer->datas;
+ uint8_t* data = static_cast<uint8_t*>(spa_data->data);
+ if (!data) {
+ RTC_LOG(LS_ERROR)
+ << "PipeWire test: Failed to record frame: invalid buffer data";
+ pw_stream_queue_buffer(pw_stream_, buffer);
+ return;
+ }
+
+ const int stride = SPA_ROUND_UP_N(width_ * kBytesPerPixel, 4);
+
+ spa_data->chunk->offset = 0;
+ spa_data->chunk->size = height_ * stride;
+ spa_data->chunk->stride = stride;
+
+ uint32_t color = rgba_color.ToUInt32();
+ for (uint32_t i = 0; i < height_; i++) {
+ uint32_t* column = reinterpret_cast<uint32_t*>(data);
+ for (uint32_t j = 0; j < width_; j++) {
+ column[j] = color;
+ }
+ data += stride;
+ }
+
+ pw_stream_queue_buffer(pw_stream_, buffer);
+ if (observer_) {
+ observer_->OnFrameRecorded();
+ }
+}
+
+void FakeScreenCastStream::StartStreaming() {
+ if (pw_stream_ && pw_node_id_ != 0) {
+ pw_stream_set_active(pw_stream_, true);
+ }
+}
+
+void FakeScreenCastStream::StopStreaming() {
+ if (pw_stream_ && pw_node_id_ != 0) {
+ pw_stream_set_active(pw_stream_, false);
+ }
+}
+
+// static
+void FakeScreenCastStream::OnCoreError(void* data,
+ uint32_t id,
+ int seq,
+ int res,
+ const char* message) {
+ FakeScreenCastStream* that = static_cast<FakeScreenCastStream*>(data);
+ RTC_DCHECK(that);
+
+ RTC_LOG(LS_ERROR) << "PipeWire test: PipeWire remote error: " << message;
+}
+
+// static
+void FakeScreenCastStream::OnStreamStateChanged(void* data,
+ pw_stream_state old_state,
+ pw_stream_state state,
+ const char* error_message) {
+ FakeScreenCastStream* that = static_cast<FakeScreenCastStream*>(data);
+ RTC_DCHECK(that);
+
+ switch (state) {
+ case PW_STREAM_STATE_ERROR:
+ RTC_LOG(LS_ERROR) << "PipeWire test: PipeWire stream state error: "
+ << error_message;
+ break;
+ case PW_STREAM_STATE_PAUSED:
+ if (that->pw_node_id_ == 0 && that->pw_stream_) {
+ that->pw_node_id_ = pw_stream_get_node_id(that->pw_stream_);
+ that->observer_->OnStreamReady(that->pw_node_id_);
+ } else {
+ // Stop streaming
+ that->is_streaming_ = false;
+ that->observer_->OnStopStreaming();
+ }
+ break;
+ case PW_STREAM_STATE_STREAMING:
+ // Start streaming
+ that->is_streaming_ = true;
+ that->observer_->OnStartStreaming();
+ break;
+ case PW_STREAM_STATE_CONNECTING:
+ break;
+ case PW_STREAM_STATE_UNCONNECTED:
+ if (that->is_streaming_) {
+ // Stop streaming
+ that->is_streaming_ = false;
+ that->observer_->OnStopStreaming();
+ }
+ break;
+ }
+}
+
+// static
+void FakeScreenCastStream::OnStreamParamChanged(void* data,
+ uint32_t id,
+ const struct spa_pod* format) {
+ FakeScreenCastStream* that = static_cast<FakeScreenCastStream*>(data);
+ RTC_DCHECK(that);
+
+ RTC_LOG(LS_INFO) << "PipeWire test: PipeWire stream format changed.";
+ if (!format || id != SPA_PARAM_Format) {
+ return;
+ }
+
+ spa_format_video_raw_parse(format, &that->spa_video_format_);
+
+ auto stride = SPA_ROUND_UP_N(that->width_ * kBytesPerPixel, 4);
+
+ uint8_t buffer[1024] = {};
+ auto builder = spa_pod_builder{buffer, sizeof(buffer)};
+
+ // Setup buffers and meta header for new format.
+
+ std::vector<const spa_pod*> params;
+ const int buffer_types = (1 << SPA_DATA_MemFd);
+ spa_rectangle resolution = SPA_RECTANGLE(that->width_, that->height_);
+
+ params.push_back(reinterpret_cast<spa_pod*>(spa_pod_builder_add_object(
+ &builder, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
+ SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&resolution),
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(16, 2, 16),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), SPA_PARAM_BUFFERS_stride,
+ SPA_POD_Int(stride), SPA_PARAM_BUFFERS_size,
+ SPA_POD_Int(stride * that->height_), SPA_PARAM_BUFFERS_align,
+ SPA_POD_Int(16), SPA_PARAM_BUFFERS_dataType,
+ SPA_POD_CHOICE_FLAGS_Int(buffer_types))));
+ params.push_back(reinterpret_cast<spa_pod*>(spa_pod_builder_add_object(
+ &builder, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, SPA_PARAM_META_type,
+ SPA_POD_Id(SPA_META_Header), SPA_PARAM_META_size,
+ SPA_POD_Int(sizeof(struct spa_meta_header)))));
+
+ pw_stream_update_params(that->pw_stream_, params.data(), params.size());
+}
+
+// static
+void FakeScreenCastStream::OnStreamAddBuffer(void* data, pw_buffer* buffer) {
+ FakeScreenCastStream* that = static_cast<FakeScreenCastStream*>(data);
+ RTC_DCHECK(that);
+
+ struct spa_data* spa_data = buffer->buffer->datas;
+
+ spa_data->mapoffset = 0;
+ spa_data->flags = SPA_DATA_FLAG_READWRITE;
+
+ if (!(spa_data[0].type & (1 << SPA_DATA_MemFd))) {
+ RTC_LOG(LS_ERROR)
+ << "PipeWire test: Client doesn't support memfd buffer data type";
+ return;
+ }
+
+ const int stride = SPA_ROUND_UP_N(that->width_ * kBytesPerPixel, 4);
+ spa_data->maxsize = stride * that->height_;
+ spa_data->type = SPA_DATA_MemFd;
+ spa_data->fd =
+ memfd_create("pipewire-test-memfd", MFD_CLOEXEC | MFD_ALLOW_SEALING);
+ if (spa_data->fd == -1) {
+ RTC_LOG(LS_ERROR) << "PipeWire test: Can't create memfd";
+ return;
+ }
+
+ spa_data->mapoffset = 0;
+
+ if (ftruncate(spa_data->fd, spa_data->maxsize) < 0) {
+ RTC_LOG(LS_ERROR) << "PipeWire test: Can't truncate to"
+ << spa_data->maxsize;
+ return;
+ }
+
+ unsigned int seals = F_SEAL_GROW | F_SEAL_SHRINK | F_SEAL_SEAL;
+ if (fcntl(spa_data->fd, F_ADD_SEALS, seals) == -1) {
+ RTC_LOG(LS_ERROR) << "PipeWire test: Failed to add seals";
+ }
+
+ spa_data->data = mmap(nullptr, spa_data->maxsize, PROT_READ | PROT_WRITE,
+ MAP_SHARED, spa_data->fd, spa_data->mapoffset);
+ if (spa_data->data == MAP_FAILED) {
+ RTC_LOG(LS_ERROR) << "PipeWire test: Failed to mmap memory";
+ } else {
+ RTC_LOG(LS_INFO) << "PipeWire test: Memfd created successfully: "
+ << spa_data->data << spa_data->maxsize;
+ }
+}
+
+// static
+void FakeScreenCastStream::OnStreamRemoveBuffer(void* data, pw_buffer* buffer) {
+ FakeScreenCastStream* that = static_cast<FakeScreenCastStream*>(data);
+ RTC_DCHECK(that);
+
+ struct spa_buffer* spa_buffer = buffer->buffer;
+ struct spa_data* spa_data = spa_buffer->datas;
+ if (spa_data && spa_data->type == SPA_DATA_MemFd) {
+ munmap(spa_data->data, spa_data->maxsize);
+ close(spa_data->fd);
+ }
+}
+
+uint32_t FakeScreenCastStream::PipeWireNodeId() {
+ return pw_node_id_;
+}
+
+} // namespace webrtc
diff --git a/modules/desktop_capture/linux/wayland/test/fake_screencast_stream.h b/modules/desktop_capture/linux/wayland/test/fake_screencast_stream.h
new file mode 100644
index 0000000..1b3bb06
--- /dev/null
+++ b/modules/desktop_capture/linux/wayland/test/fake_screencast_stream.h
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2022 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.
+ */
+
+#ifndef MODULES_DESKTOP_CAPTURE_LINUX_WAYLAND_TEST_FAKE_SCREENCAST_STREAM_H_
+#define MODULES_DESKTOP_CAPTURE_LINUX_WAYLAND_TEST_FAKE_SCREENCAST_STREAM_H_
+
+#include <pipewire/pipewire.h>
+#include <spa/param/video/format-utils.h>
+
+#include "modules/desktop_capture/linux/wayland/screencast_stream_utils.h"
+#include "modules/desktop_capture/rgba_color.h"
+#include "rtc_base/random.h"
+
+namespace webrtc {
+
+class FakeScreenCastStream {
+ public:
+ class Observer {
+ public:
+ virtual void OnFrameRecorded() = 0;
+ virtual void OnStreamReady(uint32_t stream_node_id) = 0;
+ virtual void OnStartStreaming() = 0;
+ virtual void OnStopStreaming() = 0;
+
+ protected:
+ Observer() = default;
+ virtual ~Observer() = default;
+ };
+
+ explicit FakeScreenCastStream(Observer* observer,
+ uint32_t width,
+ uint32_t height);
+ ~FakeScreenCastStream();
+
+ uint32_t PipeWireNodeId();
+
+ void RecordFrame(RgbaColor rgba_color);
+ void StartStreaming();
+ void StopStreaming();
+
+ private:
+ Observer* observer_;
+
+ // Resolution parameters.
+ uint32_t width_ = 0;
+ uint32_t height_ = 0;
+
+ bool is_streaming_ = false;
+ uint32_t pw_node_id_ = 0;
+
+ // PipeWire types
+ struct pw_context* pw_context_ = nullptr;
+ struct pw_core* pw_core_ = nullptr;
+ struct pw_stream* pw_stream_ = nullptr;
+ struct pw_thread_loop* pw_main_loop_ = nullptr;
+
+ spa_hook spa_core_listener_;
+ spa_hook spa_stream_listener_;
+
+ // event handlers
+ pw_core_events pw_core_events_ = {};
+ pw_stream_events pw_stream_events_ = {};
+
+ struct spa_video_info_raw spa_video_format_;
+
+ // PipeWire callbacks
+ static void OnCoreError(void* data,
+ uint32_t id,
+ int seq,
+ int res,
+ const char* message);
+ static void OnStreamAddBuffer(void* data, pw_buffer* buffer);
+ static void OnStreamRemoveBuffer(void* data, pw_buffer* buffer);
+ static void OnStreamParamChanged(void* data,
+ uint32_t id,
+ const struct spa_pod* format);
+ static void OnStreamStateChanged(void* data,
+ pw_stream_state old_state,
+ pw_stream_state state,
+ const char* error_message);
+};
+
+} // namespace webrtc
+
+#endif // MODULES_DESKTOP_CAPTURE_LINUX_WAYLAND_TEST_FAKE_SCREENCAST_STREAM_H_
diff --git a/modules/desktop_capture/linux/wayland/test/shared_screencast_stream_test.py b/modules/desktop_capture/linux/wayland/test/shared_screencast_stream_test.py
new file mode 100644
index 0000000..39292c8
--- /dev/null
+++ b/modules/desktop_capture/linux/wayland/test/shared_screencast_stream_test.py
@@ -0,0 +1,106 @@
+#!/usr/bin/env vpython3
+# Copyright (c) 2022 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.
+"""
+This script is the wrapper that runs the "shared_screencast_screen" test.
+"""
+
+import argparse
+import json
+import os
+import subprocess
+import sys
+
+SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
+# Get rid of "modules/desktop_capture/linux/wayland/test"
+ROOT_DIR = os.path.normpath(
+ os.path.join(SCRIPT_DIR, os.pardir, os.pardir, os.pardir, os.pardir,
+ os.pardir))
+
+
+def _ParseArgs():
+ parser = argparse.ArgumentParser(
+ description='Run shared_screencast_screen test.')
+ parser.add_argument('build_dir',
+ help='Path to the build directory (e.g. out/Release).')
+ # Unused args
+ # We just need to avoid passing these to the test
+ parser.add_argument(
+ '--isolated-script-test-perf-output',
+ default=None,
+ help='Path to store perf results in histogram proto format.')
+ parser.add_argument(
+ '--isolated-script-test-output',
+ default=None,
+ help='Path to output JSON file which Chromium infra requires.')
+
+ return parser.parse_known_args()
+
+
+def _GetPipeWireDir():
+ pipewire_dir = os.path.join(ROOT_DIR, 'third_party', 'pipewire',
+ 'linux-amd64')
+
+ if not os.path.isdir(pipewire_dir):
+ pipewire_dir = None
+
+ return pipewire_dir
+
+
+def _ConfigurePipeWirePaths(path):
+ library_dir = os.path.join(path, 'lib64')
+ pipewire_binary_dir = os.path.join(path, 'bin')
+ pipewire_config_prefix = os.path.join(path, 'share', 'pipewire')
+ pipewire_module_dir = os.path.join(library_dir, 'pipewire-0.3')
+ spa_plugin_dir = os.path.join(library_dir, 'spa-0.2')
+ media_session_config_dir = os.path.join(pipewire_config_prefix,
+ 'media-session.d')
+
+ env_vars = os.environ
+ env_vars['LD_LIBRARY_PATH'] = library_dir
+ env_vars['PIPEWIRE_CONFIG_PREFIX'] = pipewire_config_prefix
+ env_vars['PIPEWIRE_MODULE_DIR'] = pipewire_module_dir
+ env_vars['SPA_PLUGIN_DIR'] = spa_plugin_dir
+ env_vars['MEDIA_SESSION_CONFIG_DIR'] = media_session_config_dir
+ env_vars['PIPEWIRE_RUNTIME_DIR'] = '/tmp'
+ env_vars['PATH'] = env_vars['PATH'] + ':' + pipewire_binary_dir
+
+
+def main():
+ args, extra_args = _ParseArgs()
+
+ pipewire_dir = _GetPipeWireDir()
+
+ if pipewire_dir is None:
+ return 1
+
+ _ConfigurePipeWirePaths(pipewire_dir)
+
+ pipewire_process = subprocess.Popen(["pipewire"], stdout=None)
+ pipewire_media_session_process = subprocess.Popen(["pipewire-media-session"],
+ stdout=None)
+
+ test_command = os.path.join(args.build_dir, 'shared_screencast_stream_test')
+ pipewire_test_process = subprocess.run([test_command] + extra_args,
+ stdout=True,
+ check=False)
+
+ return_value = pipewire_test_process.returncode
+
+ pipewire_media_session_process.terminate()
+ pipewire_process.terminate()
+
+ if args.isolated_script_test_output:
+ with open(args.isolated_script_test_output, 'w') as f:
+ json.dump({"version": 3}, f)
+
+ return return_value
+
+
+if __name__ == '__main__':
+ sys.exit(main())