Skip to content

--multiviewer: pipeline permanently stuck after ungraceful disconnect (GStreamer element name collision) #54

@EKR-Pikes

Description

@EKR-Pikes

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

  1. 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.

  2. 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

  1. Start publisher with --multiviewer
  2. Connect a viewer — stream works
  3. Simulate an ungraceful disconnect (e.g. kill the browser tab, network dropout)
  4. Try to reconnect → fails with the "common ancestor" warning
  5. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions