Skip to content

Commit c5ce227

Browse files
Steveactions-user
authored andcommitted
```
fix(hls): Synchronize streams with blocking probes to prevent race condition Fixes a race condition in the HLS pipeline where data buffers could flow before segment events, leading to errors like "Got data flow before segment event". The previous approach of manually injecting segment events has been removed as it could cause misordering depending on probe timing. This change implements a more reliable synchronization mechanism: - Add blocking probes to video and audio queues before the muxer. - Data flow is blocked until both audio and video streams are connected. - A new method (`check_hls_streams_ready`) verifies both streams are ready. - The HLS sink's state is explicitly managed, transitioning to PLAYING only after both streams are connected and blocking probes are removed. - This guarantees synchronized data flow and proper pipeline initialization on all platforms. Changes made in `webrtc_subprocess_glib.py`. ``` [auto-enhanced]
1 parent 011fbe0 commit c5ce227

1 file changed

Lines changed: 86 additions & 59 deletions

File tree

webrtc_subprocess_glib.py

Lines changed: 86 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -774,6 +774,49 @@ def try_pending_renegotiation(self):
774774
self.log("Still not in stable state, will retry")
775775
return True # Continue the timer
776776

777+
def check_hls_streams_ready(self):
778+
"""Check if both audio and video are connected and unblock if ready"""
779+
if not self.use_hls:
780+
return
781+
782+
# Check if both streams are connected
783+
if hasattr(self, 'hls_audio_connected') and self.hls_audio_connected and \
784+
hasattr(self, 'hls_video_connected') and self.hls_video_connected:
785+
786+
self.log(" ✅ Both audio and video connected - starting HLS recording")
787+
788+
# First sync the HLS sink state
789+
if hasattr(self, 'hlssink') and self.hlssink:
790+
ret = self.hlssink.sync_state_with_parent()
791+
self.log(f" HLS sink sync result: {ret}")
792+
793+
# Ensure it's in PLAYING state
794+
state_ret, state, pending = self.hlssink.get_state(0)
795+
if state != Gst.State.PLAYING:
796+
self.log(f" HLS sink not in PLAYING state ({state}), forcing to PLAYING")
797+
ret = self.hlssink.set_state(Gst.State.PLAYING)
798+
self.log(f" HLS sink set_state result: {ret}")
799+
800+
# Wait for state change
801+
timeout = 2 * Gst.SECOND
802+
state_ret, state, pending = self.hlssink.get_state(timeout)
803+
self.log(f" HLS sink final state: {state}")
804+
805+
# Now remove the blocking probes to allow data flow
806+
if hasattr(self, 'video_probe_id') and hasattr(self, 'video_queue'):
807+
video_src = self.video_queue.get_static_pad('src')
808+
if video_src:
809+
video_src.remove_probe(self.video_probe_id)
810+
self.log(" ▶️ Unblocked video flow")
811+
812+
if hasattr(self, 'audio_probe_id') and hasattr(self, 'audio_queue'):
813+
audio_src = self.audio_queue.get_static_pad('src')
814+
if audio_src:
815+
audio_src.remove_probe(self.audio_probe_id)
816+
self.log(" ▶️ Unblocked audio flow")
817+
818+
self.log(" 🎬 HLS recording started!")
819+
777820
def setup_hls_muxer(self):
778821
"""Set up shared mpegtsmux and HLS sink for audio/video"""
779822
if hasattr(self, 'hls_mux'):
@@ -834,8 +877,8 @@ def setup_hls_muxer(self):
834877
# Add sink to pipeline
835878
self.pipe.add(self.hlssink)
836879

837-
# Don't sync state yet - wait until pads are connected
838-
# This prevents the muxer from starting before it has inputs
880+
# Keep sink in NULL state until pads are connected
881+
self.hlssink.set_state(Gst.State.NULL)
839882

840883
self.log(" ✅ HLS muxer ready for audio/video streams")
841884

@@ -1538,6 +1581,8 @@ def ndi_video_probe_cb(pad, info):
15381581
if not video_queue:
15391582
self.log("Failed to create video queue for HLS", "error")
15401583
return
1584+
# Store reference for later access
1585+
self.video_queue = video_queue
15411586

15421587
# Add identity element to help with segment handling
15431588
video_identity = Gst.ElementFactory.make('identity', 'video_identity')
@@ -1628,21 +1673,9 @@ def depay_probe_cb(pad, info):
16281673
h264_pad = h264parse.get_static_pad('src')
16291674
if h264_pad:
16301675
def h264_probe_cb(pad, info):
1631-
# Check if this is a buffer (not an event)
1632-
if info.type & Gst.PadProbeType.BUFFER:
1633-
# Inject a segment event before the first buffer
1634-
if not hasattr(self, '_h264_segment_sent'):
1635-
self._h264_segment_sent = True
1636-
# Create segment event
1637-
segment = Gst.Segment()
1638-
segment.init(Gst.Format.TIME)
1639-
event = Gst.Event.new_segment(segment)
1640-
pad.push_event(event)
1641-
self.log(" ✅ Injected segment event for HLS video")
1642-
1643-
if not hasattr(self, '_h264_probe_logged'):
1644-
self._h264_probe_logged = True
1645-
self.log(" ✅ Video data confirmed after h264parse!")
1676+
if not hasattr(self, '_h264_probe_logged'):
1677+
self._h264_probe_logged = True
1678+
self.log(" ✅ Video data confirmed after h264parse!")
16461679
return Gst.PadProbeReturn.OK
16471680
h264_pad.add_probe(Gst.PadProbeType.BUFFER, h264_probe_cb)
16481681

@@ -1663,13 +1696,6 @@ def queue_probe_cb(pad, info):
16631696
if video_pad:
16641697
src_pad = video_queue.get_static_pad('src')
16651698

1666-
# Send initial segment event before linking
1667-
segment = Gst.Segment()
1668-
segment.init(Gst.Format.TIME)
1669-
event = Gst.Event.new_segment(segment)
1670-
video_pad.send_event(event)
1671-
self.log(" ✅ Sent initial segment event to HLS video pad")
1672-
16731699
if src_pad.link(video_pad) == Gst.PadLinkReturn.OK:
16741700
self.log(" ✅ Video connected to HLS sink")
16751701
# Check hlssink state
@@ -1703,22 +1729,23 @@ def queue_probe_cb(pad, info):
17031729
for element in elements:
17041730
element.sync_state_with_parent()
17051731

1706-
# Also ensure HLS sink is in correct state
1707-
if hasattr(self, 'hlssink') and self.hlssink:
1708-
# First try to sync with parent
1709-
ret = self.hlssink.sync_state_with_parent()
1710-
self.log(f" HLS sink sync result: {ret}")
1711-
1712-
# Check the actual state
1713-
state_ret, state, pending = self.hlssink.get_state(0)
1714-
if state != Gst.State.PLAYING:
1715-
self.log(f" HLS sink not in PLAYING state ({state}), forcing to PLAYING")
1716-
ret = self.hlssink.set_state(Gst.State.PLAYING)
1717-
self.log(f" HLS sink set_state result: {ret}")
1718-
# Wait for state change to complete
1719-
timeout = 5 * Gst.SECOND # 5 second timeout
1720-
state_ret, state, pending = self.hlssink.get_state(timeout)
1721-
self.log(f" HLS sink final state: {state} (result: {state_ret})")
1732+
# Add blocking probe to video queue to prevent data flow until ready
1733+
if self.use_hls:
1734+
video_queue_src = video_queue.get_static_pad('src')
1735+
if video_queue_src:
1736+
def video_block_probe_cb(pad, info):
1737+
return Gst.PadProbeReturn.OK # Just block
1738+
self.video_probe_id = video_queue_src.add_probe(
1739+
Gst.PadProbeType.BLOCK_DOWNSTREAM,
1740+
video_block_probe_cb
1741+
)
1742+
self.log(" 🛑 Blocking video flow until both streams ready")
1743+
1744+
# Track that video is connected
1745+
self.hls_video_connected = True
1746+
1747+
# Check if both streams are ready to start
1748+
self.check_hls_streams_ready()
17221749

17231750
# Link incoming pad to queue
17241751
sink_pad = queue.get_static_pad('sink')
@@ -1957,6 +1984,8 @@ def handle_audio_pad(self, pad):
19571984
aacparse = Gst.ElementFactory.make('aacparse', None)
19581985
# Create a queue before muxer
19591986
audio_queue = Gst.ElementFactory.make('queue', 'audio_queue_hls')
1987+
# Store reference for later access
1988+
self.audio_queue = audio_queue
19601989
# Add identity element for audio segment handling
19611990
audio_identity = Gst.ElementFactory.make('identity', 'audio_identity')
19621991
if audio_identity:
@@ -2065,26 +2094,24 @@ def handle_audio_pad(self, pad):
20652094
for element in elements:
20662095
element.sync_state_with_parent()
20672096

2068-
# Add segment event injection for audio too
2069-
audio_src_pad = audio_queue.get_static_pad('src')
2070-
if audio_src_pad:
2071-
def audio_segment_probe_cb(pad, info):
2072-
if info.type & Gst.PadProbeType.BUFFER:
2073-
if not hasattr(self, '_audio_segment_sent'):
2074-
self._audio_segment_sent = True
2075-
# Inject segment event before first audio buffer
2076-
segment = Gst.Segment()
2077-
segment.init(Gst.Format.TIME)
2078-
event = Gst.Event.new_segment(segment)
2079-
pad.push_event(event)
2080-
self.log(" ✅ Injected segment event for HLS audio")
2081-
return Gst.PadProbeReturn.OK
2082-
audio_src_pad.add_probe(Gst.PadProbeType.BUFFER, audio_segment_probe_cb)
20832097

2084-
# Also ensure HLS sink is in correct state for audio
2085-
if hasattr(self, 'hlssink') and self.hlssink:
2086-
# Don't sync again if already done for video
2087-
pass
2098+
# Add blocking probe to audio queue to prevent data flow until ready
2099+
if self.use_hls:
2100+
audio_queue_src = audio_queue.get_static_pad('src')
2101+
if audio_queue_src:
2102+
def audio_block_probe_cb(pad, info):
2103+
return Gst.PadProbeReturn.OK # Just block
2104+
self.audio_probe_id = audio_queue_src.add_probe(
2105+
Gst.PadProbeType.BLOCK_DOWNSTREAM,
2106+
audio_block_probe_cb
2107+
)
2108+
self.log(" 🛑 Blocking audio flow until both streams ready")
2109+
2110+
# Track that audio is connected
2111+
self.hls_audio_connected = True
2112+
2113+
# Check if both streams are ready to start
2114+
self.check_hls_streams_ready()
20882115

20892116
# Link incoming pad to queue
20902117
sink_pad = queue.get_static_pad('sink')

0 commit comments

Comments
 (0)