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())