Bug Description
When using --multiviewer, the pipeline enters a permanently broken state after a viewer disconnects ungracefully (ICE failure or heartbeat timeout). Every subsequent connection attempt fails silently. The service must be manually restarted to recover.
Symptom
The following GStreamer warning appears in the logs every time a new viewer tries to connect, repeating indefinitely until the service is restarted:
Trying to link elements videotee and qv-<UUID> that don't share a common ancestor:
qv-<UUID> hasn't been added to a bin or pipeline, and videotee is in pipeline0
Root Cause
In createPeer(), the multiviewer branch creates a new queue element and adds it to the pipeline before attempting the link:
qv = Gst.ElementFactory.make('queue', f"qv-{client['UUID']}")
self.pipe.add(qv) # element added to pipeline
if not Gst.Element.link(vtee, qv):
return # ← early return on link failure
# ...
client['qv'] = qv # ← never reached!
If the link fails for any reason, the function returns early and client['qv'] is never set. The element qv-<UUID> is now in the pipeline but not tracked in the client dict.
When the client disconnects, _stop_pipeline_internal() calls client.get('qv') → returns None → the stale element is never removed from the pipeline.
On the next connection attempt (same UUID), Gst.ElementFactory.make('queue', 'qv-<UUID>') creates a fresh element, but self.pipe.add(qv) silently returns False because an element with that name already exists. The link then fails with the "common ancestor" warning, and the cycle repeats forever.
The same issue exists for the webrtcbin element (client['UUID'] as name).
Fix
Two changes in the multiviewer else branch of createPeer():
-
Purge stale elements before creating new ones — call self.pipe.get_by_name(...) and remove any leftover element before trying to add a new one with the same name.
-
Track client['qv'] immediately after pipe.add(), not after the link — so cleanup in _stop_pipeline_internal() always has a reference to remove.
# Purge stale element from previous failed attempt
stale_qv = self.pipe.get_by_name(f"qv-{client['UUID']}")
if stale_qv is not None:
try:
vtee.unlink(stale_qv)
except Exception:
pass
stale_qv.set_state(Gst.State.NULL)
self.pipe.remove(stale_qv)
qv = Gst.ElementFactory.make('queue', f"qv-{client['UUID']}")
self.pipe.add(qv)
client['qv'] = qv # track BEFORE link attempt so cleanup always works
if not Gst.Element.link(vtee, qv):
return
Reproduction
- Start publisher with
--multiviewer
- Connect a viewer — stream works
- Simulate an ungraceful disconnect (e.g. kill the browser tab, network dropout)
- Try to reconnect → fails with the "common ancestor" warning
- Every further reconnect attempt fails until service restart
Environment
- Raspberry Pi 5 (aarch64), Raspberry Pi OS
- GStreamer 1.x
publish.py as of April 2026 (main branch)
- Camera: USB webcam via v4l2src, H.264, 1080p30
Bug Description
When using
--multiviewer, the pipeline enters a permanently broken state after a viewer disconnects ungracefully (ICE failure or heartbeat timeout). Every subsequent connection attempt fails silently. The service must be manually restarted to recover.Symptom
The following GStreamer warning appears in the logs every time a new viewer tries to connect, repeating indefinitely until the service is restarted:
Root Cause
In
createPeer(), the multiviewer branch creates a new queue element and adds it to the pipeline before attempting the link:If the link fails for any reason, the function returns early and
client['qv']is never set. The elementqv-<UUID>is now in the pipeline but not tracked in the client dict.When the client disconnects,
_stop_pipeline_internal()callsclient.get('qv')→ returnsNone→ the stale element is never removed from the pipeline.On the next connection attempt (same UUID),
Gst.ElementFactory.make('queue', 'qv-<UUID>')creates a fresh element, butself.pipe.add(qv)silently returnsFalsebecause an element with that name already exists. The link then fails with the "common ancestor" warning, and the cycle repeats forever.The same issue exists for the
webrtcbinelement (client['UUID']as name).Fix
Two changes in the multiviewer
elsebranch ofcreatePeer():Purge stale elements before creating new ones — call
self.pipe.get_by_name(...)and remove any leftover element before trying to add a new one with the same name.Track
client['qv']immediately afterpipe.add(), not after the link — so cleanup in_stop_pipeline_internal()always has a reference to remove.Reproduction
--multiviewerEnvironment
publish.pyas of April 2026 (main branch)