WebRTCService
The WebRTC streaming service run under the centralized stream server.
Owns the whole WebRTC data path for a session: signaling, peer management, one media pipeline per display, input handling, monitoring, and the congestion-control/pacer loop. Mirrors the websockets service's policies (per-display settings, extended-desktop layout, capture pause rules) so the two transports stay in behavioral parity.
Attributes
attributesettingsOptional[AppSettings]= settingsattributetasksList[asyncio.Task]= []attributeshutdown_event= asyncio.Event()attribute_shutdown_called= Falseattributesignaling_clientOptional[WebRTCSignalingClient]= Noneattributemedia_pipelineOptional[MediaPipelinePixel]= NoneThe primary display's pipeline (also stored under the
"primary" key of display_pipelines).
attributertc_appOptional[RTCApp]= Noneattributeinput_handlerOptional[WebRTCInput]= Noneattributesystem_monitorOptional[SystemMonitor]= Noneattributegpu_monitorOptional[GPUMonitor]= NoneattributemetricsOptional[Metrics]= Noneattributepeer_id= 1attributeargsOptional[SimpleNamespace]= NoneMutable per-session snapshot of the client-tunable settings
(seeded from SETTING_DEFINITIONS); the primary display's
authority, and the seed for joining secondaries.
attributemonitoring_utils_usedDict[str, bool]= {}attributemon_hmac_turnOptional[HMACRTCMonitor]= Noneattributemon_rest_apiOptional[RESTRTCMonitor]= Noneattributemon_rtc_config_fileOptional[RTCConfigFileMonitor]= Noneattributemon_cloudflare_turnOptional[CloudflareRTCMonitor]= Noneattributepeer_managerOptional[WebRTCPeerManagement]= Noneattributesupervisor= supervisorattributedisplay_clientsDict[str, Dict[str, Any]]= {}Per-secondary-display registration state — requested dimensions, position, and the display's own copies of the client-tunable video settings.
attributedisplay_layoutsDict[str, Dict[str, int]]= {}The computed extended-desktop layout the input handler offsets coordinates against.
attributedisplay_pipelinesDict[str, MediaPipelinePixel]= {}One MediaPipelinePixel per connected display id.
attribute_last_idr_request_timesDict[str, float]= {}attribute_display_lock= asyncio.Lock()attribute_primary_dimsOptional[Tuple[int, int]]= NoneThe primary's last layout-path resolution request, or None while the pipeline dimensions are its authority.
attribute_wayland_ctl_moduleOptional[Any]= NoneFallback pixelflux handle for Wayland output management when the primary has no live capture module (any handle reaches the shared compositor backend).
attribute_host_output_capacityOptional[int]= NoneHost-capture mode: outputs the host compositor can back displays with; None until a query answers, never set when self-compositing (outputs are minted on demand there).
attribute_last_resize_requestOptional[Tuple[int, int]]= NoneLast (w, h) a client asked the primary to
become; the realized size may differ (CVT cell alignment), so
idempotence is judged against the request too.
attribute_wm_swap= MultiMonitorWindowManager()Swaps heavy DEs, which tile poorly across the per-display regions, for a minimal Openbox once a secondary joins.
attributeRECONNECT_GRACE_S= 3.0Seconds the primary capture outlives its last consumer, so a controller tab reload (drop and re-add within a second or two) reuses the warm capture while viewers and the secondary display stream through.
attribute_primary_stop_grace_taskOptional[asyncio.Task]= NoneThe pending deferred primary-capture stop.
attribute_mic_controlOptional[AudioControl]= NoneSound-server control connection for the shared SelkiesVirtualMic, provisioned once on the first mic packet (the data plane is per-peer pcmflux playback into the input sink).
attribute_mic_module_indexOptional[int]= NoneLoaded virtual-source module, reused when the websockets path already loaded it.
attribute_mic_module_owned= FalseWhether this path loaded the module and so unloads it on shutdown.
attribute_mic_provisioned= FalseSet once _mic_module_index is known.
attribute_mic_provision_lock= asyncio.Lock()Serializes concurrent first-packet provisioning across peers.
attribute_VIDEO_SETTING_APPLIERSDict[str, Callable[[MediaPipelinePixel, Any], Awaitable[Any]]]= {'rate_control_mode': lambda p, v: p.update_rate_control_mode(RateControlMode(v)), 'video_crf': lambda p, v: p.set_crf(v), 'video_bitrate': lambda p, v: p.set_video_bitrate(v), 'framerate': lambda p, v: p.set_framerate(v), 'use_cpu': lambda p, v: p.set_use_cpu(bool(v)), 'encoder': lambda p, v: p.set_encoder(str(v)), 'video_fullcolor': lambda p, v: p.set_video_fullcolor(bool(v)), 'video_streaming_mode': lambda p, v: p.set_video_streaming_mode(bool(v)), 'use_paint_over_quality': lambda p, v: p.set_use_paint_over_quality(bool(v)), 'video_paintover_crf': lambda p, v: p.set_video_paintover_crf(int(v)), 'video_paintover_burst_frames': lambda p, v: p.set_video_paintover_burst_frames(int(v))}Setting name to live per-pipeline setter; each display's pipeline owns its running values.
Functions
func__init__(self, supervisor) -> NoneSource Code
def __init__(self, supervisor: CentralizedStreamServer) -> None:
super().__init__("webrtc")
self.settings: Optional[AppSettings] = settings
self.tasks: List[asyncio.Task] = []
self.shutdown_event = asyncio.Event()
self._shutdown_called = False
self.signaling_client: Optional[WebRTCSignalingClient] = None
self.media_pipeline: Optional[MediaPipelinePixel] = None
self.rtc_app: Optional[RTCApp] = None
self.input_handler: Optional[WebRTCInput] = None
self.system_monitor: Optional[SystemMonitor] = None
self.gpu_monitor: Optional[GPUMonitor] = None
self.metrics: Optional[Metrics] = None
self.peer_id = 1
self.args: Optional[SimpleNamespace] = None
self.monitoring_utils_used: Dict[str, bool] = {}
self.mon_hmac_turn: Optional[HMACRTCMonitor] = None
self.mon_rest_api: Optional[RESTRTCMonitor] = None
self.mon_rtc_config_file: Optional[RTCConfigFileMonitor] = None
self.mon_cloudflare_turn: Optional[CloudflareRTCMonitor] = None
self.peer_manager: Optional[WebRTCPeerManagement] = None
self.supervisor = supervisor
self.display_clients: Dict[str, Dict[str, Any]] = {}
self.display_layouts: Dict[str, Dict[str, int]] = {}
self.display_pipelines: Dict[str, MediaPipelinePixel] = {}
self._last_idr_request_times: Dict[str, float] = {}
self._display_lock = asyncio.Lock()
self._primary_dims: Optional[Tuple[int, int]] = None
self._wayland_ctl_module: Optional[Any] = None
self._host_output_capacity: Optional[int] = None
self._last_resize_request: Optional[Tuple[int, int]] = None
self._wm_swap = MultiMonitorWindowManager()
self.RECONNECT_GRACE_S = 3.0
self._primary_stop_grace_task: Optional[asyncio.Task] = None
self._mic_control: Optional[AudioControl] = None
self._mic_module_index: Optional[int] = None
self._mic_module_owned = False
self._mic_provisioned = False
self._mic_provision_lock = asyncio.Lock()
self._init_default_settings()paramselfparamsupervisorCentralizedStreamServerReturns
Nonefunc_init_default_settings(self) -> NoneSeed self.args from SETTING_DEFINITIONS and derive the manual
startup geometry.
Range settings pin to the single allowed value when the range is locked (min == max), otherwise take the definition's default; other types copy the configured value verbatim.
Source Code
def _init_default_settings(self) -> None:
"""Seed ``self.args`` from ``SETTING_DEFINITIONS`` and derive the manual
startup geometry.
Range settings pin to the single allowed value when the range is locked
(min == max), otherwise take the definition's default; other types copy
the configured value verbatim.
"""
self.args = SimpleNamespace()
try:
for setting_def in SETTING_DEFINITIONS:
name = setting_def["name"]
stype = setting_def["type"]
if stype == "bool":
value = getattr(self.settings, name)[0]
elif stype == "range":
min, max = getattr(self.settings, name)
value = (
min
if min == max
else setting_def.get("meta", {}).get("default_value", 0)
)
elif stype == "enum":
value = getattr(self.settings, name)
elif stype in ("int", "float", "str", "list"):
value = getattr(self.settings, name)
else:
continue
setattr(self.args, name, value)
except Exception as e:
logger.error(f"Error initializing default settings: {e}", exc_info=True)
self._manual_dims: Optional[Tuple[int, int]] = None
if getattr(self.args, "manual_resolution", False):
width = int(getattr(self.args, "manual_width", 0) or 0)
height = int(getattr(self.args, "manual_height", 0) or 0)
if width > 0 and height > 0:
self._manual_dims = (width - (width % 2), height - (height % 2))paramselfReturns
Nonefuncinitialize_components(self) -> NoneBuild every component: metrics, signaling, the primary media pipeline, the RTC app, the input handler, and the monitors, then wire the peer manager with the fetched RTC configuration.
The settings are re-snapshotted first: the service is constructed once
at boot, but a live transport switch lands here with the settings
singleton already re-resolved for webrtc (encoder filter, rate-control
default). Metrics backs both the Prometheus endpoint and the WebRTC
CSV statistics, so it is built when either flag is on. A configured
manual resolution is applied before the pipeline is sized: on X11 the
screen is resized now and the pipeline takes what the X server
realized (CVT cell alignment can widen the mode); on Wayland the
dimensions are the resize, since the capture start sizes the compositor
output from them, and the capture scale is seeded from the configured
DPI so the first start honors it (handle_scaling updates it later).
The interposer socket paths and the gamepad backend are process-wide
state shared with the websockets service, so both transports read the
same settings for them.
Source Code
async def initialize_components(self) -> None:
"""Build every component: metrics, signaling, the primary media
pipeline, the RTC app, the input handler, and the monitors, then wire
the peer manager with the fetched RTC configuration.
The settings are re-snapshotted first: the service is constructed once
at boot, but a live transport switch lands here with the settings
singleton already re-resolved for webrtc (encoder filter, rate-control
default). Metrics backs both the Prometheus endpoint and the WebRTC
CSV statistics, so it is built when either flag is on. A configured
manual resolution is applied before the pipeline is sized: on X11 the
screen is resized now and the pipeline takes what the X server
realized (CVT cell alignment can widen the mode); on Wayland the
dimensions are the resize, since the capture start sizes the compositor
output from them, and the capture scale is seeded from the configured
DPI so the first start honors it (`handle_scaling` updates it later).
The interposer socket paths and the gamepad backend are process-wide
state shared with the websockets service, so both transports read the
same settings for them.
"""
self._init_default_settings()
if self.args.enable_metrics_http or self.args.enable_webrtc_statistics:
webrtc_csv = self.args.enable_webrtc_statistics
self.metrics = Metrics(using_webrtc_csv=webrtc_csv)
self.signaling_client = self.create_signaling_client()
# Surround (>2ch) rides Chromium's multiopus codec; the offered codec set
# must be swapped before any peer connection builds its capabilities.
if int(self.args.audio_channels) > 2:
configure_multiopus(int(self.args.audio_channels))
self.media_pipeline = MediaPipelinePixel(
async_event_loop=asyncio.get_running_loop(),
encoder=self.args.encoder,
framerate=int(self.args.framerate),
# kbps, as consumed by pixelflux.
video_bitrate=int(self.args.video_bitrate),
# Enum with a wider server-side value_range: an operator override can
# arrive as an arbitrary numeric string.
audio_bitrate=int(float(self.args.audio_bitrate)),
audio_channels=int(self.args.audio_channels),
audio_enabled=self.args.audio_enabled,
audio_device_name=self.args.audio_device_name,
crf=int(self.args.video_crf),
video_fullcolor=bool(self.args.video_fullcolor),
use_cpu=bool(self.args.use_cpu),
video_streaming_mode=bool(self.args.video_streaming_mode),
use_paint_over_quality=bool(self.args.use_paint_over_quality),
video_paintover_crf=int(self.args.video_paintover_crf),
video_paintover_burst_frames=int(self.args.video_paintover_burst_frames),
)
if self._manual_dims:
if not IS_WAYLAND:
realized = await resize_display(f"{self._manual_dims[0]}x{self._manual_dims[1]}")
if realized:
self._manual_dims = realized
self.media_pipeline.width, self.media_pipeline.height = self._manual_dims
if self.args.enable_rate_control:
self.media_pipeline.rc_mode = RateControlMode(self.args.rate_control_mode)
else:
# Rate control disabled runs CRF on both transports.
self.media_pipeline.rc_mode = RateControlMode.CRF
(
stun_servers,
turn_servers,
rtc_config,
self.monitoring_utils_used,
) = await get_rtc_configuration(self.args)
self.rtc_app = RTCApp(
async_event_loop=asyncio.get_running_loop(),
encoder=self.args.encoder,
stun_servers=stun_servers,
turn_servers=turn_servers,
)
self.rtc_app.media_pipeline = self.media_pipeline
self.rtc_app.provision_virtual_mic = self._provision_webrtc_virtual_mic
self.display_pipelines["primary"] = self.media_pipeline
self.input_handler = WebRTCInput(
rtc_app=self.rtc_app,
uinput_mouse_socket_path=getattr(self.args, "uinput_mouse_socket", "") or "",
js_socket_path_prefix=getattr(self.args, "js_socket_path", "/tmp"),
enable_clipboard=self.args.enable_clipboard,
enable_binary_clipboard="true"
if self.args.enable_binary_clipboard
else "false",
enable_cursors=self.args.enable_cursors,
cursor_size=self.args.cursor_size,
cursor_scale=1.0,
cursor_debug=self.args.debug_cursors,
upload_dir=self.args.file_manager_path,
is_wayland=IS_WAYLAND,
app_wayland_display=(getattr(self.args, "app_wayland_display", "")
or getattr(self.args, "wayland_host_display", "")),
uinput_gamepad=getattr(self.args, "uinput_gamepad", "auto"),
# Duck-typed layout source: send_x11_mouse offsets a secondary
# display's coordinates by display_layouts[display_id].
data_server_instance=self,
)
self.input_handler.initialize_upload_dir()
if IS_WAYLAND:
self.media_pipeline.scale = await self.input_handler.realize_wayland_dpi(
getattr(settings, "scaling_dpi", "96") or 96)
self.system_monitor = SystemMonitor()
# Always enabled: gpu_stats reports nothing without a supported GPU/tool.
# Keyed to the pipeline's render node so stats describe the encoding GPU.
stats_gpu_id = parse_gpu_id(getattr(self.args, "gpu_id", ""))
self.gpu_monitor = GPUMonitor(
gpu_id=stats_gpu_id if (stats_gpu_id or 0) > 0 else 0,
enabled=True,
dri_node=getattr(self.args, "encode_dri", "") or "",
)
self.create_peer_manager(rtc_config)paramselfReturns
Nonefunccreate_signaling_client(self) -> WebRTCSignalingClientCreate and configure signaling client.
Source Code
def create_signaling_client(self) -> WebRTCSignalingClient:
"""Create and configure signaling client."""
using_https = self.args.enable_https
using_basic_auth = self.args.enable_basic_auth
ws_protocol = "wss:" if using_https else "ws:"
prefix = self.settings.subfolder
username = self.settings.basic_auth_user
password = self.settings.basic_auth_password
client = WebRTCSignalingClient(
f"{ws_protocol}//127.0.0.1:{self.args.port}{prefix}/api/ws",
enable_https=using_https,
enable_basic_auth=using_basic_auth,
basic_auth_user=username,
basic_auth_password=password,
server_token=getattr(self.settings, "master_token", None),
)
return clientparamselfReturns
selkies.webrtc_signaling.WebRTCSignalingClientfunchandle_signaling_error(self, error) -> NoneHandle signaling errors.
Source Code
async def handle_signaling_error(self, error: Exception) -> None:
"""Handle signaling errors."""
logger.error(f"Signaling client error: {error}. Closing the pipelines")
await self.handle_signaling_disconnect()paramselfparamerrorExceptionReturns
Nonefunchandle_signaling_disconnect(self) -> NoneTear down every RTC connection once the signaling link drops.
Source Code
async def handle_signaling_disconnect(self) -> None:
"""Tear down every RTC connection once the signaling link drops."""
logger.info("Signaling disconnected, cleaning up all resources")
try:
await self.rtc_app.stop_all_rtc_connections()
except Exception as e:
logger.error(
f"Error during signaling disconnect cleanup: {e}", exc_info=True
)paramselfReturns
Nonefunchandle_session_start(self, session_peer_id, client_type, client_token=None, display_id='primary', display_position='right') -> NoneStart an RTC connection for a joining peer.
A secondary display's controller is gated on the effective second-screen availability first: the published setting can lag a host-side change, so the capacity is re-read and a refusal closes the peer's signaling socket with a fatal verdict (a bare return would leave the page on "Connecting..." forever).
Source Code
async def handle_session_start(
self, session_peer_id: str, client_type: str, client_token: Optional[str] = None,
display_id: str = "primary", display_position: str = "right",
) -> None:
"""Start an RTC connection for a joining peer.
A secondary display's controller is gated on the effective second-screen
availability first: the published setting can lag a host-side change, so
the capacity is re-read and a refusal closes the peer's signaling socket
with a fatal verdict (a bare return would leave the page on
"Connecting..." forever).
Args:
session_peer_id: The signaling peer id of the joining client.
client_type: "controller" or a viewer/shared role name.
client_token: Optional per-client auth token (governs input role).
display_id: The display this peer consumes ("primary" or a
secondary id).
display_position: Where a joining secondary sits relative to the
primary ("right", "left", "up", "down").
"""
logger.info(
f"starting session for client peer id: {session_peer_id} of type: {client_type} (display '{display_id}')"
)
try:
if display_id != "primary" and client_type == "controller":
await self._refresh_second_screen_capacity()
available, reason = self._second_screen_availability()
if not available:
logger.warning(
"Secondary display '%s' refused: %s", display_id, reason,
)
await self._close_peer_signaling_ws(
session_peer_id, 4000, reason.encode(),
)
return
# Dimensions arrive through the client's first resize message.
entry = self.display_clients.setdefault(display_id, {"width": 0, "height": 0})
entry["position"] = display_position
self._seed_display_settings(entry)
await self.rtc_app.start_rtc_connection(session_peer_id, client_type, client_token, display_id)
if self.args.enable_webrtc_statistics and self.metrics:
await self.metrics.initialize_webrtc_csv_file(self.args.webrtc_statistics_dir)
logger.info(f"started session for client peer id {session_peer_id}")
except Exception as e:
logger.error(
f"Error starting session for client peer id {session_peer_id}: {e}",
exc_info=True,
)
await self.rtc_app.stop_rtc_connection(session_peer_id, client_type)paramselfparamsession_peer_idstrThe signaling peer id of the joining client.
paramclient_typestr"controller" or a viewer/shared role name.
paramclient_tokenOptional[str]= NoneOptional per-client auth token (governs input role).
paramdisplay_idstr= 'primary'The display this peer consumes ("primary" or a secondary id).
paramdisplay_positionstr= 'right'Where a joining secondary sits relative to the primary ("right", "left", "up", "down").
Returns
Nonefunchandle_session_end(self, session_peer_id, client_type) -> NoneHandle end of a session initiated by a client. Stops the RTC connection and media pipeline for the given session peer id.
Source Code
async def handle_session_end(self, session_peer_id: str, client_type: str) -> None:
"""Handle end of a session initiated by a client.
Stops the RTC connection and media pipeline for the given session peer id.
"""
try:
if self.rtc_app:
await self.rtc_app.stop_rtc_connection(session_peer_id, client_type)
logger.info(
f"session ended for client peer id {session_peer_id} of type {client_type}"
)
except Exception as e:
logger.error(
f"Error handling session end for {session_peer_id}: {e}", exc_info=True
)paramselfparamsession_peer_idstrparamclient_typestrReturns
Nonefunccreate_peer_manager(self, rtc_config) -> NoneBuild the signaling-side peer manager with the fetched RTC config and the TURN/STUN/sharing options the handshake needs.
Source Code
def create_peer_manager(self, rtc_config: Any) -> None:
"""Build the signaling-side peer manager with the fetched RTC config
and the TURN/STUN/sharing options the handshake needs."""
options = argparse.Namespace(
keepalive_timeout=30,
rtc_config_file=self.args.rtc_config_json,
turn_shared_secret=self.args.turn_shared_secret,
rtc_config=rtc_config,
turn_host=self.args.turn_host,
turn_port=self.args.turn_port,
turn_protocol=self.args.turn_protocol,
turn_tls=self.args.turn_tls,
turn_auth_header_name=self.args.turn_rest_username_auth_header,
stun_host=self.args.stun_host,
stun_port=self.args.stun_port,
enable_sharing=self.args.enable_sharing,
enable_shared=self.args.enable_shared,
enable_player2=self.args.enable_player2,
enable_player3=self.args.enable_player3,
enable_player4=self.args.enable_player4,
)
self.peer_manager = WebRTCPeerManagement(options)
self.peer_manager.on_client_presence = self.supervisor.set_clients_presentparamselfparamrtc_configAnyReturns
Nonefuncsetup_callbacks(self) -> NoneWire signaling, RTC app, media pipeline, input handler and monitor callbacks to each other.
Cursors come from pixelflux on both backends (Wayland compositor / X11
XFixes monitor) and route through the input handler's transport
callback, capped at its DPI-scaled cursor size. Offers resolve their
codec and SDP munging per display, so displays can run different
encoders and chroma formats and a live full-colour toggle reaches
every later offer. DPI scaling is wired independently of
enable_resize, which gates only the primary's dynamic resolution
(in on_resize_handler): the websockets transport applies scaling
through the SETTINGS payload regardless of the resize gate, and a
secondary display's whole bring-up rides its resize message.
Source Code
def setup_callbacks(self) -> None:
"""Wire signaling, RTC app, media pipeline, input handler and monitor
callbacks to each other.
Cursors come from pixelflux on both backends (Wayland compositor / X11
XFixes monitor) and route through the input handler's transport
callback, capped at its DPI-scaled cursor size. Offers resolve their
codec and SDP munging per display, so displays can run different
encoders and chroma formats and a live full-colour toggle reaches
every later offer. DPI scaling is wired independently of
`enable_resize`, which gates only the primary's dynamic resolution
(in `on_resize_handler`): the websockets transport applies scaling
through the SETTINGS payload regardless of the resize gate, and a
secondary display's whole bring-up rides its resize message.
"""
if not self.rtc_app or not self.media_pipeline or not self.input_handler:
return
self.signaling_client.on_error = self.handle_signaling_error
self.signaling_client.on_disconnect = self.handle_signaling_disconnect
self.signaling_client.on_session_start = self.handle_session_start
self.signaling_client.on_session_end = self.handle_session_end
self.signaling_client.on_sdp = self.rtc_app.set_sdp
self.signaling_client.on_ice = self.rtc_app.set_ice
self.media_pipeline.produce_data = self.rtc_app.consume_data
self.media_pipeline.on_pipeline_started = self.send_current_cursor
self.rtc_app.request_idr_frame = self.request_idr_for_display
self.rtc_app.start_display_media = self.start_display_media
self.rtc_app.stop_display_media = self.stop_display_media
self.rtc_app.on_sdp = self.signaling_client.send_sdp
self.rtc_app.on_ice = self.signaling_client.send_ice
self.rtc_app.on_data_open = self.handle_data_channel_open
self.rtc_app.on_data_close = lambda: logger.info("Data channel closed")
self.rtc_app.on_data_error = lambda e: logger.error(f"Data channel error: {e}")
self.rtc_app.on_data_message = self.input_handler.on_message
self.rtc_app.on_peer_gone = self.handle_peer_gone
self.input_handler.on_request_keyframe = self.request_idr_for_display
self.input_handler.on_cursor_change = lambda data: (
self.rtc_app.send_cursor_data(data)
)
self.media_pipeline.on_cursor_data = lambda data: (
self.input_handler.on_cursor_change(data)
)
self.media_pipeline.get_cursor_size_cap = lambda: getattr(
self.input_handler, "cursor_size_cap", 0
)
self.input_handler.on_video_encoder_bit_rate = self.handle_video_bitrate_change
self.input_handler.on_audio_encoder_bit_rate = self.handle_audio_bitrate_change
self.input_handler.on_mouse_pointer_visible = self.handle_pointer_visible
self.input_handler.on_clipboard_read = lambda d, t: (
self.rtc_app.send_clipboard_data(d, t)
)
self.input_handler.on_set_fps = self.handle_fps_change
self.input_handler.on_client_fps = lambda fps: (
self.metrics.set_fps(fps) if self.metrics else None
)
self.input_handler.on_client_latency = lambda latency: (
self.metrics.set_latency(latency) if self.metrics else None
)
self.input_handler.on_ping_response = lambda latency: (
self.rtc_app.send_latency_time(latency)
)
self.input_handler.on_client_webrtc_stats = self.handle_client_werbtc_stats
self.input_handler.on_update_settings = self.handle_update_settings
self.input_handler.on_update_rate_control_mode = self.handle_rate_control_change
self.input_handler.on_update_crf = self.handle_crf_change
self.rtc_app.get_encoder_for_display = self._encoder_for_display
self.rtc_app.get_fullcolor_for_display = self._fullcolor_for_display
self.rtc_app.get_use_cpu_for_display = self._use_cpu_for_display
self.rtc_app.on_video_consumer_active = self.handle_video_consumer_active
self.rtc_app.on_consumers_changed = self.handle_consumers_changed
# /api/tokens updates must reach live WebRTC peers too.
selkies_module.webrtc_reconcile_hook = self.reconcile_webrtc_peers
self.input_handler.on_scaling_ratio = self.handle_scaling
self.input_handler.on_resize = self.on_resize_handler
self.gpu_monitor.on_stats = self.handle_gpu_stats
self.system_monitor.on_timer = self.handle_system_monitorparamselfReturns
Nonefunc_second_screen_availability(self) -> Tuple[bool, str]Whether this session can actually attach a second display, and the reason when it cannot (websockets-mode parity).
The admin flag gates first; past it, X11 and the self-composited Wayland backend mint another output on demand, while host capture is bounded by the host compositor's real output count (unknown until a pipeline start establishes the host session).
Source Code
def _second_screen_availability(self) -> Tuple[bool, str]:
"""Whether this session can actually attach a second display, and the
reason when it cannot (websockets-mode parity).
The admin flag gates first; past it, X11 and the self-composited
Wayland backend mint another output on demand, while host capture is
bounded by the host compositor's real output count (unknown until a
pipeline start establishes the host session).
Returns:
A ``(available, reason)`` pair; ``reason`` is empty when available.
"""
enabled, _ = self.settings.second_screen
if not enabled:
return False, "Second screens are disabled on this server."
if not IS_WAYLAND or not (self.settings.wayland_host_display or "").strip():
return True, ""
capacity = self._host_output_capacity
if capacity is None or capacity < 0:
return False, "The host compositor's outputs are not known yet."
if capacity < 2:
return False, "The host compositor has a single output, so a second display has nothing to capture."
return True, ""paramselfReturns
typing.TupleA (available, reason) pair; reason is empty when available.
func_refresh_second_screen_capacity(self) -> boolHost-capture mode only: re-read how many outputs the host exposes.
Source Code
async def _refresh_second_screen_capacity(self) -> bool:
"""Host-capture mode only: re-read how many outputs the host exposes.
Returns:
True when the answer changed, i.e. the second-screen availability
that clients were told may have flipped.
"""
if not IS_WAYLAND or not (self.settings.wayland_host_display or "").strip():
return False
module = self._wayland_capture_handle()
if module is None or not hasattr(module, "output_capacity"):
return False
try:
capacity = int(await asyncio.to_thread(module.output_capacity))
except Exception as e:
logger.warning(f"Wayland output capacity query failed: {e}")
return False
changed = capacity != self._host_output_capacity
self._host_output_capacity = capacity
return changedparamselfReturns
boolTrue when the answer changed, i.e. the second-screen availability
func_server_settings_payload(self) -> Dict[str, Any]get_server_settings with second_screen and ui_sidebar_show_apps published as EFFECTIVE availability — the admin flag AND what the backend can actually do — so dashboards never offer a second display the server would immediately refuse, nor an apps panel whose every button would fail. Adds the terminal the apps panel launches in, chosen by the session's windowing system (absent when none is installed: the client keeps its default).
Source Code
def _server_settings_payload(self) -> Dict[str, Any]:
"""get_server_settings with second_screen and ui_sidebar_show_apps
published as EFFECTIVE availability — the admin flag AND what the
backend can actually do — so dashboards never offer a second display
the server would immediately refuse, nor an apps panel whose every
button would fail. Adds the terminal the apps panel launches in, chosen
by the session's windowing system (absent when none is installed: the
client keeps its default)."""
payload = get_server_settings()
available, _ = self._second_screen_availability()
entry = payload.get("settings", {}).get("second_screen")
if isinstance(entry, dict) and entry.get("value") and not available:
payload["settings"]["second_screen"] = dict(entry, value=False)
apps = payload.get("settings", {}).get("ui_sidebar_show_apps")
if (isinstance(apps, dict) and apps.get("value")
and self.input_handler and not self.input_handler.apps_available()):
payload["settings"]["ui_sidebar_show_apps"] = dict(apps, value=False)
terminal = self.input_handler.app_terminal() if self.input_handler else None
if terminal:
payload["settings"]["app_terminal"] = {"value": terminal}
return payloadparamselfReturns
typing.Dict[str, typing.Any]funchandle_data_channel_open(self, channel=None) -> NoneGreet the peer that just joined: every display page and viewer needs the server settings for conditional UI, the current cursor, and the display roster. Sent on ITS channel when given; without one, falls back to broadcasting.
Source Code
def handle_data_channel_open(self, channel: Optional[Any] = None) -> None:
"""Greet the peer that just joined: every display page and viewer needs
the server settings for conditional UI, the current cursor, and the
display roster. Sent on ITS channel when given; without one, falls back
to broadcasting."""
logger.info("opened peer data channel for user input to X11")
server_settings_payload = self._server_settings_payload()
if channel is not None:
self.rtc_app.send_message_to_channel(
channel, "server_settings", server_settings_payload
)
displays = ["primary"] + [d for d in self.display_clients.keys() if d != "primary"]
self.rtc_app.send_message_to_channel(
channel, "display_config_update", {"displays": displays}
)
else:
self.rtc_app.send_media_data_over_channel(
"server_settings", server_settings_payload
)
self._broadcast_display_config()
self.send_current_cursor(channel)paramselfparamchannelOptional[Any]= NoneReturns
Nonefuncsend_current_cursor(self, channel=None) -> NoneResend the current cursor (on channel open / video restart): to one peer's channel when given, otherwise to every connected peer.
Idempotent; a slept/woken tab clears its cursor canvas and needs it back.
Source Code
def send_current_cursor(self, channel: Optional[Any] = None) -> None:
"""Resend the current cursor (on channel open / video restart): to one
peer's channel when given, otherwise to every connected peer.
Idempotent; a slept/woken tab clears its cursor canvas and needs it back.
"""
if not self.rtc_app:
return
cursor_data = None
if self.input_handler:
try:
cursor_data = self.input_handler.get_current_cursor_data()
except Exception as e:
logger.warning(f"Failed to fetch current cursor data: {e}")
if cursor_data is None:
cursor_data = self.rtc_app.last_cursor_sent
if not cursor_data:
return
try:
if channel is not None:
self.rtc_app.send_message_to_channel(channel, "cursor", cursor_data)
else:
self.rtc_app.send_cursor_data(cursor_data)
except Exception as e:
logger.warning(f"Failed to send current cursor to client: {e}")paramselfparamchannelOptional[Any]= NoneReturns
Nonefunchandle_pointer_visible(self, visible) -> NoneCompose the cursor into the captured video, on every display's capture (the websockets capture_cursor tunable is likewise global).
Source Code
async def handle_pointer_visible(self, visible: bool) -> None:
"""Compose the cursor into the captured video, on every display's capture
(the websockets capture_cursor tunable is likewise global)."""
for pipeline in list(self.display_pipelines.values()):
if pipeline is not None:
await pipeline.set_pointer_visible(visible)paramselfparamvisibleboolReturns
Nonefunchandle_video_bitrate_change(self, bitrate, display_id='primary') -> NoneVideo bitrate change for the display whose page sent it; sanitized against the server's configured range like the SETTINGS path, so the opcode cannot bypass a locked/narrowed range.
Source Code
async def handle_video_bitrate_change(self, bitrate: int, display_id: str = "primary") -> None:
"""Video bitrate change for the display whose page sent it; sanitized
against the server's configured range like the SETTINGS path, so the
opcode cannot bypass a locked/narrowed range."""
sanitized = sanitize_client_setting("video_bitrate", bitrate, self.settings, logger)
if sanitized is None or sanitized == self._display_setting(display_id, "video_bitrate"):
return
await self._apply_display_setting(display_id or "primary", "video_bitrate", sanitized)paramselfparambitrateintparamdisplay_idstr= 'primary'Returns
Nonefunchandle_audio_bitrate_change(self, bitrate) -> NoneHandle audio bitrate change request (bps; sanitized like SETTINGS).
Source Code
async def handle_audio_bitrate_change(self, bitrate: int) -> None:
"""Handle audio bitrate change request (bps; sanitized like SETTINGS)."""
sanitized = sanitize_client_setting("audio_bitrate", bitrate, self.settings, logger)
if sanitized is None or sanitized == getattr(self.args, "audio_bitrate", None):
return
if self.media_pipeline:
await self.media_pipeline.set_audio_bitrate(int(sanitized))
self.args.audio_bitrate = sanitizedparamselfparambitrateintReturns
Nonefunchandle_fps_change(self, fps, display_id='primary') -> NoneFramerate change for the display whose page sent it; sanitized against the server's configured range like the SETTINGS path.
Source Code
async def handle_fps_change(self, fps: int, display_id: str = "primary") -> None:
"""Framerate change for the display whose page sent it; sanitized against
the server's configured range like the SETTINGS path."""
sanitized = sanitize_client_setting("framerate", fps, self.settings, logger)
if sanitized is None or sanitized == self._display_setting(display_id, "framerate"):
return
await self._apply_display_setting(display_id or "primary", "framerate", sanitized)paramselfparamfpsintparamdisplay_idstr= 'primary'Returns
Nonefunchandle_rate_control_change(self, mode, display_id='primary') -> NoneRate-control switch for the display whose page sent it; honors the server's enable_rate_control lock like the SETTINGS path.
Source Code
async def handle_rate_control_change(self, mode: Any, display_id: str = "primary") -> None:
"""Rate-control switch for the display whose page sent it; honors the
server's enable_rate_control lock like the SETTINGS path."""
if self.args.enable_rate_control is False:
logger.debug("Server has rate control disabled. Ignoring rate-control change.")
return
# Store the plain value: str(<str-Enum>) formats as the member name on
# some supported Python versions, which would corrupt later comparisons.
mode_str = mode.value if isinstance(mode, RateControlMode) else str(mode)
await self._apply_display_setting(display_id or "primary", "rate_control_mode", mode_str)paramselfparammodeAnyparamdisplay_idstr= 'primary'Returns
Nonefunchandle_crf_change(self, crf, display_id='primary') -> NoneCRF change for the display whose page sent it; sanitized against the server's configured range like the SETTINGS path.
Source Code
async def handle_crf_change(self, crf: int, display_id: str = "primary") -> None:
"""CRF change for the display whose page sent it; sanitized against the
server's configured range like the SETTINGS path."""
sanitized = sanitize_client_setting("video_crf", int(crf), self.settings, logger)
if sanitized is None or sanitized == self._display_setting(display_id, "video_crf"):
return
await self._apply_display_setting(display_id or "primary", "video_crf", sanitized)paramselfparamcrfintparamdisplay_idstr= 'primary'Returns
Nonefunchandle_client_werbtc_stats(self, webrtc_stat_type, webrtc_stats) -> NoneIngest client-reported WebRTC stats, gated on the Metrics object itself (built for metrics-http AND/OR the CSV statistics flag) so CSV-only configs actually ingest the stats they enabled.
Source Code
async def handle_client_werbtc_stats(
self, webrtc_stat_type: str, webrtc_stats: str
) -> None:
"""Ingest client-reported WebRTC stats, gated on the Metrics object
itself (built for metrics-http AND/OR the CSV statistics flag) so
CSV-only configs actually ingest the stats they enabled."""
if self.metrics:
await self.metrics.set_webrtc_stats(webrtc_stat_type, webrtc_stats)paramselfparamwebrtc_stat_typestrparamwebrtc_statsstrReturns
Nonefuncon_resize_handler(self, res, display_id='primary') -> NoneRoute a client resolution to its display: the primary resizes the real display directly while it is alone; once a secondary display is connected (or for any secondary), the resolution feeds the extended-desktop layout instead (websockets parity).
The layout path honors an admin manual-resolution lock the way the single-display path does: every display follows the server's geometry, so a second screen cannot be the way a client escapes the lock, and a locked size is the server's own, beyond a client alignment toggle.
Source Code
async def on_resize_handler(self, res: str, display_id: str = "primary") -> None:
"""Route a client resolution to its display: the primary resizes the real
display directly while it is alone; once a secondary display is connected
(or for any secondary), the resolution feeds the extended-desktop layout
instead (websockets parity).
The layout path honors an admin manual-resolution lock the way the
single-display path does: every display follows the server's geometry,
so a second screen cannot be the way a client escapes the lock, and a
locked size is the server's own, beyond a client alignment toggle.
"""
display_id = display_id or "primary"
if display_id == "primary" and not self.args.enable_resize:
logger.warning(f"remote resizing disabled, skipping resize to {res}")
return
if display_id != "primary" or self.display_clients:
locked_dims = self._server_locked_dims()
if locked_dims is not None:
logger.warning(
f"Client attempted to resize to {res} but server is in manual resolution mode. "
f"Using the configured {locked_dims[0]}x{locked_dims[1]} instead."
)
w, h = locked_dims
else:
dims = parse_resize_dims(res)
if dims is None:
logger.error(f"Invalid resize request: {res}")
return
w, h = dims
if self._display_setting(display_id, "force_aligned_resolution"):
w, h = align_dims_16(w, h)
if display_id == "primary":
self._primary_dims = (w, h)
else:
entry = self.display_clients.get(display_id)
if entry is None:
logger.warning(f"Resize for unknown display '{display_id}' ignored.")
return
entry["width"], entry["height"] = w, h
await self.reconfigure_displays()
return
self._primary_dims = None
await self._resize_primary_display(res)paramselfparamresstrparamdisplay_idstr= 'primary'Returns
Nonefunc_server_locked_dims(self) -> Optional[Tuple[int, int]]The geometry an admin-configured manual-resolution lock pins the desktop to, or None when the server sets no lock. Derived on every read from the server settings (never from the client-writable args, whose manual trio is the client's own manual/auto toggle) and from the dimensions startup realized, so the lock cannot drift with client state. The settings layer guarantees positive manual dimensions while the lock is on; its own defaults stand in should one be unusable, so a locked server never falls back to honoring the client's request.
Source Code
def _server_locked_dims(self) -> Optional[Tuple[int, int]]:
"""The geometry an admin-configured manual-resolution lock pins the desktop
to, or None when the server sets no lock. Derived on every read from the
server settings (never from the client-writable args, whose manual trio is
the client's own manual/auto toggle) and from the dimensions startup
realized, so the lock cannot drift with client state. The settings layer
guarantees positive manual dimensions while the lock is on; its own
defaults stand in should one be unusable, so a locked server never
falls back to honoring the client's request."""
server_is_manual, _ = self.settings.manual_resolution
if not server_is_manual:
return None
if self._manual_dims:
return self._manual_dims
width = int(getattr(self.settings, "manual_width", 0) or 0)
height = int(getattr(self.settings, "manual_height", 0) or 0)
if width <= 0:
width = 1024
if height <= 0:
height = 768
return (width - (width % 2), height - (height % 2))paramselfReturns
typing.Optional[typing.Tuple[int, int]]func_resize_primary_display(self, res) -> NoneResize the single (primary-only) display to a client-requested resolution, keep the capture dimensions in sync with what was realized, and tell the client the realized size when it differs.
Only an admin-configured manual-resolution lock blocks client resizes
(websockets parity); the client's own manual/auto toggle in args
must not gate here, since in client manual mode the chosen resolution
arrives through this same path. Idempotent: clients re-assert their
resolution on reconnects and settings broadcasts, and re-applying the
current size would churn RandR (X11) or restart the capture (Wayland)
for nothing; the last request counts as applied too, or a request the
realized size differs from (CVT cell alignment) would read as pending
forever. On Wayland there is no X server to resize: the compositor
output follows the capture dimensions, so a running capture restarts
and the compositor's realized geometry (it may even-mask or refuse the
mode) is reconciled and pushed to the client.
Source Code
async def _resize_primary_display(self, res: str) -> None:
"""Resize the single (primary-only) display to a client-requested
resolution, keep the capture dimensions in sync with what was realized,
and tell the client the realized size when it differs.
Only an admin-configured manual-resolution lock blocks client resizes
(websockets parity); the client's own manual/auto toggle in `args`
must not gate here, since in client manual mode the chosen resolution
arrives through this same path. Idempotent: clients re-assert their
resolution on reconnects and settings broadcasts, and re-applying the
current size would churn RandR (X11) or restart the capture (Wayland)
for nothing; the last request counts as applied too, or a request the
realized size differs from (CVT cell alignment) would read as pending
forever. On Wayland there is no X server to resize: the compositor
output follows the capture dimensions, so a running capture restarts
and the compositor's realized geometry (it may even-mask or refuse the
mode) is reconciled and pushed to the client.
"""
if self._server_locked_dims() is not None:
logger.warning(
f"Client attempted to resize to {res} but server is in manual resolution mode. Request ignored."
)
return
try:
dims = parse_resize_dims(res)
if dims is None:
logger.error(f"Invalid resize request: {res}. Ignoring")
if self.media_pipeline:
self.media_pipeline.last_resize_success = False
return
target_w, target_h = dims
if getattr(self.args, "force_aligned_resolution", False):
target_w, target_h = align_dims_16(target_w, target_h)
if (
self.media_pipeline
and self.media_pipeline.last_resize_success
and (
(self.media_pipeline.width == target_w
and self.media_pipeline.height == target_h)
or self._last_resize_request == (target_w, target_h)
)
):
logger.debug(f"Resolution already {target_w}x{target_h}; skipping re-apply.")
return
if IS_WAYLAND:
self.media_pipeline.width = target_w
self.media_pipeline.height = target_h
# The capture, not the whole pipeline: the session's first request
# can land while audio is still coming up. An unstarted capture
# reads the new size when it starts.
if self.media_pipeline.is_screen_capturing():
await self.media_pipeline.restart_screen_capture()
await self._push_wayland_realized_geometry("primary", self.media_pipeline)
self.media_pipeline.last_resize_success = True
self._last_resize_request = (target_w, target_h)
logger.info(
f"Wayland capture resized to {self.media_pipeline.width}x{self.media_pipeline.height}"
f" (requested {target_w}x{target_h})"
)
return
realized = await resize_display(f"{target_w}x{target_h}")
if realized:
realized_w, realized_h = realized
if (realized_w, realized_h) != (target_w, target_h):
logger.info(
f"resize_display realized {realized_w}x{realized_h} for request {target_w}x{target_h}"
)
else:
logger.info(f"resize_display('{target_w}x{target_h}') reported success")
# A zero-size region re-reads the live root now and keeps root-follow;
# the auto-adjust poll trails ~30 frames, leaving new bands out of frame.
capture_module = getattr(self.media_pipeline, "capture_module", None)
if capture_module is not None:
try:
await asyncio.to_thread(
capture_module.update_capture_region, 0, 0, 0, 0
)
except Exception as e:
logger.warning(f"Capture re-follow after resize failed: {e}")
self.media_pipeline.width = realized_w
self.media_pipeline.height = realized_h
self.media_pipeline.last_resize_success = True
self._last_resize_request = (target_w, target_h)
if self.rtc_app is not None:
self.rtc_app.send_remote_resolution(f"{realized_w}x{realized_h}", "primary")
else:
logger.error(
f"resize_display('{target_w}x{target_h}') reported failure"
)
self.media_pipeline.last_resize_success = False
except Exception as e:
logger.error(
f"Error during resize handling for '{res}': {e}", exc_info=True
)
if self.media_pipeline:
self.media_pipeline.last_resize_success = FalseparamselfparamresstrReturns
Nonefuncrequest_idr_for_display(self, display_id='primary') -> NoneSchedule a dynamic IDR frame on the display's encoder, throttled by a per-display floor (websockets REQUEST_KEYFRAME parity).
Any number of viewers share one encoder, and an unthrottled data-channel request or PLI storm would let a single client force keyframe bursts for every consumer. A request landing inside the floor is satisfied by the IDR the previous request already scheduled.
Source Code
async def request_idr_for_display(self, display_id: str = "primary") -> None:
"""Schedule a dynamic IDR frame on the display's encoder, throttled by a
per-display floor (websockets REQUEST_KEYFRAME parity).
Any number of viewers share one encoder, and an unthrottled
data-channel request or PLI storm would let a single client force
keyframe bursts for every consumer. A request landing inside the floor
is satisfied by the IDR the previous request already scheduled.
"""
display_id = display_id or "primary"
now = time.monotonic()
if now - self._last_idr_request_times.get(display_id, 0.0) < 0.25:
return
self._last_idr_request_times[display_id] = now
pipeline = self.display_pipelines.get(display_id)
if pipeline is not None:
await pipeline.dynamic_idr_frame()paramselfparamdisplay_idstr= 'primary'Returns
Nonefunc_provision_webrtc_virtual_mic(self) -> NoneBring up the SelkiesVirtualMic once for the WebRTC transport (shared provisioning with the websockets 0x02 path). Called from the per-peer mic playback start on the first mic packet; the lock + flag make concurrent first-packet calls across peers provision exactly once, and the shared helper reuses a source the websockets path already loaded rather than double-loading it.
Source Code
async def _provision_webrtc_virtual_mic(self) -> None:
"""Bring up the SelkiesVirtualMic once for the WebRTC transport (shared
provisioning with the websockets 0x02 path). Called from the per-peer mic
playback start on the first mic packet; the lock + flag make concurrent
first-packet calls across peers provision exactly once, and the shared
helper reuses a source the websockets path already loaded rather than
double-loading it."""
if self._mic_provisioned:
return
async with self._mic_provision_lock:
if self._mic_provisioned:
return
if self._mic_control is None:
self._mic_control = AudioControl("selkies-webrtc-mic")
audio_device_name = getattr(self.media_pipeline, "audio_device_name", None)
is_capturing = bool(getattr(self.media_pipeline, "_is_pcmflux_capturing", False))
self._mic_module_index, self._mic_module_owned = (
await self._mic_control.ensure_virtual_microphone(audio_device_name, is_capturing)
)
self._mic_provisioned = self._mic_module_index is not NoneparamselfReturns
Nonefunc_teardown_webrtc_virtual_mic(self) -> NoneUnload the virtual-source module (only if this path loaded it) and release the mic control connection on shutdown.
Source Code
async def _teardown_webrtc_virtual_mic(self) -> None:
"""Unload the virtual-source module (only if this path loaded it) and
release the mic control connection on shutdown."""
control = self._mic_control
self._mic_control = None
if control is None:
return
if self._mic_module_index is not None and self._mic_module_owned:
logger.info(f"Unloading WebRTC virtual mic module {self._mic_module_index}.")
await control.unload_module(self._mic_module_index)
self._mic_module_index = None
self._mic_module_owned = False
self._mic_provisioned = False
await control.aclose()paramselfReturns
Nonefuncstart_display_media(self, display_id) -> NoneA display's consumer connected: the primary starts its pipeline right away; a secondary waits for its dimensions (the client's first resize message), which trigger the layout pass that creates its pipeline.
A consumer reclaiming the primary cancels a pending grace stop, and
start_media_pipeline is idempotent, so a controller tab reload that
reconnects inside the grace reuses the still-warm capture. A Wayland
start only enqueues a compositor command, so its real outcome is read
back through the same barrier the secondaries use: a pipeline that
believes it is running with no live capture would leave the page
waiting on frames that never arrive, so it is stopped and logged and
the next consumer retries (this also surfaces host death, where
reap_dead_host flips is_capturing off). The pipeline start is what
establishes the host session in host-capture mode, so the host's
output count — and the second-screen availability announced on
channel open — can first become known here and is re-published.
Source Code
async def start_display_media(self, display_id: str) -> None:
"""A display's consumer connected: the primary starts its pipeline right
away; a secondary waits for its dimensions (the client's first resize
message), which trigger the layout pass that creates its pipeline.
A consumer reclaiming the primary cancels a pending grace stop, and
`start_media_pipeline` is idempotent, so a controller tab reload that
reconnects inside the grace reuses the still-warm capture. A Wayland
start only enqueues a compositor command, so its real outcome is read
back through the same barrier the secondaries use: a pipeline that
believes it is running with no live capture would leave the page
waiting on frames that never arrive, so it is stopped and logged and
the next consumer retries (this also surfaces host death, where
`reap_dead_host` flips `is_capturing` off). The pipeline start is what
establishes the host session in host-capture mode, so the host's
output count — and the second-screen availability announced on
channel open — can first become known here and is re-published.
"""
if display_id == "primary" and self.media_pipeline:
self._cancel_primary_stop_grace()
await self.media_pipeline.start_media_pipeline()
if (IS_WAYLAND and self.media_pipeline.is_media_pipeline_running()
and not await self._wayland_capture_live("primary", self.media_pipeline)):
last_error = self._wayland_capture_last_error(self.media_pipeline, "primary")
logger.error(
"Primary Wayland capture is not live after start"
+ (f": {last_error}." if last_error else "."))
await self.media_pipeline.stop_media_pipeline()
return
caveat = (self._wayland_capture_last_error(self.media_pipeline, "primary")
if IS_WAYLAND else None)
if caveat:
logger.warning(f"Primary Wayland capture started with a caveat: {caveat}")
if await self._refresh_second_screen_capacity() and self.rtc_app:
self.rtc_app.send_media_data_over_channel(
"server_settings", self._server_settings_payload()
)paramselfparamdisplay_idstrReturns
Nonefuncstop_display_media(self, display_id) -> NoneRelease a display's pipeline: the primary's stop is deferred by a reconnect grace (a controller tab reload reconnects within a second or two and reuses the warm capture, and viewers/display2 keep streaming throughout — websockets _teardown_if_unclaimed parity); a secondary is fully unregistered and the desktop re-laid-out at once.
Source Code
async def stop_display_media(self, display_id: str) -> None:
"""Release a display's pipeline: the primary's stop is deferred by a
reconnect grace (a controller tab reload reconnects within a second or
two and reuses the warm capture, and viewers/display2 keep streaming
throughout — websockets _teardown_if_unclaimed parity); a secondary is
fully unregistered and the desktop re-laid-out at once."""
if display_id == "primary":
self._schedule_primary_stop_grace()
return
async with self._display_lock:
pipeline = self.display_pipelines.pop(display_id, None)
self.display_clients.pop(display_id, None)
self.display_layouts.pop(display_id, None)
if pipeline is not None:
await pipeline.stop_media_pipeline()
await self.reconfigure_displays()paramselfparamdisplay_idstrReturns
Nonefunc_cancel_primary_stop_grace(self) -> NoneDrop a pending deferred primary-capture stop: a consumer reclaimed the display before the grace elapsed.
Source Code
def _cancel_primary_stop_grace(self) -> None:
"""Drop a pending deferred primary-capture stop: a consumer reclaimed
the display before the grace elapsed."""
task = self._primary_stop_grace_task
self._primary_stop_grace_task = None
if task is not None and not task.done():
task.cancel()paramselfReturns
Nonefunc_schedule_primary_stop_grace(self) -> NoneStop the primary capture after RECONNECT_GRACE_S unless a consumer reclaims it first. A page reload drops and re-adds its peer within the window, so tearing the capture down immediately would black out a reconnecting controller (and stall the audio fan-out the viewers share) for no reason; if nobody reclaims the primary, the stop runs after the grace. At most one grace is pending at a time.
Source Code
def _schedule_primary_stop_grace(self) -> None:
"""Stop the primary capture after RECONNECT_GRACE_S unless a consumer
reclaims it first. A page reload drops and re-adds its peer within the
window, so tearing the capture down immediately would black out a
reconnecting controller (and stall the audio fan-out the viewers share)
for no reason; if nobody reclaims the primary, the stop runs after the
grace. At most one grace is pending at a time."""
if self._primary_stop_grace_task is not None and not self._primary_stop_grace_task.done():
return
if self.media_pipeline is None or not self.media_pipeline.is_media_pipeline_running():
return
async def _stop_after_grace() -> None:
"""Stop the primary unless reclaimed. No lock: the event loop
serializes start/stop with this coroutine's resume."""
try:
await asyncio.sleep(self.RECONNECT_GRACE_S)
except asyncio.CancelledError:
return
self._primary_stop_grace_task = None
if self._primary_display_has_consumer():
logger.info("Primary reclaimed within the grace; capture kept.")
return
if self.media_pipeline is not None:
logger.info("Primary unclaimed after the grace; stopping its capture.")
await self.media_pipeline.stop_media_pipeline()
self._primary_stop_grace_task = asyncio.create_task(_stop_after_grace())paramselfReturns
Nonefunc_primary_display_has_consumer(self) -> boolWhether any peer (controller or viewer) still consumes the primary.
Source Code
def _primary_display_has_consumer(self) -> bool:
"""Whether any peer (controller or viewer) still consumes the primary."""
if self.rtc_app is None:
return False
return any(
(p.get("display_id") or "primary") == "primary"
for p in self.rtc_app.peer_connections.values()
)paramselfReturns
boolfunc_wayland_capture_handle(self) -> Optional[Any]A pixelflux handle for compositor output management (any ScreenCapture reaches the shared Wayland backend); prefers the primary pipeline's live capture module.
Source Code
def _wayland_capture_handle(self) -> Optional[Any]:
"""A pixelflux handle for compositor output management (any ScreenCapture
reaches the shared Wayland backend); prefers the primary pipeline's live
capture module."""
module = getattr(self.media_pipeline, "capture_module", None)
if module is not None:
return module
if PixelfluxScreenCapture is None:
return None
if self._wayland_ctl_module is None:
self._wayland_ctl_module = PixelfluxScreenCapture()
return self._wayland_ctl_moduleparamselfReturns
typing.Optional[typing.Any]func_destroy_wayland_secondary_outputs(self, keep_oid=None) -> NoneRetire every secondary compositor output except keep_oid (the
primary, output 0, always persists).
Source Code
async def _destroy_wayland_secondary_outputs(self, keep_oid: Optional[int] = None) -> None:
"""Retire every secondary compositor output except `keep_oid` (the
primary, output 0, always persists)."""
module = self._wayland_capture_handle()
if module is None:
return
try:
for out in await asyncio.to_thread(module.list_outputs):
if out[0] != 0 and out[0] != keep_oid:
await asyncio.to_thread(module.destroy_output, out[0])
except Exception as e:
logger.warning(f"Wayland output teardown failed: {e}")paramselfparamkeep_oidOptional[int]= NoneReturns
Nonefunc_apply_wayland_extension(self, did, layouts) -> boolRealize the extended layout as compositor outputs, BEFORE the secondary's pipeline binds a capture — the Wayland counterpart of apply_extended_layout.
The primary (output 0) MOVES to its layout offset ('left'/'up' place it off-origin); a secondary reposition is a destroy + recreate (its capture rebinds on the pipeline restart that follows), destroyed before the primary moves so the rectangles never overlap.
Source Code
async def _apply_wayland_extension(self, did: str, layouts: Dict[str, Dict[str, int]]) -> bool:
"""Realize the extended layout as compositor outputs, BEFORE the
secondary's pipeline binds a capture — the Wayland counterpart of
apply_extended_layout.
The primary (output 0) MOVES to its layout offset ('left'/'up' place it
off-origin); a secondary reposition is a destroy + recreate (its
capture rebinds on the pipeline restart that follows), destroyed before
the primary moves so the rectangles never overlap.
Returns:
False when the output cannot be created or the primary cannot move
(the caller drops the display).
"""
module = self._wayland_capture_handle()
if module is None:
return False
oid = wayland_output_id(did)
s = layouts[did]
scale = float(getattr(self.media_pipeline, "scale", 1.0) or 1.0)
try:
outputs = {o[0]: o for o in await asyncio.to_thread(module.list_outputs)}
except Exception as e:
logger.error(f"Wayland list_outputs failed: {e}")
outputs = {}
await self._destroy_wayland_secondary_outputs(keep_oid=oid)
existing = outputs.get(oid)
if existing is not None and (existing[1], existing[2]) != (s["x"], s["y"]):
logger.info(f"Wayland output {oid} moves to +{s['x']}+{s['y']}; recreating it.")
await asyncio.to_thread(module.destroy_output, oid)
existing = None
p = layouts.get("primary") or {"x": 0, "y": 0}
existing0 = outputs.get(0)
current0 = (existing0[1], existing0[2]) if existing0 is not None else (0, 0)
if (p["x"], p["y"]) != current0:
if not await wayland_reposition_primary(module, p["x"], p["y"]):
return False
if existing is not None:
return True
try:
created = bool(await asyncio.to_thread(
module.create_output, oid, s["w"], s["h"], s["x"], s["y"], scale))
except Exception as e:
logger.error(f"Wayland create_output {oid} failed: {e}")
return False
if created and self.input_handler:
# Which of the session's own screens a capture drives changed.
self.input_handler.resync_session_screens()
return createdparamselfparamdidstrparamlayoutsDict[str, Dict[str, int]]Returns
boolFalse when the output cannot be created or the primary cannot move
func_wayland_capture_live(self, did, pipeline) -> boolWhether the display's capture really runs in the compositor. The geometry read is a barrier: it is answered only after the queued capture start finished, so is_capturing is authoritative afterwards.
Source Code
async def _wayland_capture_live(self, did: str, pipeline: MediaPipelinePixel) -> bool:
"""Whether the display's capture really runs in the compositor. The
geometry read is a barrier: it is answered only after the queued capture
start finished, so is_capturing is authoritative afterwards."""
module = getattr(pipeline, "capture_module", None)
if module is None:
return False
try:
await asyncio.to_thread(module.get_realized_geometry, wayland_output_id(did))
return bool(module.is_capturing)
except Exception:
return FalseparamselfparamdidstrparampipelineMediaPipelinePixelReturns
boolfunc_wayland_capture_last_error(self, pipeline, did) -> Optional[str]The reason a display's Wayland capture failed, or a caveat a live one came up
with (encoder fell back to CPU, host connect refused), or None. Read straight from
capture_state (no barrier); the caller ensures ordering. None on an older
pixelflux without the readback.
Source Code
def _wayland_capture_last_error(
self, pipeline: Optional[MediaPipelinePixel], did: str
) -> Optional[str]:
"""The reason a display's Wayland capture failed, or a caveat a live one came up
with (encoder fell back to CPU, host connect refused), or None. Read straight from
``capture_state`` (no barrier); the caller ensures ordering. None on an older
pixelflux without the readback."""
module = getattr(pipeline, "capture_module", None) if pipeline is not None else None
getter = getattr(module, "capture_state", None) if module is not None else None
if getter is None:
return None
try:
_state, last_error = getter(wayland_output_id(did))
return last_error
except Exception:
return NoneparamselfparampipelineOptional[MediaPipelinePixel]paramdidstrReturns
typing.Optional[str]func_realized_wayland_dims(self, did) -> Optional[Tuple[int, int]]The (width, height) the compositor currently has for this
display's output, or None when it cannot be read.
Source Code
async def _realized_wayland_dims(self, did: str) -> Optional[Tuple[int, int]]:
"""The ``(width, height)`` the compositor currently has for this
display's output, or None when it cannot be read."""
if not IS_WAYLAND:
return None
pipeline = (self.media_pipeline if did == "primary"
else self.display_pipelines.get(did))
module = getattr(pipeline, "capture_module", None)
if module is None or not hasattr(module, "get_realized_geometry"):
return None
try:
geom = await asyncio.to_thread(
module.get_realized_geometry, wayland_output_id(did))
except Exception as e:
logger.warning(f"Wayland realized-geometry read failed for '{did}': {e}")
return None
if geom is None:
logger.warning(f"Wayland realized-geometry read for '{did}' timed out; size unknown.")
return None
w, h, _scale = geom
return (w, h) if w > 0 and h > 0 else NoneparamselfparamdidstrReturns
typing.Optional[typing.Tuple[int, int]]func_push_wayland_realized_geometry(self, did, pipeline) -> NoneRead what the pixelflux compositor actually realized on this display's output after a capture (re)start (it may even-mask dimensions or keep the old mode on a GBM allocation failure), fold it into the pipeline/layout state the input math offsets against, and push the corrected size to the clients over the existing system resolution message — the WR counterpart of the WS realized clamp + broadcast. The stream itself re-negotiates through the encoder (the track's intrinsic size IS the realized resolution); this closes the control-plane loop. The read is also a barrier: the compositor answers only after the queued capture (re)start finished. The push is unconditional (idempotent, WS-broadcast parity: the client's request may have been snapped by sanitization before the pipeline saw it) and scoped to this display's channels so a secondary's size never rescales the primary page.
Source Code
async def _push_wayland_realized_geometry(
self, did: str, pipeline: Optional[MediaPipelinePixel]
) -> None:
"""Read what the pixelflux compositor actually realized on this
display's output after a capture (re)start (it may even-mask dimensions
or keep the old mode on a GBM allocation failure), fold it into the
pipeline/layout state the input math offsets against, and push the
corrected size to the clients over the existing system resolution
message — the WR counterpart of the WS realized clamp + broadcast. The
stream itself re-negotiates through the encoder (the track's intrinsic
size IS the realized resolution); this closes the control-plane loop.
The read is also a barrier: the compositor answers only after the
queued capture (re)start finished. The push is unconditional
(idempotent, WS-broadcast parity: the client's request may have been
snapped by sanitization before the pipeline saw it) and scoped to this
display's channels so a secondary's size never rescales the primary
page."""
if not IS_WAYLAND or pipeline is None:
return
module = getattr(pipeline, "capture_module", None)
if module is None or not hasattr(module, "get_realized_geometry"):
return
try:
geom = await asyncio.to_thread(
module.get_realized_geometry, wayland_output_id(did))
except Exception as e:
logger.warning(f"Wayland realized-geometry read failed for '{did}': {e}")
return
if geom is None:
# A timeout is unknown geometry, not "nothing to reconcile".
logger.warning(f"Wayland realized-geometry read for '{did}' timed out; state left unreconciled.")
return
w, h, scale = geom
if w <= 0 or h <= 0:
return
pipeline.width, pipeline.height = w, h
if did == "primary":
if self._primary_dims is not None:
self._primary_dims = (w, h)
else:
entry = self.display_clients.get(did)
if entry is not None:
entry["width"], entry["height"] = w, h
layout = self.display_layouts.get(did)
if layout is not None:
layout["w"], layout["h"] = w, h
logger.info(f"Wayland realized geometry for '{did}': {w}x{h} @ scale {scale}")
if self.rtc_app is not None:
self.rtc_app.send_remote_resolution(f"{w}x{h}", did)paramselfparamdidstrparampipelineOptional[MediaPipelinePixel]Returns
Nonefunc_apply_wayland_cursor_size(self, dpi_value) -> NoneWayland counterpart of the X11 per-DPI cursor resize: the compositor reloads its theme cursor (composited overlay and named-cursor delivery both re-render) at the DPI-scaled size, live, no capture restart.
Source Code
async def _apply_wayland_cursor_size(self, dpi_value: float) -> None:
"""Wayland counterpart of the X11 per-DPI cursor resize: the compositor
reloads its theme cursor (composited overlay and named-cursor delivery
both re-render) at the DPI-scaled size, live, no capture restart."""
if CURSOR_SIZE is None:
return
module = self._wayland_capture_handle()
setter = getattr(module, "set_cursor_size", None) if module else None
if setter is None:
logger.warning("Wayland cursor resize unavailable (no set_cursor_size).")
return
size = cursor_size_for_dpi(dpi_value, CURSOR_SIZE)
try:
if await asyncio.to_thread(setter, size):
logger.info(f"Wayland cursor size set to {size} (DPI {dpi_value}).")
else:
logger.warning(f"Wayland compositor refused cursor size {size}.")
except Exception as e:
logger.warning(f"Wayland cursor resize failed: {e}")paramselfparamdpi_valuefloatReturns
Nonefunc_display_consumers(self, display_id) -> List[Dict[str, Any]]Registered peers (controller + viewers) consuming this display's stream.
Source Code
def _display_consumers(self, display_id: str) -> List[Dict[str, Any]]:
"""Registered peers (controller + viewers) consuming this display's
stream."""
if self.rtc_app is None:
return []
return [
p for p in self.rtc_app.peer_connections.values()
if (p.get("display_id") or "primary") == display_id
]paramselfparamdisplay_idstrReturns
typing.List[typing.Dict[str, typing.Any]]funchandle_peer_gone(self, peer_id, peer=None) -> NoneRelease the input a departing peer may still be holding, matching the websockets disconnect cleanup. Gamepad slots are per-connection, so they are always released. Held keys and pointer buttons are one global desktop state instead: they are force-released only when the departing peer could drive input and no input-capable peer is left, so a viewer (or a second display's peer) leaving never drops the controller's held keys or its in-flight drag. A controller that vanishes while others remain is covered by the input handler's heartbeat stale-sweep.
Source Code
async def handle_peer_gone(
self, peer_id: str, peer: Optional[Dict[str, Any]] = None
) -> None:
"""Release the input a departing peer may still be holding, matching the
websockets disconnect cleanup. Gamepad slots are per-connection, so they
are always released. Held keys and pointer buttons are one global desktop
state instead: they are force-released only when the departing peer could
drive input and no input-capable peer is left, so a viewer (or a second
display's peer) leaving never drops the controller's held keys or its
in-flight drag. A controller that vanishes while others remain is covered
by the input handler's heartbeat stale-sweep."""
if self.input_handler is None:
return
try:
await self.input_handler.release_gamepads_for_conn(peer_id)
except Exception as e:
logger.warning(f"Gamepad release for departed peer {peer_id} failed: {e}")
if self.rtc_app is None or not self.rtc_app.peer_holds_input_authority(peer):
return
if (peer.get("display_id") or "primary") != "primary":
# The departing peer is already out of peer_connections: survivors only.
for survivor in list(self.rtc_app.peer_connections.values()):
if self.rtc_app.peer_holds_input_authority(survivor):
return
for release in (self.input_handler.release_mouse_buttons,
self.input_handler.reset_keyboard):
try:
await release()
except Exception as e:
logger.warning(f"Input release for departed peer {peer_id} failed: {e}")paramselfparampeer_idstrparampeerOptional[Dict[str, Any]]= NoneReturns
Nonefunchandle_video_consumer_active(self, peer_id, display_id, active) -> NoneTab-visibility pause/resume for ONE peer (data-channel STOP_VIDEO / START_VIDEO, websockets parity). The peer's own RTP sender gates its delivery; the shared capture only stops once EVERY consumer of the display (controller and viewers alike) is paused. A resuming peer always gets an IDR — its decoder needs a resync even when the capture kept running for other consumers — with PLI as the fallback.
Source Code
async def handle_video_consumer_active(self, peer_id: str, display_id: str, active: bool) -> None:
"""Tab-visibility pause/resume for ONE peer (data-channel STOP_VIDEO /
START_VIDEO, websockets parity). The peer's own RTP sender gates its
delivery; the shared capture only stops once EVERY consumer of the
display (controller and viewers alike) is paused. A resuming peer always
gets an IDR — its decoder needs a resync even when the capture kept
running for other consumers — with PLI as the fallback."""
display_id = display_id or "primary"
peer = self.rtc_app.peer_connections.get(peer_id) if self.rtc_app else None
if peer is None:
return
peer["video_paused"] = not active
sender = peer.get("video_sender")
if sender is not None:
# A disabled sender keeps draining its relay proxy but sends no RTP,
# so only this peer's stream stalls.
sender._enabled = active
pipeline = self.display_pipelines.get(display_id)
if pipeline is None:
return
if active:
await self._resume_display_capture(display_id, pipeline,
"consumer resume", idr_always=True)
elif all(p.get("video_paused", False) for p in self._display_consumers(display_id)):
if await pipeline.pause_screen_capture():
logger.info(
f"All consumers of display '{display_id}' are paused; capture stopped."
)paramselfparampeer_idstrparamdisplay_idstrparamactiveboolReturns
Nonefunc_close_peer_signaling_ws(self, peer_id, code, message) -> NoneFatal verdict on ONE peer's signaling socket (websockets KILL parity); bounded so a wedged socket cannot stall the caller.
Source Code
async def _close_peer_signaling_ws(self, peer_id: str, code: int, message: bytes) -> None:
"""Fatal verdict on ONE peer's signaling socket (websockets KILL parity);
bounded so a wedged socket cannot stall the caller."""
if self.peer_manager is None:
return
async with self.peer_manager.lock:
peer = self.peer_manager.peers.get(peer_id)
peer_ws = getattr(peer, "ws", None) if peer is not None else None
if peer_ws is not None and not peer_ws.closed:
try:
await asyncio.wait_for(peer_ws.close(code=code, message=message), timeout=2.0)
except Exception:
passparamselfparampeer_idstrparamcodeintparammessagebytesReturns
Nonefuncreconcile_webrtc_peers(self) -> NoneToken-update reconciliation for LIVE WebRTC peers (websockets reconcile_clients parity): a revoked or role-changed token closes the peer (signaling verdict 4002 + pipeline stop); an mk-token handoff pushes the new input verdict to every surviving peer over its data channel, controllers included (a handoff strips their authority too). Per-message input authority already reads the live store — this covers the media stream and the client-side grant, which otherwise persist until the peer disconnects itself. A slot-only change keeps the peer but is pushed as a role_update (websockets ROLE_UPDATE parity): the gamepad slot mapping lives client-side and would silently desync.
Source Code
async def reconcile_webrtc_peers(self) -> None:
"""Token-update reconciliation for LIVE WebRTC peers (websockets
reconcile_clients parity): a revoked or role-changed token closes the
peer (signaling verdict 4002 + pipeline stop); an mk-token handoff
pushes the new input verdict to every surviving peer over its data
channel, controllers included (a handoff strips their authority too).
Per-message input authority already reads the live store — this covers
the media stream and the client-side grant, which otherwise persist
until the peer disconnects itself. A slot-only change keeps the peer
but is pushed as a role_update (websockets ROLE_UPDATE parity): the
gamepad slot mapping lives client-side and would silently desync."""
if self.rtc_app is None:
return
tokens, mk = current_session_tokens()
for peer_id, peer in list(self.rtc_app.peer_connections.items()):
token = peer.get("client_token")
if not token:
# Token-less peer: governed by its URL role only.
continue
ctype = peer.get("client_type")
role_now = "controller" if ctype == ClientType.CONTROLLER else "viewer"
new_perms = tokens.get(token)
if not new_perms or (new_perms.get("role") or "controller") != role_now:
reason = "Token revoked" if not new_perms else "Permissions changed significantly"
logger.info(f"Disconnecting WebRTC peer {peer_id}: {reason}")
await self._close_peer_signaling_ws(peer_id, 4002, reason.encode())
try:
await self.rtc_app.stop_rtc_connection(peer_id, role_now)
except Exception:
logger.warning(f"stop_rtc_connection failed for {peer_id}", exc_info=True)
continue
self.rtc_app._send_collab_state(peer.get("data_channel"), ctype, token)
new_slot = new_perms.get("slot")
if new_slot != peer.get("client_slot"):
peer["client_slot"] = new_slot
channel = peer.get("data_channel")
if channel is not None and channel.readyState == "open":
try:
verdict = json.dumps({"role": role_now, "slot": new_slot})
channel.send(json.dumps(
{"type": "system", "data": {"action": f"role_update,{verdict}"}}))
except Exception:
logger.debug("role_update send failed (channel closing)", exc_info=True)paramselfReturns
Nonefunchandle_consumers_changed(self, display_id) -> NoneA peer joined or left a display's consumer set: re-evaluate the all-paused stop in both directions — a departing unpaused peer cannot leave the capture running for hidden-only consumers, and a JOINING unpaused peer must re-open a capture the rule stopped (else a viewer arriving while every prior consumer is hidden gets a permanently black stream: a paused capture emits no RTP, so the browser never even PLIs). (A departing controller's pipeline is torn down elsewhere; both pause_screen_capture and resume_screen_capture no-op on a stopped pipeline.)
Source Code
async def handle_consumers_changed(self, display_id: str) -> None:
"""A peer joined or left a display's consumer set: re-evaluate the
all-paused stop in both directions — a departing unpaused peer cannot
leave the capture running for hidden-only consumers, and a JOINING
unpaused peer must re-open a capture the rule stopped (else a viewer
arriving while every prior consumer is hidden gets a permanently black
stream: a paused capture emits no RTP, so the browser never even PLIs).
(A departing controller's pipeline is torn down elsewhere; both
pause_screen_capture and resume_screen_capture no-op on a stopped
pipeline.)"""
display_id = display_id or "primary"
pipeline = self.display_pipelines.get(display_id)
if pipeline is None:
return
consumers = self._display_consumers(display_id)
if not consumers:
return
if all(p.get("video_paused", False) for p in consumers):
if await pipeline.pause_screen_capture():
logger.info(
f"All remaining consumers of display '{display_id}' are paused; capture stopped."
)
else:
# A live capture already flows RTP; the joining browser's PLI resyncs it.
await self._resume_display_capture(display_id, pipeline, "joining consumer")paramselfparamdisplay_idstrReturns
Nonefunc_resume_display_capture(self, display_id, pipeline, why, idr_always=False) -> NoneResume a capture stopped by the all-consumers-paused rule (no-op on a live or fully-stopped pipeline) and request the resync IDR — always, or only when the resume actually restarted the capture.
Source Code
async def _resume_display_capture(self, display_id: str, pipeline: MediaPipelinePixel,
why: str, idr_always: bool = False) -> None:
"""Resume a capture stopped by the all-consumers-paused rule (no-op on a
live or fully-stopped pipeline) and request the resync IDR — always, or
only when the resume actually restarted the capture."""
restarted = False
try:
restarted = await pipeline.resume_screen_capture()
if restarted:
logger.info(f"Display '{display_id}': capture restarted ({why}).")
except Exception as e:
logger.error(f"Display '{display_id}': capture resume failed ({why}): {e}")
if idr_always or restarted:
await self.request_idr_for_display(display_id)paramselfparamdisplay_idstrparampipelineMediaPipelinePixelparamwhystrparamidr_alwaysbool= FalseReturns
Nonefunc_drop_wayland_secondary(self, did, reason) -> NoneRefuse a secondary display the compositor cannot realize: unregister it, stop its pipeline, destroy its output, and close its peers with a fatal signaling verdict (4000) so the client does not re-register in a loop — the Wayland mirror of the X11 unrealizable-extension drop. The primary output, which may sit at a 'left'/'up' offset for the arrangement this display anchored, goes back to the origin. Caller holds _display_lock.
Source Code
async def _drop_wayland_secondary(self, did: str, reason: str) -> None:
"""Refuse a secondary display the compositor cannot realize: unregister
it, stop its pipeline, destroy its output, and close its peers with a
fatal signaling verdict (4000) so the client does not re-register in a
loop — the Wayland mirror of the X11 unrealizable-extension drop. The
primary output, which may sit at a 'left'/'up' offset for the
arrangement this display anchored, goes back to the origin. Caller
holds _display_lock."""
pipeline = self.display_pipelines.pop(did, None)
self.display_clients.pop(did, None)
self.display_layouts.pop(did, None)
primary_layout = self.display_layouts.get("primary")
if primary_layout:
# Input offsets follow the layout; the primary re-anchors at the origin.
primary_layout["x"], primary_layout["y"] = 0, 0
if pipeline is not None:
await pipeline.stop_media_pipeline()
module = self._wayland_capture_handle()
if module is not None:
try:
await asyncio.to_thread(module.destroy_output, wayland_output_id(did))
except Exception:
pass
await wayland_reposition_primary(module, 0, 0)
if self.peer_manager is not None:
async with self.peer_manager.lock:
doomed = [
p.ws for p in self.peer_manager.peers.values()
if p.peer_type != "server"
and getattr(p, "display_id", "primary") == did
and getattr(p, "ws", None) is not None and not p.ws.closed
]
for peer_ws in doomed:
try:
await asyncio.wait_for(
peer_ws.close(code=4000, message=reason.encode("utf-8")),
timeout=2.0,
)
except Exception:
pass
if self.rtc_app is not None:
await self.rtc_app.close_display_peers(did)
logger.error(f"Secondary display '{did}' dropped on Wayland: {reason}")paramselfparamdidstrparamreasonstrReturns
Nonefunc_drop_x11_secondary(self, did, reason) -> NoneRefuse a secondary display the X server cannot fit in its root: unregister it, stop its pipeline, and close its peers with a fatal signaling verdict (4000) so the client does not reload and re-register in a loop — the X11 mirror of the compositor-side drop, and the websockets engine's KILL parity. Caller holds _display_lock.
Unregistering inline rather than through stop_display_media, which would re-acquire the lock.
Source Code
async def _drop_x11_secondary(self, did: str, reason: str) -> None:
"""Refuse a secondary display the X server cannot fit in its root:
unregister it, stop its pipeline, and close its peers with a fatal
signaling verdict (4000) so the client does not reload and re-register
in a loop — the X11 mirror of the compositor-side drop, and the
websockets engine's KILL parity. Caller holds _display_lock.
Unregistering inline rather than through stop_display_media, which
would re-acquire the lock.
"""
pipeline = self.display_pipelines.pop(did, None)
self.display_clients.pop(did, None)
self.display_layouts.pop(did, None)
primary_layout = self.display_layouts.get("primary")
if primary_layout:
# Input offsets follow the layout; the primary is back at the origin.
primary_layout["x"], primary_layout["y"] = 0, 0
if pipeline is not None:
await pipeline.stop_media_pipeline()
if self.peer_manager is not None:
async with self.peer_manager.lock:
doomed = [
p.ws for p in self.peer_manager.peers.values()
if p.peer_type != "server"
and getattr(p, "display_id", "primary") == did
and getattr(p, "ws", None) is not None and not p.ws.closed
]
for peer_ws in doomed:
try:
await asyncio.wait_for(
peer_ws.close(code=4000, message=reason.encode("utf-8")),
timeout=2.0,
)
except Exception:
pass
if self.rtc_app is not None:
await self.rtc_app.close_display_peers(did)
logger.error(
f"Extended layout for '{did}' is unrealizable; the secondary display stays "
f"disabled. {reason}"
)paramselfparamdidstrparamreasonstrReturns
Nonefuncreconfigure_displays(self) -> NoneLay the extended desktop out for the connected displays and point each display's capture at its region — the WR counterpart of the websockets reconfigure engine, for the primary plus one secondary display. On Wayland the layout realizes as compositor outputs instead of xrandr monitors.
With no laid-out secondary the plain full-screen capture is restored: the secondary's compositor output goes away (its windows relocate to the primary) and the primary re-anchors at the origin, or on X11 the stale selkies-* monitors are cleared and the framebuffer shrunk; a secondary registered without dimensions yet (or whose layout was unrealizable) sends the primary's diverted resolution request straight to the real display. Otherwise the primary size comes from its last layout-path request, else its pipeline dimensions (or the live screen resolution on X11), passes the auto-resize feedback clamp, and the dual layout is realized. A layout the server cannot realize takes the secondary down with it, since an input channel left connected would keep feeding a display with no laid-out region. A new secondary's pipeline is built from its own settings (its SETTINGS arrive before its first resize lays it out), falling back per key to the service defaults, and a bring-up failure drops it so the next reconfigure retries instead of finding a dead pipeline.
Source Code
async def reconfigure_displays(self) -> None:
"""Lay the extended desktop out for the connected displays and point each
display's capture at its region — the WR counterpart of the websockets
reconfigure engine, for the primary plus one secondary display. On
Wayland the layout realizes as compositor outputs instead of xrandr
monitors.
With no laid-out secondary the plain full-screen capture is restored:
the secondary's compositor output goes away (its windows relocate to
the primary) and the primary re-anchors at the origin, or on X11 the
stale selkies-* monitors are cleared and the framebuffer shrunk; a
secondary registered without dimensions yet (or whose layout was
unrealizable) sends the primary's diverted resolution request straight
to the real display. Otherwise the primary size comes from its last
layout-path request, else its pipeline dimensions (or the live screen
resolution on X11), passes the auto-resize feedback clamp, and the
dual layout is realized. A layout the server cannot realize takes the
secondary down with it, since an input channel left connected would
keep feeding a display with no laid-out region. A new secondary's
pipeline is built from its own settings (its SETTINGS arrive before
its first resize lays it out), falling back per key to the service
defaults, and a bring-up failure drops it so the next reconfigure
retries instead of finding a dead pipeline.
"""
async with self._display_lock:
secondary = next(
((did, info) for did, info in self.display_clients.items()
if did != "primary" and info.get("width", 0) > 0 and info.get("height", 0) > 0),
None,
)
if secondary is None:
if self.display_layouts:
self.display_layouts = {}
p_w, p_h = self._primary_dims or (self.media_pipeline.width, self.media_pipeline.height)
# The pipeline dimensions are the primary's authority again.
self._primary_dims = None
if IS_WAYLAND:
await self._destroy_wayland_secondary_outputs()
await wayland_reposition_primary(self._wayland_capture_handle(), 0, 0)
self.media_pipeline.capture_region = None
if (self.media_pipeline.width, self.media_pipeline.height) != (p_w, p_h):
self.media_pipeline.width, self.media_pipeline.height = p_w, p_h
if self.media_pipeline.is_media_pipeline_running():
await self.media_pipeline.restart_screen_capture()
self._broadcast_display_config()
return
# Before the shrink, so no monitor lingers outside the framebuffer.
await clear_selkies_monitors()
realized = await resize_display(f"{p_w}x{p_h}")
if realized:
p_w, p_h = realized
self.media_pipeline.capture_region = None
self.media_pipeline.width, self.media_pipeline.height = p_w, p_h
if self.media_pipeline.is_media_pipeline_running():
await self.media_pipeline.restart_screen_capture()
self._push_x11_layout_geometry({"primary": {"w": p_w, "h": p_h}})
elif self._primary_dims is not None:
await self._resize_primary_display(
"{}x{}".format(*self._primary_dims)
)
self._broadcast_display_config()
return
did, info = secondary
await self._wm_swap.ensure_for(len(self.display_clients), IS_WAYLAND)
if self._primary_dims is None:
p_w, p_h = self.media_pipeline.width, self.media_pipeline.height
if IS_WAYLAND:
# The compositor rejects overlapping outputs, so the offset comes
# from its realized geometry, not a capture size trailing a resize.
realized = await self._realized_wayland_dims("primary")
if realized is not None:
p_w, p_h = realized
if p_w <= 0 or p_h <= 0:
if IS_WAYLAND:
logger.error("Cannot determine primary display size; aborting layout.")
return
curr, _, _, _, _ = await get_new_res("1x1")
try:
p_w, p_h = (int(v) for v in curr.lower().split("x"))
except (ValueError, AttributeError):
logger.error("Cannot determine primary display size; aborting layout.")
return
self._primary_dims = (p_w, p_h)
position = info.get("position", "right")
self._primary_dims = clamp_primary_feedback(
self._primary_dims, self.display_layouts, position
)
layouts, total_w, total_h = compute_dual_layout(
self._primary_dims, (info["width"], info["height"]), position,
)
layouts[did] = layouts.pop("secondary")
if IS_WAYLAND:
if not await self._apply_wayland_extension(did, layouts):
await self._drop_wayland_secondary(
did, "The compositor cannot create an output for this display."
)
return
else:
# apply_extended_layout fits `layouts` to the root really produced:
# kept displays may shrink and an unplaceable one disappears from it.
requested = {d: (r["w"], r["h"]) for d, r in layouts.items()}
if (not await apply_extended_layout(layouts, total_w, total_h)
or did not in layouts):
await self._drop_x11_secondary(
did, "The X server cannot extend the desktop to fit this display."
)
return
# Write back only what the root cut down: a rectangle the layout
# itself derived must not feed the next pass as its own input.
for fitted_id, fitted in layouts.items():
if (fitted["w"], fitted["h"]) == requested[fitted_id]:
continue
client = self.display_clients.get(fitted_id)
if client is not None:
client["width"], client["height"] = fitted["w"], fitted["h"]
if fitted_id == "primary":
self._primary_dims = (fitted["w"], fitted["h"])
self.display_layouts = layouts
p = layouts["primary"]
if IS_WAYLAND:
# _apply_wayland_extension already moved the primary output; only a
# size change needs the capture restart that resizes it.
if (self.media_pipeline.width, self.media_pipeline.height) != (p["w"], p["h"]):
await self.media_pipeline.update_capture_region(p["x"], p["y"], p["w"], p["h"])
await self._push_wayland_realized_geometry("primary", self.media_pipeline)
else:
await self.media_pipeline.update_capture_region(p["x"], p["y"], p["w"], p["h"])
s = layouts[did]
pipeline = self.display_pipelines.get(did)
if pipeline is None:
setting = lambda key: self._display_setting(did, key)
pipeline = MediaPipelinePixel(
async_event_loop=asyncio.get_running_loop(),
encoder=str(setting("encoder")),
framerate=int(setting("framerate")),
video_bitrate=int(setting("video_bitrate")),
audio_enabled=False,
width=s["w"],
height=s["h"],
crf=int(setting("video_crf")),
video_fullcolor=bool(setting("video_fullcolor")),
use_cpu=bool(setting("use_cpu")),
video_streaming_mode=bool(setting("video_streaming_mode")),
use_paint_over_quality=bool(setting("use_paint_over_quality")),
video_paintover_crf=int(setting("video_paintover_crf")),
video_paintover_burst_frames=int(setting("video_paintover_burst_frames")),
display_id=did,
capture_region=(s["x"], s["y"]),
)
if self.args.enable_rate_control:
pipeline.rc_mode = RateControlMode(setting("rate_control_mode"))
else:
pipeline.rc_mode = self.media_pipeline.rc_mode
# The scale ladder runs for this display's own screen rather than
# copying whatever the primary was left with (a no-op field on X11).
if IS_WAYLAND and self.input_handler is not None:
pipeline.scale = await self.input_handler.realize_wayland_dpi(
getattr(self, "_last_applied_dpi", None)
or getattr(settings, "scaling_dpi", 96) or 96,
session_screen_index(did), (s["w"], s["h"]))
else:
pipeline.scale = getattr(self.media_pipeline, "scale", 1.0)
# The native-cursor toggle is global across displays.
pipeline.capture_cursor = self.media_pipeline.capture_cursor
pipeline.produce_data = (
lambda buf, pts, kind, _did=did: self.rtc_app.consume_data(buf, pts, kind, _did)
)
# pixelflux's cursor-callback slot is process-global (last registration
# wins), so every display must route cursors into the same sink.
pipeline.on_cursor_data = self.media_pipeline.on_cursor_data
pipeline.get_cursor_size_cap = self.media_pipeline.get_cursor_size_cap
self.display_pipelines[did] = pipeline
try:
await pipeline.start_media_pipeline()
except Exception as e:
logger.error(f"Secondary display '{did}' pipeline failed to start ({e}); will retry on next reconfigure.")
self.display_pipelines.pop(did, None)
if IS_WAYLAND:
await self._drop_wayland_secondary(
did, "The compositor could not start a capture for this display."
)
return
if IS_WAYLAND and not await self._wayland_capture_live(did, pipeline):
last_error = self._wayland_capture_last_error(pipeline, did)
await self._drop_wayland_secondary(
did,
last_error or "The compositor could not start a capture for this "
"display (encoder session or GPU resources exhausted).",
)
return
if IS_WAYLAND:
await self._push_wayland_realized_geometry(did, pipeline)
logger.info(f"Secondary display '{did}' pipeline started at {s}")
else:
# A Wayland restart is a full capture reconfigure: skip it when the
# region is unchanged and the capture is verifiably live.
unchanged = IS_WAYLAND and (
(pipeline.width, pipeline.height) == (s["w"], s["h"])
and pipeline.capture_region == (s["x"], s["y"])
and await self._wayland_capture_live(did, pipeline)
)
if not unchanged:
await pipeline.update_capture_region(s["x"], s["y"], s["w"], s["h"])
if IS_WAYLAND:
await self._push_wayland_realized_geometry(did, pipeline)
if not IS_WAYLAND:
self._push_x11_layout_geometry(layouts)
self._broadcast_display_config()paramselfReturns
Nonefunc_push_x11_layout_geometry(self, layouts) -> NoneTell each laid-out display's pages the size the X11 layout pass gave it (websockets parity: the engine broadcasts every display's realized resolution after each pass). The root may not fit the request, and a page whose display the server cut down would otherwise keep its requested size in its manual-mode bookkeeping and re-assert it forever; idempotent on the client for an unchanged size. The Wayland branches push through the compositor's realized geometry instead.
Source Code
def _push_x11_layout_geometry(self, layouts: Dict[str, Dict[str, int]]) -> None:
"""Tell each laid-out display's pages the size the X11 layout pass gave
it (websockets parity: the engine broadcasts every display's realized
resolution after each pass). The root may not fit the request, and a
page whose display the server cut down would otherwise keep its
requested size in its manual-mode bookkeeping and re-assert it
forever; idempotent on the client for an unchanged size. The Wayland
branches push through the compositor's realized geometry instead."""
if self.rtc_app is None:
return
for did, rect in layouts.items():
w, h = int(rect.get("w", 0)), int(rect.get("h", 0))
if w > 0 and h > 0:
self.rtc_app.send_remote_resolution(f"{w}x{h}", did)paramselfparamlayoutsDict[str, Dict[str, int]]Returns
Nonefunc_broadcast_display_config(self) -> NoneTell every connected page which displays are attached (websockets parity: the primary page forces browser-cursor rendering while a secondary is connected, keyed off this broadcast).
Source Code
def _broadcast_display_config(self) -> None:
"""Tell every connected page which displays are attached (websockets
parity: the primary page forces browser-cursor rendering while a
secondary is connected, keyed off this broadcast)."""
if not self.rtc_app:
return
displays = ["primary"] + [d for d in self.display_clients.keys() if d != "primary"]
self.rtc_app.send_media_data_over_channel(
"display_config_update", {"displays": displays}
)paramselfReturns
Nonefunc_update_cursor_cap(self, dpi_value) -> NoneScale the remote-cursor delivery cap with the DPI and push it to every running capture (pixelflux applies cursor_size_cap live through update_tunables; later (re)starts read it through CaptureSettings).
Source Code
def _update_cursor_cap(self, dpi_value: float) -> None:
"""Scale the remote-cursor delivery cap with the DPI and push it to
every running capture (pixelflux applies cursor_size_cap live through
update_tunables; later (re)starts read it through CaptureSettings)."""
ih = self.input_handler
if ih is None:
return
try:
ih.system_dpi = float(dpi_value)
ih.cursor_size_cap = int(ih.max_cursor_size * float(dpi_value) / 96.0)
except Exception as e:
logger.debug(f"cursor cap update skipped: {e}")
return
updated = 0
for did, pipeline in list(self.display_pipelines.items()):
module = getattr(pipeline, "capture_module", None)
if pipeline is None or module is None or not pipeline.is_media_pipeline_running():
continue
try:
module.update_tunables(pipeline.generate_capture_settings())
updated += 1
except Exception as e:
logger.debug(f"Live cursor cap update skipped for '{did}': {e}")
logger.info(f"Cursor size cap {ih.cursor_size_cap}px for DPI {dpi_value} "
f"({updated} live capture(s) updated).")paramselfparamdpi_valuefloatReturns
Nonefunchandle_scaling(self, dpi_value) -> NoneApply a client DPI sync to the desktop (X11 xrdb/cursor themes) or run the per-display Wayland scale ladder.
Fractional DPI is legal on the shared verb (websockets parity); the desktop DPI property itself is integral. Bounded by the declared scaling_dpi span so a client cannot drive xrdb — and, with it, the cursor size and the Wayland compositor scale — to an arbitrary value. An operator-set DPI (CLI/env) governs the desktop and is never clobbered by a client sync. Idempotent: the dashboard and the core each re-assert their DPI on settings broadcasts, and every apply churns xrdb, xsettingsd SIGHUP and cursor themes. On Wayland the DPI runs the scale ladder per display: the session compositor scales the screen backing it, and only what it leaves becomes that display's capture scale, whose change restarts the capture (the WS path threads the same scale through CaptureSettings).
Source Code
async def handle_scaling(self, dpi_value: float) -> None:
"""Apply a client DPI sync to the desktop (X11 xrdb/cursor themes) or
run the per-display Wayland scale ladder.
Fractional DPI is legal on the shared verb (websockets parity); the
desktop DPI property itself is integral. Bounded by the declared
scaling_dpi span so a client cannot drive xrdb — and, with it, the
cursor size and the Wayland compositor scale — to an arbitrary value.
An operator-set DPI (CLI/env) governs the desktop and is never
clobbered by a client sync. Idempotent: the dashboard and the core
each re-assert their DPI on settings broadcasts, and every apply churns
xrdb, xsettingsd SIGHUP and cursor themes. On Wayland the DPI runs the
scale ladder per display: the session compositor scales the screen
backing it, and only what it leaves becomes that display's capture
scale, whose change restarts the capture (the WS path threads the same
scale through CaptureSettings).
"""
try:
dpi_value = min(SCALING_DPI_MAX,
max(SCALING_DPI_MIN, int(round(float(dpi_value)))))
except (TypeError, ValueError, OverflowError):
logger.warning(f"Ignoring malformed DPI sync: {dpi_value!r}")
return
if settings._overridden.get("scaling_dpi", False):
logger.info("Ignoring client DPI sync: scaling_dpi is operator-overridden.")
return
if getattr(self, "_last_applied_dpi", None) == int(dpi_value):
logger.debug(f"DPI already {int(dpi_value)}; skipping re-apply.")
return
if not IS_WAYLAND:
if await set_dpi(int(dpi_value)):
self._last_applied_dpi = int(dpi_value)
logger.info(f"Successfully set DPI to {dpi_value}")
else:
logger.error(f"Failed to set DPI to {dpi_value}")
# Before the Wayland restarts below, which read the cap through
# CaptureSettings; a compositor that absorbs the scale restarts nothing.
self._update_cursor_cap(dpi_value)
if IS_WAYLAND:
self._last_applied_dpi = int(dpi_value)
for did, pipeline in list(self.display_pipelines.items()):
if pipeline is None:
continue
new_scale = (await self.input_handler.realize_wayland_dpi(
dpi_value, session_screen_index(did),
(pipeline.width, pipeline.height))
if self.input_handler else float(dpi_value) / 96.0)
if pipeline.scale == new_scale:
continue
pipeline.scale = new_scale
if pipeline.is_media_pipeline_running():
await pipeline.restart_screen_capture()
await self._push_wayland_realized_geometry(did, pipeline)
await self._apply_wayland_cursor_size(dpi_value)
return
if CURSOR_SIZE is None:
# Auto: only the DPI itself is applied.
return
new_cursor_size = cursor_size_for_dpi(dpi_value, CURSOR_SIZE)
logger.info(
f"Attempting to set cursor size to: {new_cursor_size} (based on DPI {dpi_value})"
)
if await set_cursor_size(new_cursor_size):
logger.info(f"Successfully set cursor size to {new_cursor_size}")
else:
logger.error(f"Failed to set cursor size to {new_cursor_size}")paramselfparamdpi_valuefloatReturns
Nonefunchandle_system_monitor(self, t) -> NoneSystem-monitor tick: push CPU/memory stats and a ping to clients, and recover the audio capture if its worker died since the last tick.
pcmflux reports a clean start while its worker is still coming up, so a
device that dies during bring-up (or later) only shows through
last_error; polling it here cycles the audio capture. Audio lives on
the primary pipeline; the call no-ops on the audio-less secondaries.
Source Code
async def handle_system_monitor(self, t: float) -> None:
"""System-monitor tick: push CPU/memory stats and a ping to clients,
and recover the audio capture if its worker died since the last tick.
pcmflux reports a clean start while its worker is still coming up, so a
device that dies during bring-up (or later) only shows through
`last_error`; polling it here cycles the audio capture. Audio lives on
the primary pipeline; the call no-ops on the audio-less secondaries.
"""
if self.input_handler and self.rtc_app and self.system_monitor:
self.input_handler.ping_start = t
self.rtc_app.send_system_stats(
self.system_monitor.cpu_percent,
self.system_monitor.mem_total,
self.system_monitor.mem_used,
)
self.rtc_app.send_ping(t)
if self.media_pipeline is not None:
try:
await self.media_pipeline.recover_audio_if_failed()
except Exception as e:
logger.debug(f"audio health poll skipped: {e}")paramselfparamtfloatReturns
Nonefunchandle_gpu_stats(self, load, memory_total, memory_used) -> NoneGPU-monitor tick: push GPU stats to clients and into metrics.
Source Code
async def handle_gpu_stats(
self, load: float, memory_total: int, memory_used: int
) -> None:
"""GPU-monitor tick: push GPU stats to clients and into metrics."""
if self.rtc_app:
self.rtc_app.send_gpu_stats(load, memory_total, memory_used)
if self.metrics:
self.metrics.set_gpu_utilization(load * 100)paramselfparamloadfloatparammemory_totalintparammemory_usedintReturns
Nonefunc_seed_display_settings(self, entry) -> NoneGive a joining secondary display its own copy of every client-tunable video setting, taken from the args at join time (websockets parity: a display registers with a full seeded snapshot). Sharing the args instead would make a later primary change move what the secondary reports as its current value while its stream keeps running the old one, and the equality guards on its own controls would then compare against a value that display never ran.
Source Code
def _seed_display_settings(self, entry: Dict[str, Any]) -> None:
"""Give a joining secondary display its own copy of every client-tunable
video setting, taken from the args at join time (websockets parity: a
display registers with a full seeded snapshot). Sharing the args instead
would make a later primary change move what the secondary reports as its
current value while its stream keeps running the old one, and the equality
guards on its own controls would then compare against a value that display
never ran."""
for key in list(self._VIDEO_SETTING_APPLIERS) + ["force_aligned_resolution"]:
if key in entry:
continue
value = getattr(self.args, key, None)
if value is not None:
entry[key] = valueparamselfparamentryDict[str, Any]Returns
Nonefunc_display_setting(self, display_id, key) -> AnyA display's current value for a client-tunable setting: the primary reads the service args; a secondary reads its own stored overrides, falling back to the args it was seeded from (websockets model: each display's SETTINGS payload configures only that display's stream).
Source Code
def _display_setting(self, display_id: str, key: str) -> Any:
"""A display's current value for a client-tunable setting: the primary
reads the service args; a secondary reads its own stored overrides,
falling back to the args it was seeded from (websockets model: each
display's SETTINGS payload configures only that display's stream)."""
if display_id != "primary":
entry = self.display_clients.get(display_id)
if entry is not None and key in entry:
return entry[key]
return getattr(self.args, key, None)paramselfparamdisplay_idstrparamkeystrReturns
typing.Anyfunc_store_display_setting(self, display_id, key, value) -> NoneRecord a setting as the display's current value (args for the primary, the display's own entry for a secondary).
Source Code
def _store_display_setting(self, display_id: str, key: str, value: Any) -> None:
"""Record a setting as the display's current value (args for the
primary, the display's own entry for a secondary)."""
if display_id == "primary":
setattr(self.args, key, value)
else:
entry = self.display_clients.get(display_id)
if entry is not None:
entry[key] = valueparamselfparamdisplay_idstrparamkeystrparamvalueAnyReturns
Nonefunc_apply_display_setting(self, display_id, key, value) -> NoneStore one video setting as the display's current value and apply it to the display's live pipeline when it exists (a secondary that has not been laid out yet picks the stored value up at pipeline creation).
The primary's encoder is written through to the settings singleton:
transport services re-seed from it on a mode switch, so a client pick
must live there to survive the trip, and _encoder_client_set marks a
fresh pick during the webrtc leg, which outranks any stashed pre-clamp
value on the switch back. The RTCApp's global encoder is kept current
too: it is the default codec/munge choice for connections created
later, while secondaries resolve per display.
Source Code
async def _apply_display_setting(self, display_id: str, key: str, value: Any) -> None:
"""Store one video setting as the display's current value and apply it to
the display's live pipeline when it exists (a secondary that has not been
laid out yet picks the stored value up at pipeline creation).
The primary's encoder is written through to the settings singleton:
transport services re-seed from it on a mode switch, so a client pick
must live there to survive the trip, and `_encoder_client_set` marks a
fresh pick during the webrtc leg, which outranks any stashed pre-clamp
value on the switch back. The RTCApp's global encoder is kept current
too: it is the default codec/munge choice for connections created
later, while secondaries resolve per display.
"""
applier = self._VIDEO_SETTING_APPLIERS.get(key)
if applier is None:
return
self._store_display_setting(display_id, key, value)
pipeline = self.display_pipelines.get(display_id)
if pipeline is not None:
await applier(pipeline, value)
if key == "encoder" and display_id == "primary":
self.settings.encoder = str(value)
self.settings._encoder_client_set = True
if self.rtc_app:
self.rtc_app.encoder = str(value)paramselfparamdisplay_idstrparamkeystrparamvalueAnyReturns
Nonefunc_encoder_for_display(self, display_id) -> strSource Code
def _encoder_for_display(self, display_id: str) -> str:
return str(self._display_setting(display_id, "encoder") or self.args.encoder)paramselfparamdisplay_idstrReturns
strfunc_fullcolor_for_display(self, display_id) -> boolSource Code
def _fullcolor_for_display(self, display_id: str) -> bool:
return bool(self._display_setting(display_id, "video_fullcolor"))paramselfparamdisplay_idstrReturns
boolfunc_use_cpu_for_display(self, display_id) -> boolSource Code
def _use_cpu_for_display(self, display_id: str) -> bool:
return bool(self._display_setting(display_id, "use_cpu"))paramselfparamdisplay_idstrReturns
boolfunchandle_update_settings(self, settings_json, display_id='primary') -> NoneApply a client SETTINGS payload to the display that sent it.
Every allowed entry needs server-side backing: a live setter dispatched
below, or state the server reads later (the manual-resolution trio feeds
the start-time resize and resolution policy; the live resize itself
rides the r, input message). Video keys apply to the SENDING display
only (websockets model); audio and the clipboard policy are
stream-global whichever display asserts them. A scaling_dpi in the
primary's payload runs handle_scaling (websockets parity: the client
seeds its DPR-derived value into its very first payload, so the right
scale lands on the first sync rather than the dashboard's later
correction); a secondary's displayPosition may move it to any side
of the primary after joining.
Source Code
async def handle_update_settings(
self, settings_json: Dict[str, Any], display_id: str = "primary"
) -> None:
"""Apply a client SETTINGS payload to the display that sent it.
Every allowed entry needs server-side backing: a live setter dispatched
below, or state the server reads later (the manual-resolution trio feeds
the start-time resize and resolution policy; the live resize itself
rides the `r,` input message). Video keys apply to the SENDING display
only (websockets model); audio and the clipboard policy are
stream-global whichever display asserts them. A `scaling_dpi` in the
primary's payload runs `handle_scaling` (websockets parity: the client
seeds its DPR-derived value into its very first payload, so the right
scale lands on the first sync rather than the dashboard's later
correction); a secondary's `displayPosition` may move it to any side
of the primary after joining.
"""
settings_allowed_to_update = [
"rate_control_mode",
"video_crf",
"video_bitrate",
"audio_bitrate",
"framerate",
"use_cpu",
"enable_binary_clipboard",
"manual_resolution",
"manual_width",
"manual_height",
"force_aligned_resolution",
"encoder",
"video_fullcolor",
"video_streaming_mode",
"use_paint_over_quality",
"video_paintover_crf",
"video_paintover_burst_frames",
]
# Startup resolution policy, not a per-stream tunable.
primary_only_keys = ("manual_resolution", "manual_width", "manual_height")
display_id = display_id or "primary"
if display_id != "primary" and display_id not in self.display_clients:
logger.warning(
f"Ignoring settings for unknown display '{display_id}' (not connected)."
)
return
# Seat-global hint: base-layout push on Wayland, informational on X11.
kb_layout = settings_json.get("keyboardLayout")
if kb_layout and self.input_handler is not None:
await self.input_handler.apply_client_keyboard_layout(kb_layout)
dpi_val = settings_json.get("scaling_dpi")
if dpi_val is not None and display_id == "primary":
try:
await self.handle_scaling(float(dpi_val))
except (TypeError, ValueError):
logger.warning(f"Ignoring malformed scaling_dpi in SETTINGS: {dpi_val!r}")
def sanitize_value(name: str, client_value: Any) -> Any:
"""One-transport wrapper over the shared sanitizer (settings.py)."""
return sanitize_client_setting(name, client_value, self.settings, logger)
new_position = settings_json.get("displayPosition")
if new_position is not None and display_id != "primary":
new_position = str(new_position)
if new_position not in ("right", "left", "up", "down"):
logger.warning(f"Ignoring invalid displayPosition from '{display_id}': {new_position!r}")
else:
entry = self.display_clients.get(display_id)
if entry is not None and entry.get("position", "right") != new_position:
entry["position"] = new_position
await self.reconfigure_displays()
for key in settings_allowed_to_update:
client_value = settings_json.get(key)
if client_value is None:
continue
if key == "rate_control_mode" and self.args.enable_rate_control is False:
logger.debug(
f"Server has rate control disabled. Ignoring update for '{key}'."
)
continue
if key in primary_only_keys and display_id != "primary":
continue
if getattr(self.args, key, None) is None:
logger.warning(f"Received unknown setting '{key}' from client")
continue
current_value = self._display_setting(display_id, key)
sanitized_value = sanitize_value(key, client_value)
if sanitized_value is None or sanitized_value == current_value:
continue
if key == "audio_bitrate":
# Audio lives only on the primary pipeline.
if self.media_pipeline:
await self.media_pipeline.set_audio_bitrate(int(sanitized_value))
setattr(self.args, key, sanitized_value)
elif key == "enable_binary_clipboard":
await self.input_handler.update_binary_clipboard_setting(sanitized_value)
setattr(self.args, key, sanitized_value)
elif key in self._VIDEO_SETTING_APPLIERS:
await self._apply_display_setting(display_id, key, sanitized_value)
else:
# No live setter: stored for the resize paths / startup to read.
self._store_display_setting(display_id, key, sanitized_value)
logger.debug(
f"Updated setting '{key}' for display '{display_id}' from {current_value} to {sanitized_value}"
)paramselfparamsettings_jsonDict[str, Any]paramdisplay_idstr= 'primary'Returns
Nonefuncmon_rtc_config(self, stun_servers, turn_servers, rtc_config) -> NoneMonitor callback: fan a refreshed RTC config out to the signaling server (for clients) and the RTC app (for its own ICE agents).
Source Code
def mon_rtc_config(
self, stun_servers: List[str], turn_servers: List[str], rtc_config: Any
) -> None:
"""Monitor callback: fan a refreshed RTC config out to the signaling
server (for clients) and the RTC app (for its own ICE agents)."""
if self.peer_manager:
logger.debug("updating signaling server RTC config")
self.peer_manager.set_rtc_config(rtc_config)
if self.rtc_app:
logger.debug("updating STUN/TURN servers in RTC app")
self.rtc_app.update_rtc_config(stun_servers, turn_servers)paramselfparamstun_serversList[str]paramturn_serversList[str]paramrtc_configAnyReturns
Nonefunc_ensure_pacer(self, pc, peer, display_id) -> Optional[Any]Ensure the per-transport packet pacer is enabled/configured (called from the congestion loop; idempotent and cheap).
Encoder ceiling: the display's configured video bitrate, CBR or not.
The shared DTLS transport is reachable via pc.sctp only once the
data-channel m-line is negotiated, while media (and TWCC estimates)
can flow before that, so any transceiver's sender transport — the same
shared RTCDtlsTransport — serves as the fallback. The IDR floor is
bootstrapped from the session-start keyframe: on a late attach, waiting
for the next natural IDR would start it at 0 and reset on the first
real burst.
Source Code
def _ensure_pacer(self, pc: Any, peer: Dict[str, Any], display_id: str) -> Optional[Any]:
"""Ensure the per-transport packet pacer is enabled/configured (called
from the congestion loop; idempotent and cheap).
Encoder ceiling: the display's configured video bitrate, CBR or not.
The shared DTLS transport is reachable via `pc.sctp` only once the
data-channel m-line is negotiated, while media (and TWCC estimates)
can flow before that, so any transceiver's sender transport — the same
shared RTCDtlsTransport — serves as the fallback. The IDR floor is
bootstrapped from the session-start keyframe: on a late attach, waiting
for the next natural IDR would start it at 0 and reset on the first
real burst.
Returns:
The DTLS transport (so callers can snapshot its pacer), or None
when it is not available yet.
"""
transport = getattr(getattr(pc, "sctp", None), "transport", None)
if transport is None:
for tr in pc.getTransceivers() or []:
transport = getattr(getattr(tr, "sender", None), "transport", None)
if transport is not None:
break
if transport is None:
return None
lo_kbps, hi_kbps = settings.video_bitrate
enc_kbps = float(self._display_setting(display_id, "video_bitrate") or hi_kbps)
enc_bps = int(max(lo_kbps, min(hi_kbps, enc_kbps)) * 1000)
if not transport.pacer_enabled():
vsender = None
for tr in pc.getTransceivers() or []:
if getattr(tr, "kind", None) == "video":
vsender = tr.sender
break
transport.enable_pacer(
encoder_bps=enc_bps,
# Must hit the encoder pipeline: video rides the pre-encoded pack()
# path, where the sender's __force_keyframe flag is silently ignored.
request_keyframe=lambda did_=display_id: asyncio.ensure_future(
self.request_idr_for_display(did_)),
)
kf_bytes = getattr(vsender, "_keyframe_bytes", None)
if kf_bytes:
transport.note_video_keyframe(
kf_bytes, natural=getattr(vsender, "_keyframe_natural", True))
if transport.pacer_enabled():
logger.info(
f"WebRTC pacer enabled for display '{display_id}' "
f"(encoder ceiling {enc_bps // 1000} kbps)")
else:
transport.set_pacer_encoder_bps(enc_bps)
return transportparamselfparampcAnyparampeerDict[str, Any]paramdisplay_idstrReturns
typing.OptionalThe DTLS transport (so callers can snapshot its pacer), or None
func_congestion_control_loop(self) -> NoneGCC-style bitrate adaptation from transport-wide-cc receiver feedback: per display, follow the slowest of ITS peers' goodput estimates with headroom, back off multiplicatively on loss, and retarget that display's encoder within the allowed video_bitrate range — one display's congested link never steers another's stream. Only CBR mode has a target to steer.
The user-selected bitrate is the ceiling: control only backs off below it and recovers up to it. Clamping to the allowed range instead let a fast local segment ramp an 8000 kbps session to 80000+ kbps, saturating the real path (TURN/WAN) with queuing lag and loss-corrupted frames. Damage-gated encoders are application-limited, so measured goodput is merely what was sent (an idle screen reads ~0), not link capacity: it may lift the target when it shows real headroom but never drags it down; otherwise the target recovers multiplicatively toward the ceiling after a loss backoff. The pacer rides the same tick but is configured for every peer before the feedback gate, since it must run on links that never send transport-cc; this is its only configuration path, so one bad peer must never kill the loop.
Source Code
async def _congestion_control_loop(self) -> None:
"""GCC-style bitrate adaptation from transport-wide-cc receiver feedback:
per display, follow the slowest of ITS peers' goodput estimates with
headroom, back off multiplicatively on loss, and retarget that display's
encoder within the allowed video_bitrate range — one display's congested
link never steers another's stream. Only CBR mode has a target to steer.
The user-selected bitrate is the ceiling: control only backs off below
it and recovers up to it. Clamping to the allowed range instead let a
fast local segment ramp an 8000 kbps session to 80000+ kbps, saturating
the real path (TURN/WAN) with queuing lag and loss-corrupted frames.
Damage-gated encoders are application-limited, so measured goodput is
merely what was sent (an idle screen reads ~0), not link capacity: it
may lift the target when it shows real headroom but never drags it
down; otherwise the target recovers multiplicatively toward the
ceiling after a loss backoff. The pacer rides the same tick but is
configured for every peer before the feedback gate, since it must run
on links that never send transport-cc; this is its only configuration
path, so one bad peer must never kill the loop.
"""
lo_kbps, hi_kbps = settings.video_bitrate
logger.info(
f"Congestion control loop started (CBR only, range {lo_kbps}-{hi_kbps} kbps)."
)
# No getattr default: a misnamed setting must raise, not disable the pacer.
pacer_on = bool(settings.webrtc_pacer[0])
logger.info(f"WebRTC pacer setting: {'ON' if pacer_on else 'OFF'}.")
while True:
await asyncio.sleep(1.0)
rtc_app = self.rtc_app
if not rtc_app:
continue
per_display: Dict[str, Dict[str, Any]] = {}
for peer in rtc_app.peer_connections.values():
pc = peer.get("peer_conn")
did = peer.get("display_id", "primary") or "primary"
if pacer_on:
try:
dtls = self._ensure_pacer(pc, peer, did)
if self.metrics is not None and dtls is not None:
self.metrics.set_pacer_snapshot(did, dtls.pacer_snapshot())
except Exception:
logger.exception("_ensure_pacer failed (display %s)", did)
sctp = getattr(pc, "sctp", None)
estimate = getattr(getattr(sctp, "transport", None), "twcc_estimate", None)
if not estimate:
continue
bucket = per_display.setdefault(
did, {"goodputs": [], "worst_loss": 0.0})
if estimate.get("goodput_bps"):
bucket["goodputs"].append(estimate["goodput_bps"])
bucket["worst_loss"] = max(bucket["worst_loss"], estimate.get("loss_fraction", 0.0))
for did, bucket in per_display.items():
if not self.args.congestion_control:
continue
pipeline = self.display_pipelines.get(did)
if (
pipeline is None
or getattr(pipeline, "rc_mode", None) != RateControlMode.CBR
):
continue
goodputs, worst_loss = bucket["goodputs"], bucket["worst_loss"]
if not goodputs:
continue
current = float(pipeline.video_bitrate)
ceiling = float(self._display_setting(did, "video_bitrate") or hi_kbps)
ceiling = max(lo_kbps, min(hi_kbps, ceiling))
if worst_loss > 0.10:
target = current * 0.7
else:
# Goodput may lift the target, never drag it down (see docstring).
target = min(ceiling, max(current * 1.15, min(goodputs) * 0.85 / 1_000))
target = round(max(lo_kbps, min(ceiling, target)))
if target != round(current):
logger.info(
f"Congestion control[{did}]: video bitrate {current:.0f} -> {target:.0f} kbps "
f"(goodput {min(goodputs) / 1e3:.1f} kbps, loss {worst_loss:.1%})"
)
await pipeline.set_video_bitrate(target)paramselfReturns
Nonefuncstart_components(self) -> NoneStart the background tasks: input/clipboard/cursor workers, startup DPI and cursor size, the congestion/pacer loop, the monitors, the signaling client, and the configured TURN credential refreshers.
The configured desktop DPI is applied at startup so the first session
sees it before any client syncs its own (96 is unity, so nothing is
applied then); on Wayland it becomes the session compositor's output
scale, and with none up yet the input handler re-applies when it
adopts one. An explicit cursor size goes to the X server here
(handle_scaling re-derives it on DPI changes); Wayland gets it via
CaptureSettings. Logical monitors describe the layout of whichever
transport defined them, and a live switch leaves the previous one's
behind — a desktop keeps tiling against a stale rectangle — so on X11
they are cleared; this service defines its own
when a second display arrives and needs none for a single one.
Source Code
async def start_components(self) -> None:
"""Start the background tasks: input/clipboard/cursor workers, startup
DPI and cursor size, the congestion/pacer loop, the monitors, the
signaling client, and the configured TURN credential refreshers.
The configured desktop DPI is applied at startup so the first session
sees it before any client syncs its own (96 is unity, so nothing is
applied then); on Wayland it becomes the session compositor's output
scale, and with none up yet the input handler re-applies when it
adopts one. An explicit cursor size goes to the X server here
(`handle_scaling` re-derives it on DPI changes); Wayland gets it via
CaptureSettings. Logical monitors describe the layout of whichever
transport defined them, and a live switch leaves the previous one's
behind — a desktop keeps tiling against a stale rectangle — so on X11
they are cleared; this service defines its own
when a second display arrives and needs none for a single one.
"""
if self.input_handler:
self.tasks.append(asyncio.create_task(self.input_handler.connect()))
self.tasks.append(asyncio.create_task(self.input_handler.start_clipboard()))
self.tasks.append(asyncio.create_task(self.input_handler.probe_apps_runner()))
self.tasks.append(
asyncio.create_task(self.input_handler.start_cursor_monitor())
)
startup_dpi = int(float(getattr(settings, "scaling_dpi", "96") or 96))
if startup_dpi != 96:
if IS_WAYLAND:
if self.input_handler is not None:
await self.input_handler.realize_wayland_dpi(startup_dpi)
self._last_applied_dpi = startup_dpi
elif await set_dpi(startup_dpi):
self._last_applied_dpi = startup_dpi
if not IS_WAYLAND and settings.cursor_size > 0:
initial_dpi = float(getattr(settings, "scaling_dpi", "96"))
await set_cursor_size(cursor_size_for_dpi(initial_dpi, CURSOR_SIZE))
if not IS_WAYLAND:
await clear_selkies_monitors()
# The pacer rides the congestion loop's tick but is gated on its own flag.
if self.args.congestion_control or bool(settings.webrtc_pacer[0]):
self.tasks.append(asyncio.create_task(self._congestion_control_loop()))
if self.gpu_monitor:
self.gpu_monitor.start()
if self.system_monitor:
self.system_monitor.start()
if self.signaling_client:
self.signaling_client.start()
if self.monitoring_utils_used:
turn_rest_username = self.args.turn_rest_username.replace(":", "-")
if self.monitoring_utils_used.get("using_hmac_turn", False):
self.mon_hmac_turn = HMACRTCMonitor(
turn_host=self.args.turn_host,
turn_port=self.args.turn_port,
turn_shared_secret=self.args.turn_shared_secret,
turn_username=turn_rest_username,
turn_protocol=self.args.turn_protocol,
turn_tls=self.args.turn_tls,
stun_host=self.args.stun_host,
stun_port=self.args.stun_port,
period=60,
enabled=True,
)
self.mon_hmac_turn.on_rtc_config = self.mon_rtc_config
self.mon_hmac_turn.start()
if self.monitoring_utils_used.get("using_rest_api", False):
self.mon_rest_api = RESTRTCMonitor(
turn_rest_uri=self.args.turn_rest_uri,
turn_rest_username=turn_rest_username,
turn_rest_username_auth_header=self.args.turn_rest_username_auth_header,
turn_protocol=self.args.turn_protocol,
turn_rest_protocol_header=self.args.turn_rest_protocol_header,
turn_tls=self.args.turn_tls,
turn_rest_tls_header=self.args.turn_rest_tls_header,
turn_api_key=self.args.turn_rest_api_key,
period=60,
enabled=True,
)
self.mon_rest_api.on_rtc_config = self.mon_rtc_config
self.mon_rest_api.start()
if self.monitoring_utils_used.get("using_rtc_config_json", False):
self.mon_rtc_config_file = RTCConfigFileMonitor(
rtc_file=self.args.rtc_config_json, enabled=True
)
self.mon_rtc_config_file.on_rtc_config = self.mon_rtc_config
await self.mon_rtc_config_file.start()
if self.monitoring_utils_used.get("using_cloudflare_turn", False):
self.mon_cloudflare_turn = CloudflareRTCMonitor(
turn_token_id=self.args.cloudflare_turn_token_id,
api_token=self.args.cloudflare_turn_api_token,
enabled=True,
)
self.mon_cloudflare_turn.on_rtc_config = self.mon_rtc_config
self.mon_cloudflare_turn.start()paramselfReturns
Nonefuncshutdown(self) -> NoneGracefully shutdown all components.
Source Code
async def shutdown(self) -> None:
"""Gracefully shutdown all components."""
if self._shutdown_called:
logger.info("Shutdown already called, skipping")
return
self._shutdown_called = True
logger.info("Starting shutdown sequence")
self._cancel_primary_stop_grace()
for task in list(self.tasks):
try:
if not task.done():
task.cancel()
except Exception:
logger.exception("Error cancelling task during shutdown")
async def _await_with_timeout(
coro: Awaitable[Any], name: str, timeout: float = 3.0
) -> Optional[Any]:
"""Await one component's stop with a timeout, swallowing every
error so one wedged component cannot abort the whole shutdown."""
try:
return await asyncio.wait_for(coro, timeout=timeout)
except asyncio.TimeoutError:
logger.warning(
f"Timeout while waiting for {name} to stop (after {timeout}s)"
)
except asyncio.CancelledError:
logger.info(f"{name} was cancelled during shutdown")
except Exception as e:
logger.exception(f"Error while stopping {name}: {e}")
return None
try:
await asyncio.wait_for(
asyncio.gather(*self.tasks, return_exceptions=True), timeout=5.0
)
except asyncio.TimeoutError:
logger.warning(
"Some background tasks did not exit within timeout; continuing with component shutdown"
)
except Exception:
logger.exception("Unexpected error while awaiting background tasks")
stop_coros = []
if self.signaling_client:
stop_coros.append(
(
_await_with_timeout(
self.signaling_client.stop(), "signaling_client", 3.0
)
)
)
for display_id, pipeline in list(self.display_pipelines.items()):
stop_coros.append(
(
_await_with_timeout(
pipeline.stop_media_pipeline(), f"media_pipeline[{display_id}]", 3.0
)
)
)
if self.media_pipeline and "primary" not in self.display_pipelines:
stop_coros.append(
(
_await_with_timeout(
self.media_pipeline.stop_media_pipeline(), "media_pipeline", 3.0
)
)
)
if self.rtc_app:
stop_coros.append(
(
_await_with_timeout(
self.rtc_app.stop_all_rtc_connections(), "rtc_app", 3.0
)
)
)
stop_coros.append(
_await_with_timeout(
self._teardown_webrtc_virtual_mic(), "webrtc_virtual_mic", 3.0
)
)
if self.input_handler:
try:
self.input_handler.stop_clipboard()
except Exception:
logger.exception("Error stopping clipboard monitor")
try:
self.input_handler.stop_cursor_monitor()
except Exception:
logger.exception("Error stopping cursor monitor")
stop_coros.append(
(
_await_with_timeout(
self.input_handler.disconnect(), "input_handler.disconnect", 3.0
)
)
)
if self.gpu_monitor:
stop_coros.append(
(_await_with_timeout(self.gpu_monitor.stop(), "gpu_monitor", 2.0))
)
if self.system_monitor:
stop_coros.append(
(_await_with_timeout(self.system_monitor.stop(), "system_monitor", 2.0))
)
if self.mon_hmac_turn:
stop_coros.append(
(
_await_with_timeout(
self.mon_hmac_turn.stop(), "HMAC RTC Monitor", 2.0
)
)
)
if self.mon_rest_api:
stop_coros.append(
(_await_with_timeout(self.mon_rest_api.stop(), "REST RTC Monitor", 2.0))
)
if self.mon_rtc_config_file:
stop_coros.append(
(
_await_with_timeout(
self.mon_rtc_config_file.stop(), "RTC Config File Monitor", 2.0
)
)
)
if self.mon_cloudflare_turn:
stop_coros.append(
(
_await_with_timeout(
self.mon_cloudflare_turn.stop(), "Cloudflare TURN RTC Monitor", 2.0
)
)
)
if stop_coros:
try:
await asyncio.wait_for(
asyncio.gather(*stop_coros, return_exceptions=True), timeout=5
)
except asyncio.TimeoutError:
logger.warning(
"Component shutdown exceeded global timeout; some components may still be cleaning up"
)
except Exception:
logger.exception(
"Unexpected error during concurrent component shutdown"
)
if self.metrics:
try:
# unregister() drains the CSV executor with shutdown(wait=True).
await asyncio.to_thread(self.metrics.unregister)
except Exception as e:
logger.exception(f"Error unregistering metrics: {e}")
self.tasks.clear()
self.signaling_client = None
self.media_pipeline = None
self.rtc_app = None
self.input_handler = None
self.system_monitor = None
self.gpu_monitor = None
self.metrics = None
self.mon_hmac_turn = None
self.mon_rest_api = None
self.mon_rtc_config_file = None
logger.info("Shutdown complete")paramselfReturns
Nonefuncrun(self) -> NoneBring the service up and block until the shutdown event fires.
Source Code
async def run(self) -> None:
"""Bring the service up and block until the shutdown event fires."""
self._shutdown_called = False
try:
_install_webrtc_teardown_noise_filters(asyncio.get_running_loop())
await self.initialize_components()
self.setup_callbacks()
await self.start_components()
await self.shutdown_event.wait()
except asyncio.CancelledError:
logger.info("Received webrtc stream mode shutdown")
except Exception as e:
logger.critical(f"Fatal error: {e}", exc_info=True)
sys.exit(1)
finally:
await self.shutdown()paramselfReturns
Nonefuncstart(self) -> NoneSupervisor entry point: run the service until stop() is called.
Source Code
async def start(self) -> None:
"""Supervisor entry point: run the service until stop() is called."""
self.shutdown_event.clear()
await self.run()paramselfReturns
Nonefuncstop(self) -> NoneSignal run() to exit and shut the service down.
Source Code
async def stop(self) -> None:
"""Signal run() to exit and shut the service down."""
self.shutdown_event.set()paramselfReturns
Nonefuncregister_routes(self, api_prefix, main_router) -> NoneRegister the WebRTC HTTP/WebSocket endpoints.
Every endpoint lives under /api so the single nginx /api proxy rule fronts them — the signaling socket included, since that location forwards WebSocket upgrades. Paths off /api (such as a bare /turn or /webrtc/signaling) would sit unproxied behind the LSIO nginx: the TURN fetch and the signaling handshake would 404 and freeze the dashboard.
Source Code
def register_routes(self, api_prefix: str, main_router: web.UrlDispatcher) -> None:
"""Register the WebRTC HTTP/WebSocket endpoints.
Every endpoint lives under /api so the single nginx /api proxy rule
fronts them — the signaling socket included, since that location
forwards WebSocket upgrades. Paths off /api (such as a bare /turn or
/webrtc/signaling) would sit unproxied behind the LSIO nginx: the TURN
fetch and the signaling handshake would 404 and freeze the dashboard.
"""
main_router.add_get(
f"{api_prefix}/api/webrtc/signaling{{slash:/?}}", self.rtc_ws_handler
)
main_router.add_get(f"{api_prefix}/api/ws", self.rtc_ws_handler)
main_router.add_get(f"{api_prefix}/api/turn", self.handle_turn_req)paramselfparamapi_prefixstrparammain_routerweb.UrlDispatcherReturns
Nonefuncrtc_ws_handler(self, request) -> Union[web.Response, web.WebSocketResponse]Accept a signaling WebSocket, refusing with 409/503 while the WebRTC mode is inactive or still starting.
Source Code
async def rtc_ws_handler(
self, request: web.Request
) -> Union[web.Response, web.WebSocketResponse]:
"""Accept a signaling WebSocket, refusing with 409/503 while the WebRTC
mode is inactive or still starting."""
if self.supervisor.current_mode != self.mode:
return web.Response(status=409, text="WebRTC mode is inactive")
if self.peer_manager is None:
return web.Response(status=503, headers={"Retry-After": "1"},
text="WebRTC service is still starting")
# autoping=False so the signaling loop sees PONG frames and can feed
# the upload uplink gauge's clock; the loop answers PING itself.
ws = web.WebSocketResponse(autoping=False)
await ws.prepare(request)
peername = request.transport.get_extra_info("peername")
remote_address = peername if peername else (request.remote, 0)
await self.peer_manager.signaling_handler(
ws, remote_address, auth_role_ceiling=request.get("auth_role_ceiling")
)
return wsparamselfparamrequestweb.RequestReturns
typing.Union[aiohttp.web.aiohttp.web.Response, aiohttp.web.aiohttp.web.WebSocketResponse]funcuplink_session_conns(self) -> List[Tuple[Any, Optional[str], Optional[str]]](websocket, session token, peer ip) per connected browser
signaling peer, for the supervisor's upload uplink gauge. The
signaling socket stays open for the session's life, so it is the
WebRTC transport's window onto the client's uplink.
Source Code
def uplink_session_conns(self) -> List[Tuple[Any, Optional[str], Optional[str]]]:
"""``(websocket, session token, peer ip)`` per connected browser
signaling peer, for the supervisor's upload uplink gauge. The
signaling socket stays open for the session's life, so it is the
WebRTC transport's window onto the client's uplink."""
if self.peer_manager is None:
return []
conns = []
for peer in list(self.peer_manager.peers.values()):
if peer.peer_type != "client":
continue
raddr = peer.raddr
ip = raddr[0] if isinstance(raddr, (tuple, list)) and raddr else None
conns.append((peer.ws, peer.client_token, ip))
return connsparamselfReturns
typing.List[typing.Tuple[typing.Any, typing.Optional[str], typing.Optional[str]]]funchandle_turn_req(self, request) -> web.ResponseServe a TURN credential request via the peer manager, refusing with 409/503 while the WebRTC mode is inactive or still starting.
Source Code
async def handle_turn_req(self, request: web.Request) -> web.Response:
"""Serve a TURN credential request via the peer manager, refusing with
409/503 while the WebRTC mode is inactive or still starting."""
if self.supervisor.current_mode != self.mode:
return web.json_response({"error": "WebRTC mode is inactive"}, status=409)
if self.peer_manager is None:
return web.json_response(
{"error": "WebRTC service is still starting"},
status=503, headers={"Retry-After": "1"})
return await self.peer_manager.handle_turn_req(request)paramselfparamrequestweb.RequestReturns
aiohttp.web.aiohttp.web.Response