Capture portals, WebRTC, and media security

Analyze capture permissions, media transport and privacy boundaries.

15 min readChapter 144 of 168Intermediate → advanced
In this chapter

Before you start#

Read portals and accessibility, PipeWire buffers, and media pipelines. This chapter uses xdg-desktop-portal 1.18.4 and a separately pinned WebRTC source commit. The exercise models state and a private pipe; it never contacts a portal, browser, screen, microphone, camera or network service.

Learning goals#

Trace a portal capture request into an fd-scoped PipeWire connection and a conditional video-processing path. Separate user consent, buffer ownership, media timing, network connectivity and endpoint authentication. Handle cancellation and late callbacks without leaking descriptors or exposing data.

Source walkthrough#

Capture authority and transport
  1. Portal request

    User-mediated session and source selection

  2. PipeWire remote

    Authorized node and buffer lifetimes

  3. Video pipeline

    Validate, convert and encode frames

  4. Network transport

    ICE connectivity and authenticated encryption depend on signaling

Conceptual model. Conditional stages, not a screen capture or network session.

Desktop capture crosses several authority and lifetime boundaries. A requesting application may be sandboxed and lack direct screen access. The ScreenCast portal offers an asynchronous request and session interface that delegates source selection to a desktop backend under its policy. A user-mediated choice can authorize a particular session and source, but it is not a universal grant to inspect all windows, control input, access a camera or keep capturing indefinitely. An unsandboxed process can have other operating-system privileges; the portal alone is not a global restriction on every process.

A common portal sequence is CreateSession, SelectSources, Start, then OpenPipeWireRemote, subject to the actual interface version and backend (ScreenCast API). Each asynchronous method has arguments, a request object and a response or error. A session object persists across several requests and has its own state and close behavior. A client should subscribe to the predicted request response before issuing a call when using a handle token, then reconcile the actual handle and validate the result. The method reply that returns a request handle is not the user's final approval; completion is reported through the Request interface response signal (Request API). See xdg-desktop-portal 1.18.4 ScreenCast XML and frontend implementation.

The diagram below orders the ScreenCast calls and the separate Request response. It is a teaching model of the method order, not a record of an approval or of a session.

ScreenCast method order
ScreenCast method order1. Client app to Portal frontend: CreateSession with handle token. 2. Client app to Portal frontend: SelectSources for chosen source types. 3. Portal frontend to Desktop backend: Forward selection to the backend. 4. Desktop backend to Client app (return): Request response after user decision. 5. Client app to Portal frontend: Start the session. 6. Portal frontend to Client app (return): Stream information on success. 7. Client app to Portal frontend: OpenPipeWireRemote. 8. Portal frontend to PipeWire remote: Open remote for the session streams. 9. Portal frontend to Client app (return): Descriptor in D-Bus fd list.Client appPortal frontendDesktop backendPipeWire remoteCreateSession withhandle tokenSelectSources forchosen source typesForward selection tothe backendRequest response after user decisionStart the sessionStream informationon successOpenPipeWireRemoteOpen remote for the session streamsDescriptor in D-Busfd list
  1. Client appPortal frontend

    CreateSession with handle token

  2. Client appPortal frontend

    SelectSources for chosen source types

  3. Portal frontendDesktop backend

    Forward selection to the backend

  4. Desktop backendClient app

    Return. Request response after user decision

  5. Client appPortal frontend

    Start the session

  6. Portal frontendClient app

    Return. Stream information on success

  7. Client appPortal frontend

    OpenPipeWireRemote

  8. Portal frontendPipeWire remote

    Open remote for the session streams

  9. Portal frontendClient app

    Return. Descriptor in D-Bus fd list

Time runs downward. Dashed arrows show returns.

Read the text version
  1. Client app → Portal frontend: CreateSession with handle token
  2. Client app → Portal frontend: SelectSources for chosen source types
  3. Portal frontend → Desktop backend: Forward selection to the backend
  4. Desktop backend → Client app (return): Request response after user decision
  5. Client app → Portal frontend: Start the session
  6. Portal frontend → Client app (return): Stream information on success
  7. Client app → Portal frontend: OpenPipeWireRemote
  8. Portal frontend → PipeWire remote: Open remote for the session streams
  9. Portal frontend → Client app (return): Descriptor in D-Bus fd list
Conceptual model. Teaching model of xdg-desktop-portal 1.18.4 method order; the user choice and backend policy are not observed, so this is not a captured trace.

A token is a correlation value used by the request-path convention, not a general authorization credential. The backend can reject a request, the user can cancel, the session can close, and the bus can disconnect. A robust client keeps one terminal-state rule for each request, unsubscribes and frees callback data on every terminal path, and handles a response that arrives while local cancellation is underway. The parent-window identifier helps the desktop associate an interaction with a window where supported; it does not itself prove consent.

Session validation and remote fd

From xdg-desktop-portal 1.18.4 src/screen-cast.c, handle_start, LGPL version 2 or later as stated in the file; request locking and later state checks are omitted:

c
  session = acquire_session (arg_session_handle, request);
  if (!session)
    {
      g_dbus_method_invocation_return_error (invocation,
                                             G_DBUS_ERROR,
                                             G_DBUS_ERROR_ACCESS_DENIED,
                                             "Invalid session");

The first statement attempts to acquire the session associated with the supplied handle and request. The condition handles failure, and the following call begins returning an access-denied error for an invalid session. The closing arguments and return in the source complete that error path; the handler does not advance to a valid start state when acquisition fails. The surrounding code locks the request, holds and later unreferences the session, and checks the session state before proceeding. A guessed path or stale handle is not a substitute for an authorized live session.

After a successful start response, the portal can provide stream information including node identifiers. OpenPipeWireRemote supplies a Unix fd for a PipeWire remote under the interface's rules. That fd is an authority-bearing reference to a connection endpoint, not a screenshot and not a globally reusable node identifier. A node ID is meaningful in the server/connection and permission context that supplied it. A later server restart or session closure can invalidate it, and a different client might not have permission to bind the same ID. Closing one fd does not close a duplicate held elsewhere; each owner must close its own reference.

In src/screen-cast.c, handle_open_pipewire_remote in xdg-desktop-portal 1.18.4, LGPL version 2 or later as stated in the file (the session checks and the start of the handler are omitted):

c
  out_fd_list = g_unix_fd_list_new ();
  fd = pw_core_steal_fd (remote->core);
  fd_id = g_unix_fd_list_append (out_fd_list, fd, &error);
  close (fd);
  pipewire_remote_destroy (remote);

  if (fd_id == -1)
    {
      g_dbus_method_invocation_return_error (invocation,
                                             G_DBUS_ERROR,
                                             G_DBUS_ERROR_ACCESS_DENIED,
                                             "Failed to append fd: %s",
                                             error->message);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  xdp_dbus_screen_cast_complete_open_pipewire_remote (object, invocation,
                                                      out_fd_list,
                                                      g_variant_new_handle (fd_id));

The portal takes ownership of the PipeWire core's descriptor, appends it to a GLib fd list that travels with the D-Bus reply, and closes its own copy. The reply carries a handle index rather than the integer. GLib's g_unix_fd_list_append documents that it duplicates the descriptor, so the list keeps its own reference; this excerpt does not show what the client does with the handle.

A client that receives the remote fd must follow the specific PipeWire connection API's ownership contract. It should close or transfer the fd exactly once, destroy proxies and streams when finished, and avoid closing a descriptor that another thread still uses without coordination. A successful remote open does not prove that a video frame has arrived. If connection or mapping fails, release the request, session, fd and partial stream objects that were actually acquired. RemoteDesktop is a separate portal interface for input-related authority; screen capture approval should not be treated as permission to inject keyboard or pointer events. This chapter never requests that authority.

Buffer processing in WebRTC

A capture node can deliver PipeWire buffers with format and layout negotiated at runtime. A video buffer can have multiple planes, offsets, strides and crop metadata. Its dimensions and valid region must be checked before reading or converting pixels. Memory may be mapped CPU memory, memfd-backed memory or DMA-BUF where the implementation and platform support it. DMA-BUF import, modifiers and fences have device-specific requirements; a fallback copy may be needed, and neither zero-copy nor synchronization support is universal. A pointer into a buffer cannot be retained after the buffer is returned to PipeWire unless the implementation has made an appropriately owned copy or reference under the relevant API.

PipeWire documents that capture buffers are returned for reuse through pw_stream_queue_buffer() and that stream-owned buffers remain valid according to the stream lifecycle (stream API, streams guide). The following is from WebRTC commit 3710345e71ad3a7c1f4ed4efef152fd4fc24869a, modules/desktop_capture/linux/wayland/shared_screencast_stream.cc, OnStreamProcess. The file states a BSD-style project license; earlier metadata lookup and later processing are omitted:

c++
    // Queue buffer for reuse; it will not be processed further.
    pw_stream_queue_buffer(that->pw_stream_, buffer);
    return;

The comment explains this particular branch: corrupted metadata caused the buffer to be dropped. The next statement returns the buffer to the stream, and the final statement exits before normal processing. The surrounding function dequeues available buffers, returns older ones when a newer buffer is present, checks header metadata, calls ProcessBuffer for the retained non-corrupt buffer, and queues that buffer afterward. That is a specific implementation's strategy, not a universal guarantee that all capture clients discard intermediate frames or use the same thread.

In src/pipewire/stream.c, PipeWire 1.2.0, MIT per its SPDX header (the rest of the function is omitted):

c
int pw_stream_queue_buffer(struct pw_stream *stream, struct pw_buffer *buffer)
{
	struct stream *impl = SPA_CONTAINER_OF(stream, struct stream, this);
	struct buffer *b = SPA_CONTAINER_OF(buffer, struct buffer, this);
	int res;

	if (b->busy)
		SPA_ATOMIC_DEC(b->busy->count);

	pw_log_trace_fp("%p: queue buffer %d size:%"PRIu64, stream, b->id,
			b->this.size);
	if ((res = queue_push(impl, &impl->queued, b)) < 0)
		return res;

The function decrements a busy counter attached to the buffer and pushes the buffer onto the stream's queued list. The omitted lines include the trigger path for output streams, so this excerpt says nothing about when the graph reuses the buffer. The chapter's rule that a pointer is not used after queueing is consistent with this push, but the excerpt does not prove that every consumer follows it.

A callback can be running while another thread requests session shutdown. Destroying the stream or unmapping a buffer while the callback still accesses it can cause use-after-free. A correct client stops new processing, coordinates the PipeWire loop and callback lifetime using its API, returns or retires buffers appropriately, then releases mappings, fds, stream and session resources. A late callback must not access a closed UI object or enqueue a frame into a destroyed encoder. Ownership of an application frame copy is separate from ownership of the PipeWire buffer from which it was produced.

A cancellation and late-buffer model

Consider a synthetic request R with token capture42. The client subscribes for its response and starts a session request. Before a response arrives, the user closes the client window. The client marks R cancelled, requests close where the object exists, and stops accepting new work. If a late response reports denial, the callback releases its subscription and request state. If a late response includes a resource that the client no longer needs, the client follows the interface's ownership rules to close or release that resource rather than beginning capture. Cancellation is not proof that callbacks cannot still run.

The four states below model one synthetic request R. They are the client's own bookkeeping, not messages from the portal.

Cancellation and late responses
  1. Pending

    Subscribed to the response signal; no final result yet.

  2. Cancelled locally

    The UI closed; new work is refused and close is requested where possible.

  3. Late response

    Its arrival does not restart capture; the state check decides.

  4. Release

    Close any descriptor or session the response produced, then unsubscribe.

Conceptual model. Teaching model for one synthetic request R, not a trace of xdg-desktop-portal 1.18.4; a late response is judged by state, not by arrival.

Now suppose a different request was approved and the client obtained remote fd F, a PipeWire connection and stream S. A process callback dequeues buffer B just as shutdown begins. The client must coordinate so the callback either completes its bounded processing and returns B or follows the stream's documented teardown path; the control thread must not close F and free S while the callback still uses them. After the data path stops, destroy S and its proxies, close any fd references still owned by the client, and close the portal session under the applicable interface. If an encoder queue already contains an owned frame copy, its consumer must release that copy separately. This is a teaching sequence, not a recorded WebRTC execution.

From capture frame to network packet

A captured frame can be represented by a WebRTC VideoFrame or another internal frame object with dimensions, timestamp and a reference to pixel storage. Conversion or scaling can create a new owned buffer; hardware encoding may use a platform-specific representation. The encoder produces encoded frames, and an RTP packetizer divides media into packets appropriate for the selected codec and transport. RTP timestamps and sequence numbers support media timing and loss handling. RTCP provides feedback and control information, potentially including reports that help estimate quality or synchronize streams. Exact codecs, implementations and features vary by build and negotiation.

A bounded queue between capture and encoder prevents unbounded memory growth when encoding is slower than capture. A real-time video application might discard an old frame to reduce latency, while another application might choose different behavior. Dropping must release the frame reference and preserve valid timestamps for later frames. If a frame is retained for an asynchronous encoder, its backing memory must remain alive until the encoder releases it. Returning a PipeWire buffer early is safe only when the encoder uses an independent copy or a properly synchronized reference with an appropriate lifetime.

RTP clock rates are selected by payload format (see RFC 3551); an illustrative audio/video timing model can use a 48 kHz audio clock and a 90 kHz video RTP clock. At 0.1 second, their nominal timestamp increments are 4,800 and 9,000 respectively; comparing those raw integers would be meaningless. Mapping clocks requires time bases, a shared or correlated time reference, and drift handling. Capture and network scheduling can vary, so a nominally aligned pair can later diverge. Jitter buffers, resampling or clock adaptation may be involved. This arithmetic is synthetic and does not measure any host device or network.

ICE is a framework for finding usable network paths between peers. STUN can help discover address information or check connectivity, and TURN can provide a relay when direct paths are unsuitable. Their availability and behavior depend on configuration and network policy. A successful ICE connectivity check establishes that a path can carry traffic under the protocol; it does not by itself establish the human identity or trustworthiness of the remote participant. This exercise contacts no ICE, STUN or TURN server.

WebRTC uses DTLS-SRTP keying and SRTP to protect media transport (RFC 8827). ICE connectivity checks establish a usable path, not a person’s identity (RFC 8445). A DTLS fingerprint binds the transport to a key; identity assurance depends on how signaling conveys/authenticates that fingerprint or on independent verification (RFC 8826). Authentication relies on the negotiated fingerprints, signaling and application design; encryption in transit does not prove that the person receiving the stream is the intended person. A malicious or compromised endpoint can see content after decryption, and a signaling channel can misrepresent identity if the application does not authenticate it appropriately. User consent, source selection, endpoint identity, retention and revocation remain separate questions from cryptographic transport protection.

A browser's own camera or screen permission interface is also separate from the desktop portal's decision. A browser can deny access before a portal request, a portal can deny or limit a request that the browser accepted, and enterprise or desktop policy can add other constraints. An application must handle every denial without trying to bypass it. Permission can be revoked while a session is active; stop capture, notify the user under the application policy, and release stream and network resources. A green “encrypted” indicator would not establish that the selected window or participant is correct.

Error propagation and teardown

A failure can occur at request creation, source selection, session start, remote connection, format negotiation, buffer mapping, frame conversion, encoder submission or network transport. Each layer owns different objects and reports different errors. A portal response code cannot diagnose an encoder failure, and a network disconnect does not automatically close every local fd or stop a pending capture callback. Track a state machine with explicit ownership: request subscription, session handle, remote fd, core, stream, mapped buffers, frame queue, encoder and network connection. On shutdown, stop producing frames, coordinate callbacks, drain or discard queued frames under policy, then release each layer in the order its API requires.

Consider a buffer with a declared stride smaller than the bytes required for a row in the negotiated format, or an offset and height that exceed the mapped region. The application must reject or safely handle the buffer rather than trust the metadata and read beyond the allocation. Integer overflow in size calculations can turn a superficially small bound into an unsafe access. A corrupted or unsupported buffer can be returned or discarded under the stream contract, as the quoted WebRTC branch illustrates for corrupted header metadata. The example does not claim that this one branch validates every format, modifier or plane.

A process receiving a file descriptor may duplicate it for another worker. The duplicate is another reference to the same underlying kernel object, not a deep copy of its data. If the original owner closes its fd, the worker's duplicate can remain valid until it closes it, subject to remote shutdown and protocol state. Conversely, passing only the numeric integer through an ordinary in-process queue while closing the descriptor can leave the worker with a number that now refers to something else. Transfer explicit ownership or duplicate the descriptor deliberately and document which thread closes which reference.

Practice#

This private model covers cancellation and clock arithmetic. It does not invoke a portal, capture data or send network packets:

sh
python3 - <<'PY'
from fractions import Fraction
state = 'pending'
state = 'cancelled'
late_response = 'approved'
if state != 'pending':
    print('late response ignored:', late_response)
audio = Fraction(4800, 48000)
video = Fraction(9000, 90000)
print('same synthetic time:', audio == video, audio)
assert state == 'cancelled' and audio == video
PY

Expected output says the late approval is ignored and both values are 1/10 second. This toy model has no actual returned fd; a real late response that transfers an owned resource needs explicit cleanup. As a source-only alternative, use the R/F/S/B sequence above and draw an ownership table for request, session, remote fd, stream, dequeued buffer and frame copy. For the failure case, show how an invalid session is rejected by the quoted portal branch and why a callback cannot continue using a freed stream. No actual permission prompt or capture is required.

Knowledge check#

  1. Does a ScreenCast request handle mean that a user approved capture?
  2. What does the portal's invalid-session branch do?
  3. Why must a late successful response still have a cleanup path after cancellation?
  4. What does the WebRTC corrupted-buffer branch do before returning?
  5. Why do ICE connectivity and encrypted media not prove the remote person's identity?

Answers#

  1. No. It identifies an asynchronous request; a later response and session state determine the outcome.
  2. It returns an access-denied error for the invalid session instead of starting the capture session.
  3. The response may carry resources or arrive after local UI teardown. Their owners must release them, and the client must not restart cancelled capture accidentally.
  4. It queues the buffer for reuse and returns without normal processing in that branch.
  5. Connectivity and transport encryption do not independently authenticate the application's human participant or guarantee trustworthy signaling and endpoints.

Checkpoint#

Submit the request/session/fd/buffer ownership timeline and the two-time-base calculation. Pass when cancellation does not imply callback disappearance, the returned buffer is not used after queueing, and consent and identity are separate from transport encryption. If not, trace the two source excerpts and retry the synthetic model. This is practice, not certification.

Sources#

Read portal ScreenCast implementation, portal ScreenCast interface, WebRTC PipeWire capture implementation, and PipeWire stream implementation. The timeline and timing values are synthetic; no capture or network connection was performed.

A little further beneath the surface.

Check your understanding with the checkpoint. Marking a chapter as read saves a bookmark, not a proficiency assessment.