# \[Bug\] PS2 deadlocks when a Blueprint sends a data-channel response while input is streaming (5.8)

**URL:** <https://forums.unrealengine.com/t/bug-ps2-deadlocks-when-a-blueprint-sends-a-data-channel-response-while-input-is-streaming-5-8/2843220>\
**Category:** Programming & Scripting\
**Tags:** unreal-engine, CPP\
**Created:** [October 21, 2026, 9:29am UTC](https://forums.unrealengine.com/t/bug-ps2-deadlocks-when-a-blueprint-sends-a-data-channel-response-while-input-is-streaming-5-8/2843220 "2026-10-21T09:29:50Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![XuangXenXin1](https://d3kjluh73b9h9o.cloudfront.net/original/4X/2/8/5/28502927f1d97ba1051b3b47ccfe17233bf41ef0.png) [@XuangXenXin1](https://forums.unrealengine.com/u/XuangXenXin1)\
**Post date:** [October 21, 2026, 9:29am UTC](https://forums.unrealengine.com/t/bug-ps2-deadlocks-when-a-blueprint-sends-a-data-channel-response-while-input-is-streaming-5-8/2843220/1 "2026-10-21T09:29:50Z")

</div>

**# [Bug] Pixel Streaming 2 deadlocks when a Blueprint sends a data-channel response while input is streaming (UE 5.8)**

**## Summary**

`FEpicRtcStreamer::SendAllPlayersMessage()` sends data-channel messages \*\*while holding the participants

map lock\*\*. Sending marshals onto the WebRTC signalling thread and can block. The signalling thread needs

the _\*same\*_ lock when it handles an inbound data-track message (`OnDataTrackMessage` → `FindPlayerFromTrack`

→ `TThreadSafeMap::ApplyUntil`). The result is a classic AB-BA deadlock between the game thread and the

signalling thread: the whole game freezes with 0% CPU and 0% GPU, and never recovers.

This is reachable from the _\*documented\*_ Blueprint workflow: `OnInputEvent` → `SendPixelStreaming2Response`.

Any project that echoes UI/mouse interactions back to the browser will eventually hit it.

**## Environment**

- Unreal Engine 5.8.3 (`++UE5+Release-5.8`, CL 58210709), installed (Launcher) build

- Windows 11 25H2, RTX 5070, driver 617.42

- Packaged Shipping and Development builds both affected

- Pixel Streaming 2 plugin, H.264 and AV1 both affected

**## Symptoms**

- Streaming runs fine for a while, then **\*\*freezes permanently\*\*** — the engine stops ticking, CPU and GPU

usage drop to 0%, but the process does not crash.

- Most easily reproduced by **\*\*dragging in the scene\*\*** (continuous mouse input) while the project replies

to input events.

- Only one stream is required; it is not related to stream count or encoder.

- The network/data channel appears “connected” from the browser side, but no frames are produced.

**## Root cause**

**### 1. The send happens under the map lock**

`Engine/Plugins/Media/PixelStreaming2/Source/PixelStreaming2RTC/Private/EpicRtcStreamer.cpp`

```cpp

void FEpicRtcStreamer::SendAllPlayersMessage(FString MessageType, const FString& Descriptor)

{

```
Participants->Apply(\[&MessageType, &Descriptor\](FString PlayerId, TSharedPtr<FPlayerContext> Participant) {

    if (Participant->DataTrack && !IsSFU(PlayerId))

    {

        Participant->DataTrack->SendMessage(MessageType, Descriptor); // <-- send inside the lock

    }

});

```

}

```

`TThreadSafeMap::Apply()` holds `Mutex` for the whole traversal

(`Plugins/Media/PixelStreaming2/Source/PixelStreaming2/Internal/ThreadSafeMap.h:224`):

```cpp

void Apply(T&& Visitor)

{

```
FScopeLock Lock(&Mutex);

for (typename Super::ElementSetType::TIterator It(Super::Pairs); It; ++It)

{

    Visitor(It->Key, It->Value);

}

```

}

// Note: Do not call a map method inside a visitor or you will deadlock

```

`DataTrack-\>SendMessage()` ends up in `webrtc::SctpDataChannel::Send()`, which marshals the call onto the

signalling thread (`MethodCall\<…\>::Marshal`) and **\*\*waits\*\*** for it to run.

**### 2. The signalling thread needs the same lock**

```cpp

void FEpicRtcStreamer::OnDataTrackMessage(EpicRtcDataTrackInterface\* DataTrack, const EpicRtcDataFrame& DataFrame)

{

```
...

else if (!FindPlayerFromTrack(DataTrack, PlayerId)) // -> Participants->ApplyUntil() -> FScopeLock Mutex

```

```

**### 3. AB-BA**

- **\*\*Game thread\*\*** : holds `Mutex` in `Apply()` → blocks in `SctpDataChannel::Send` waiting for the

signalling thread.

- **\*\*Signalling thread\*\*** : inside `OnDataTrackMessage` → `FindPlayerFromTrack` → blocks on `Mutex`.

Neither can make progress.

**## Evidence (full-memory dump, shipping build, symbols from the PDB)**

Game thread (`. 0 Id: … “CrBrowserMain”` — BLUI/CEF renamed the game thread):

```

ntdll!NtWaitForSingleObject

KERNELBASE!WaitForSingleObjectEx

Template58!rtc::Event::WinSystemEvent::Wait

Template58!webrtc::MethodCall\<webrtc::DataChannelInterface,bool,webrtc::DataBuffer const &\>::Marshal

Template58!webrtc::SctpDataChannel::Send

Template58!EpicRtc::DataTrackInternal::PushFrame

Template58!UE::PixelStreaming2::FEpicRtcDataTrack::SendMessage

Template58!UE::PixelStreaming2::FEpicRtcStreamer::SendAllPlayersMessage

…

Template58!UPixelStreaming2Input::execSendPixelStreaming2Response ← Blueprint node

Template58!UObject::ProcessEvent

Template58!AActor::Tick

```

Signalling thread (`“signalling_thread_pixel_streaming_conference_instance”`):

```

ntdll!RtlEnterCriticalSection ← waiting for the SAME mutex

ntdll!RtlSleepConditionVariableCS

Template58!UE::PixelStreaming2::TThreadSafeMap\<FString,TSharedPtr\>::ApplyUntil\<

```
UE::PixelStreaming2::FEpicRtcStreamer::FindPlayerFromTrack<EpicRtcDataTrackInterface\*>>

```

Template58!UE::PixelStreaming2::FEpicRtcStreamer::FindPlayerFromTrack

Template58!UE::PixelStreaming2::FEpicRtcStreamer::OnDataTrackMessage

Template58!UE::PixelStreaming2::FEpicRtcDataTrackObserver::OnDataTrackMessage

Template58!EpicRtc::DataTrackInternal::OnMessage

Template58!webrtc::SctpDataChannel::OnDataReceived

Template58!rtc:🧵:Dispatch

```

**## Reproduction outline**

1. Packaged (or Development) build, Pixel Streaming 2 enabled, `-PixelStreamingURL=ws://:/`

`-RenderOffscreen`.

2. Blueprint: a `PixelStreaming2Input` component → bind `OnInputEvent` → call

`SendPixelStreaming2Response`.

3. Connect a browser and drag/interact continuously.

4. After a short while the engine freezes (0% CPU/GPU). A dump shows the two stacks above.

**## Suggested fix**

Take a snapshot of the data tracks under the lock, then send once the lock has been released — the same

pattern `SendPlayerMessage` already uses correctly:

```cpp

void FEpicRtcStreamer::SendAllPlayersMessage(FString MessageType, const FString& Descriptor)

{

```
TArray<TSharedPtr<FEpicRtcDataTrack>> DataTracks;

Participants->Apply(\[&DataTracks\](FString PlayerId, TSharedPtr<FPlayerContext> Participant) {

    if (Participant->DataTrack && !IsSFU(PlayerId))

    {

        DataTracks.Add(Participant->DataTrack); // collect only

    }

});

for (const TSharedPtr<FEpicRtcDataTrack>& DataTrack : DataTracks)

{

    DataTrack->SendMessage(MessageType, Descriptor); // lock released

}

```

}

```

The same hazard exists in two more places and should be fixed identically:

- `FEpicRtcStreamer::SendFileData()` — already carries a TODO that describes exactly this problem:

\*"because we lock when we visit the data channels it might be a bad idea. At some point it would be

good to take a snapshot of the keys in the map when we start, then one by one get the channel and send

the data"\*.

- `FFreezeFrame::FreezeFrameCapture()` (`FreezeFrame.cpp`) — sends the JPEG to every player from inside

`Players-\>Apply`.

Contrast with `FEpicRtcStreamer::SendPlayerMessage()`, which is correct — it copies the `DataTrack`

shared pointer out of the container and sends **\*\*after\*\*** the lock is released. All engine-internal traffic

(latency reports, stats) uses that safe path, which is why this only shows up in app-driven sends.

**## Note on 5.6 vs 5.8**

We upgraded the same project from 5.6 to 5.8. The C++ path above is unchanged between the two versions

(`SendAllPlayersMessage`, `TThreadSafeMap::Apply`, `FindPlayerFromTrack`, `OnDataTrackMessage` are

identical), but the project did **\*\*not\*\*** freeze on 5.6. The difference appears to be inside the EpicRtc /

libwebrtc build shipped with 5.8 (the EpicRtc API was reshaped between versions:

`EpicRtcDataFrameInput` + `EpicRtcDataFrameInterface` + `PopFrame` → `EpicRtcDataFrame`). On 5.8 the game

thread demonstrably blocks inside `SctpDataChannel::Send`; on 5.6 it evidently returned without waiting on

the signalling thread, so the pre-existing lock misuse stayed latent.

**## Workaround for other projects**

Until this is fixed, defer the response by one frame so it is not sent from inside the input dispatch:

```

OnInputEvent → SetTimerForNextTick → SendPixelStreaming2Response

```

This does not remove the bug but drastically shrinks the window in which the signalling thread can be

waiting for the participants lock at the moment of the send.

**## Verified**

Patching `SendAllPlayersMessage` (and `SendFileData` / `FreezeFrameCapture`) as shown above and rebuilding,

the freeze no longer reproduces in our project under continuous scene dragging with a connected browser.
