diff --git a/BUILD.gn b/BUILD.gn index 3caa4c50cc..f962484154 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -106,6 +106,9 @@ if (!build_with_chromium) { "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 6cd918e767..0ae460afe2 100644 --- a/DEPS +++ b/DEPS @@ -490,6 +490,21 @@ deps = { ], '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 4f017933bf..15a55cc10f 100644 --- a/infra/specs/client.webrtc.json +++ b/infra/specs/client.webrtc.json @@ -3324,6 +3324,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": { @@ -4623,6 +4649,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": { @@ -5056,6 +5108,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": { @@ -6358,6 +6436,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": { @@ -6792,6 +6896,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": { diff --git a/infra/specs/gn_isolate_map.pyl b/infra/specs/gn_isolate_map.pyl index d35a46c421..598b15fac6 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 d4b8cf9811..f207c3da4f 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 6db19d0da4..666bbc7ec3 100644 --- a/infra/specs/tryserver.webrtc.json +++ b/infra/specs/tryserver.webrtc.json @@ -5941,6 +5941,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": { @@ -6380,6 +6406,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": { @@ -6814,6 +6866,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": { @@ -7706,6 +7784,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": { @@ -8621,6 +8725,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": { @@ -9054,6 +9184,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": { diff --git a/infra/specs/waterfalls.pyl b/infra/specs/waterfalls.pyl index bad6afa01f..bf7ea4c5a3 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 dd5b37b218..db70df2fc7 100644 --- a/modules/desktop_capture/BUILD.gn +++ b/modules/desktop_capture/BUILD.gn @@ -95,6 +95,66 @@ if (rtc_include_tests) { } } + 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 06a97b8f29..139a8c37a5 100644 --- a/modules/desktop_capture/linux/wayland/pipewire.sigs +++ b/modules/desktop_capture/linux/wayland/pipewire.sigs @@ -31,6 +31,8 @@ pw_stream * pw_stream_new(pw_core *core, const char *name, pw_properties *props) 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 48a15c1942..aacf68ef99 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 @@ class SharedScreenCastStreamPrivate { 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 CaptureFrame(); + std::unique_ptr CaptureFrame(); std::unique_ptr CaptureCursor(); DesktopVector CaptureCursorPosition(); @@ -99,6 +100,7 @@ class SharedScreenCastStreamPrivate { // 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 @@ void SharedScreenCastStreamPrivate::StopAndCleanupStream() { pw_main_loop_ = nullptr; } -std::unique_ptr SharedScreenCastStreamPrivate::CaptureFrame() { +std::unique_ptr +SharedScreenCastStreamPrivate::CaptureFrame() { webrtc::MutexLock lock(&queue_lock_); if (!pw_stream_ || !queue_.current_frame()) { - return std::unique_ptr{}; + return std::unique_ptr{}; } - std::unique_ptr frame = queue_.current_frame()->Share(); - return std::move(frame); + return queue_.current_frame()->Share(); } std::unique_ptr SharedScreenCastStreamPrivate::CaptureCursor() { @@ -628,8 +630,18 @@ void SharedScreenCastStreamPrivate::ProcessBuffer(pw_buffer* buffer) { DesktopRect::MakeWH(bitmap->size.width, bitmap->size.height)); mouse_cursor_ = std::make_unique( 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 @@ void SharedScreenCastStreamPrivate::ProcessBuffer(pw_buffer* buffer) { } if (!src) { + // For testing purpose + if (observer_) { + observer_->OnFailedToProcessBuffer(); + } return; } @@ -730,6 +746,11 @@ void SharedScreenCastStreamPrivate::ProcessBuffer(pw_buffer* buffer) { videocrop_metadata->region.size.height > static_cast(stream_size_.height()))) { RTC_LOG(LS_ERROR) << "Stream metadata sizes are wrong!"; + + if (observer_) { + observer_->OnFailedToProcessBuffer(); + } + return; } @@ -795,6 +816,10 @@ void SharedScreenCastStreamPrivate::ProcessBuffer(pw_buffer* buffer) { 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 @@ void SharedScreenCastStreamPrivate::ProcessBuffer(pw_buffer* buffer) { 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 @@ void SharedScreenCastStream::UpdateScreenCastStreamResolution(uint32_t width, private_->UpdateScreenCastStreamResolution(width, height); } +void SharedScreenCastStream::SetObserver( + SharedScreenCastStream::Observer* observer) { + private_->SetObserver(observer); +} + void SharedScreenCastStream::StopScreenCastStream() { private_->StopScreenCastStream(); } -std::unique_ptr SharedScreenCastStream::CaptureFrame() { +std::unique_ptr 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 66a3f45bdb..c58d8402ac 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 SharedScreenCastStreamPrivate; class RTC_EXPORT SharedScreenCastStream : public rtc::RefCountedNonVirtual { 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 CreateDefault(); bool StartScreenCastStream(uint32_t stream_node_id); @@ -35,6 +48,7 @@ class RTC_EXPORT SharedScreenCastStream 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 @@ class RTC_EXPORT SharedScreenCastStream // 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 CaptureFrame(); + std::unique_ptr 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 @@ class RTC_EXPORT SharedScreenCastStream 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 0000000000..23296f5d14 --- /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 +#include + +#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(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 fake_screencast_stream_; + rtc::scoped_refptr 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 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(red_color.ToUInt32())); + + // Test DesktopFrameQueue + RgbaColor green_color(0, 255, 0); + fake_screencast_stream_->RecordFrame(green_color); + frameRetrievedEvent.Wait(kShortWait); + std::unique_ptr 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(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 0000000000..8f973da060 --- /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 +#include +#include +#include + +#include +#include +#include + +#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 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(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(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(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(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(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 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_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_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(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(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 0000000000..1b3bb06c47 --- /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 +#include + +#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 0000000000..39292c88ea --- /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())