@@ -34,9 +34,14 @@ internal import LiveKitUniFFI
3434/// delegates are immutable after init. Not an actor — the UniFFI delegate callbacks are synchronous
3535/// and can't `await`.
3636final class DataStreams : NSObject , @unchecked Sendable , Loggable {
37- private let incoming : LiveKitUniFFI . IncomingDataStreamManager
3837 private let outgoing : LiveKitUniFFI . OutgoingDataStreamManager
3938
39+ // Created lazily on the first inbound packet, not at init: the incoming manager's payload cap
40+ // comes from the room's options, which aren't finalized until `connect` — after this coordinator
41+ // is built at `Room.init`. Deferring lets it pick up a `maxPayloadSize` passed at connect time.
42+ // StateSync-guarded so it's constructed exactly once even if packets race in.
43+ private let _incoming = StateSync < LiveKitUniFFI . IncomingDataStreamManager ? > ( nil )
44+
4045 // Held weakly: the Room owns this coordinator, so the back-reference must not retain it. Used
4146 // for the room-level encryption type stamped onto stream info, and for logging.
4247 private weak var room : Room ?
@@ -58,22 +63,31 @@ final class DataStreams: NSObject, @unchecked Sendable, Loggable {
5863 private let orderedTopics = StateSync < Set < String > > ( [ ] )
5964 private let orderedTails = StateSync < [ String : [ String : Task < Void , Never > ] ] > ( [ : ] )
6065
61- init ( room: Room , maxPayloadSize : Int ? = nil ) {
66+ init ( room: Room ) {
6267 self . room = room
63- let incomingDelegate = IncomingDelegate ( )
6468 let outgoingDelegate = OutgoingDelegate ( room: room)
6569 let registry = Registry ( room: room)
66- // `maxPayloadSize` caps the reassembled size of an incoming stream (nil → the core's default
67- // cap). Topic routing (incl. the `lk.rpc` guard) is handled Swift-side in `Room+DataStream`,
68- // matching the previous pure-Swift implementation.
69- incoming = LiveKitUniFFI . IncomingDataStreamManager (
70- delegate: incomingDelegate,
71- maxPayloadByteLength: maxPayloadSize. map { UInt64 ( $0) } ,
72- )
7370 outgoing = LiveKitUniFFI . OutgoingDataStreamManager ( delegate: outgoingDelegate, registry: registry)
7471 super. init ( )
75- // The FFI manager retains its delegate strongly, so the delegate points back here weakly.
76- incomingDelegate. coordinator = self
72+ }
73+
74+ /// The incoming manager, created on first use with the room's current payload cap. Topic routing
75+ /// (incl. the `lk.rpc` guard) is handled Swift-side in `Room+DataStream`.
76+ private func incomingManager( ) -> LiveKitUniFFI . IncomingDataStreamManager {
77+ _incoming. mutate { existing in
78+ if let existing { return existing }
79+ let delegate = IncomingDelegate ( )
80+ delegate. coordinator = self
81+ // `nil` → the core's default cap. Read now (first packet, i.e. post-connect) so a
82+ // `maxPayloadSize` supplied via `connect(roomOptions:)` is honored.
83+ let maxPayloadSize = room? . _state. roomOptions. dataStreamOptions. maxPayloadSize
84+ let manager = LiveKitUniFFI . IncomingDataStreamManager (
85+ delegate: delegate,
86+ maxPayloadByteLength: maxPayloadSize. map { UInt64 ( $0) } ,
87+ )
88+ existing = manager
89+ return manager
90+ }
7791 }
7892
7993 // Room-level encryption type, stamped onto every stream info as it crosses the FFI boundary.
@@ -178,7 +192,7 @@ final class DataStreams: NSObject, @unchecked Sendable, Loggable {
178192 /// the incoming manager. The FFI re-decodes the serialized `DataPacket` itself.
179193 func handleIncoming( _ dataPacket: Livekit_DataPacket ) {
180194 guard let data = try ? dataPacket. serializedData ( ) else { return }
181- incoming . handlePacketReceived ( packet: data)
195+ incomingManager ( ) . handlePacketReceived ( packet: data)
182196 }
183197
184198 // MARK: - Stream lifecycle
@@ -187,13 +201,14 @@ final class DataStreams: NSObject, @unchecked Sendable, Loggable {
187201 /// on a reader that will never finish would otherwise stall its topic's ordered queue. Handler
188202 /// registrations survive, so streams arriving after a reconnect are still handled.
189203 func reset( ) {
190- incoming. abortAllStreams ( )
204+ // No-op if the incoming manager was never created (no packets received): nothing is open.
205+ _incoming. copy ( ) ? . abortAllStreams ( )
191206 }
192207
193208 /// Fails open incoming streams sent by `identity` (they disconnected mid-send), so their readers
194209 /// throw and their handlers return instead of hanging.
195210 func closeStreams( from identity: Participant . Identity ) {
196- incoming . abortStreamsFrom ( identity: identity. stringValue)
211+ _incoming . copy ( ) ? . abortStreamsFrom ( identity: identity. stringValue)
197212 }
198213
199214 // MARK: - Stream open dispatch (called from the incoming delegate)
0 commit comments