2121
2222from PySide6 .QtCore import QObject , QThread , QTimer , Signal
2323from PySide6 .QtWidgets import (
24- QCheckBox , QFileDialog , QGroupBox , QHBoxLayout , QHeaderView , QLabel ,
25- QLineEdit , QMessageBox , QPushButton , QTableWidget , QTableWidgetItem ,
26- QVBoxLayout , QWidget ,
24+ QCheckBox , QComboBox , QFileDialog , QGroupBox , QHBoxLayout , QHeaderView ,
25+ QLabel , QLineEdit , QMessageBox , QPushButton , QTableWidget ,
26+ QTableWidgetItem , QVBoxLayout , QWidget ,
2727)
2828
2929from je_auto_control .gui ._i18n_helpers import TranslatableMixin
@@ -76,15 +76,22 @@ class UsbPassthroughPanel(TranslatableMixin, QWidget):
7676 def __init__ (self , parent : Optional [QWidget ] = None , * ,
7777 acl : Optional [UsbAcl ] = None ,
7878 loopback_factory : Optional [Callable [[], UsbLoopback ]] = None ,
79+ remote_client_provider : Optional [
80+ Callable [[], Optional [Any ]]
81+ ] = None ,
7982 ) -> None :
8083 super ().__init__ (parent )
8184 self ._tr_init ()
8285 self ._acl = acl if acl is not None else UsbAcl ()
8386 self ._loopback_factory = loopback_factory or self ._default_loopback
87+ self ._remote_client_provider = (
88+ remote_client_provider or _default_remote_client
89+ )
8490 self ._loopback : Optional [UsbLoopback ] = None
8591 self ._thread : Optional [QThread ] = None
8692 self ._host_badge = _StatusBadge ()
8793 self ._viewer_status = QLabel ("" )
94+ self ._source_combo = QComboBox ()
8895 self ._auto_check = QCheckBox ()
8996 self ._auto_check .toggled .connect (self ._on_auto_toggled )
9097 self ._hotplug_timer = QTimer (self )
@@ -97,6 +104,7 @@ def __init__(self, parent: Optional[QWidget] = None, *,
97104 self ._remote_token = QLineEdit ()
98105 self ._remote_token .setEchoMode (QLineEdit .EchoMode .Password )
99106 self ._build_layout ()
107+ self ._populate_source_combo ()
100108 self ._apply_local_headers ()
101109 self ._apply_shared_headers ()
102110 self ._refresh_local_devices ()
@@ -161,6 +169,10 @@ def _build_viewer_section(self) -> QWidget:
161169 intro = self ._tr (QLabel (), "usb_share_intro" )
162170 intro .setWordWrap (True )
163171 layout .addWidget (intro )
172+ source_row = QHBoxLayout ()
173+ source_row .addWidget (self ._tr (QLabel (), "usb_share_source_label" ))
174+ source_row .addWidget (self ._source_combo , stretch = 1 )
175+ layout .addLayout (source_row )
164176 layout .addWidget (self ._shared_table , stretch = 1 )
165177 use_row = QHBoxLayout ()
166178 list_btn = self ._tr (QPushButton (), "usb_share_fetch_shared" )
@@ -204,8 +216,18 @@ def _apply_shared_headers(self) -> None:
204216 _t ("usb_share_col_product" ), _t ("usb_share_col_serial" ),
205217 ])
206218
219+ def _populate_source_combo (self ) -> None :
220+ index = max (0 , self ._source_combo .currentIndex ())
221+ self ._source_combo .blockSignals (True )
222+ self ._source_combo .clear ()
223+ self ._source_combo .addItem (_t ("usb_share_source_local" ))
224+ self ._source_combo .addItem (_t ("usb_share_source_remote" ))
225+ self ._source_combo .setCurrentIndex (index )
226+ self ._source_combo .blockSignals (False )
227+
207228 def retranslate (self ) -> None :
208229 TranslatableMixin .retranslate (self )
230+ self ._populate_source_combo ()
209231 self ._apply_local_headers ()
210232 self ._apply_shared_headers ()
211233 self ._refresh_host_badge ()
@@ -330,15 +352,40 @@ def _import_acl(self) -> None:
330352 )
331353 self ._refresh_local_devices ()
332354
333- # --- use (loopback) ----------------------------------------------------
355+ # --- use (loopback or live WebRTC) -------------------------------------
356+
357+ def _source_is_remote (self ) -> bool :
358+ return self ._source_combo .currentIndex () == 1
359+
360+ def _active_use_client (self ) -> Any :
361+ """Return the client for the selected source, or raise a friendly error.
362+
363+ Both the loopback bundle and the WebRTC ``UsbChannelClient``
364+ expose ``list_devices`` and ``open`` with the same signatures, so
365+ the use actions are source-agnostic.
366+ """
367+ if self ._source_is_remote ():
368+ client = self ._remote_client_provider ()
369+ if client is None :
370+ raise RuntimeError (_t ("usb_share_no_webrtc" ))
371+ return client
372+ if self ._loopback is None :
373+ raise RuntimeError (_t ("usb_share_enable_first" ))
374+ return self ._loopback
375+
376+ def _use_client_or_warn (self ) -> Any :
377+ try :
378+ return self ._active_use_client ()
379+ except RuntimeError as error :
380+ self ._info (str (error ))
381+ return None
334382
335383 def _list_shared (self ) -> None :
336- loop = self ._loopback
337- if loop is None :
338- self ._info (_t ("usb_share_enable_first" ))
384+ client = self ._use_client_or_warn ()
385+ if client is None :
339386 return
340387 self ._viewer_status .setText (_t ("usb_share_listing" ))
341- self ._run_async (loop .list_devices , self ._apply_shared , self ._fail )
388+ self ._run_async (client .list_devices , self ._apply_shared , self ._fail )
342389
343390 def _apply_shared (self , devices : List [dict ]) -> None :
344391 self ._viewer_status .setText (
@@ -354,9 +401,8 @@ def _apply_shared(self, devices: List[dict]) -> None:
354401 self ._shared_table .setItem (row , col , QTableWidgetItem (str (text )))
355402
356403 def _open_selected (self ) -> None :
357- loop = self ._loopback
358- if loop is None :
359- self ._info (_t ("usb_share_enable_first" ))
404+ client = self ._use_client_or_warn ()
405+ if client is None :
360406 return
361407 row = _selected_row (self ._shared_table )
362408 if row is None :
@@ -369,7 +415,7 @@ def _open_selected(self) -> None:
369415 _t ("usb_share_opening" ).format (vid = vid , pid = pid ),
370416 )
371417 self ._run_async (
372- lambda : _probe_device (loop , vid , pid , serial ),
418+ lambda : _probe_device (client , vid , pid , serial ),
373419 lambda descriptor : self ._opened (vid , pid , descriptor ),
374420 self ._fail ,
375421 )
@@ -458,10 +504,23 @@ def _selected_row(table: QTableWidget) -> Optional[int]:
458504 return rows [0 ] if rows else None
459505
460506
461- def _probe_device (loop : UsbLoopback , vid : str , pid : str ,
507+ def _default_remote_client () -> Optional [Any ]:
508+ """Return the live WebRTC viewer's USB client, or None if unavailable."""
509+ try :
510+ from je_auto_control .utils .remote_desktop .registry import registry
511+ except ImportError :
512+ return None
513+ return registry .webrtc_usb_client ()
514+
515+
516+ def _probe_device (client : Any , vid : str , pid : str ,
462517 serial : Optional [str ]) -> bytes :
463- """Open the device, read its descriptor as a liveness proof, close."""
464- handle = loop .open (vendor_id = vid , product_id = pid , serial = serial )
518+ """Open the device, read its descriptor as a liveness proof, close.
519+
520+ ``client`` is either a :class:`UsbLoopback` or a WebRTC
521+ ``UsbChannelClient`` — both expose the same ``open`` signature.
522+ """
523+ handle = client .open (vendor_id = vid , product_id = pid , serial = serial )
465524 try :
466525 return handle .control_transfer (
467526 bm_request_type = _DESC_REQUEST_TYPE , b_request = _DESC_REQUEST ,
0 commit comments