Skip to content

Commit 4d9dc5f

Browse files
committed
Fix 288ms UI hang in video publishing flow
1 parent 05690c0 commit 4d9dc5f

File tree

1 file changed

+49
-31
lines changed

1 file changed

+49
-31
lines changed

Sources/StreamVideo/WebRTC/v2/PeerConnection/MediaAdapters/LocalMediaAdapters/LocalVideoMediaAdapter.swift

Lines changed: 49 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ final class LocalVideoMediaAdapter: LocalMediaAdapting, @unchecked Sendable {
125125
/// Removes all transceivers from storage and logs details about the
126126
/// deallocation process.
127127
deinit {
128-
Task { @MainActor [transceiverStorage] in
128+
Task { [transceiverStorage] in
129129
transceiverStorage.removeAll()
130130
}
131131

@@ -207,35 +207,52 @@ final class LocalVideoMediaAdapter: LocalMediaAdapting, @unchecked Sendable {
207207

208208
/// Starts publishing the local video track.
209209
func publish() {
210-
processingQueue.async { @MainActor [weak self] in
210+
processingQueue.async { [weak self] in
211211
guard
212212
let self,
213213
!primaryTrack.isEnabled
214214
else {
215215
return
216216
}
217217
primaryTrack.isEnabled = true
218-
219-
do {
220-
try await startVideoCapturingSession()
221-
} catch {
222-
log.error(error)
218+
219+
// Don't wait for camera to start - do it in parallel
220+
Task { [weak self] in
221+
do {
222+
try await self?.startVideoCapturingSession()
223+
} catch {
224+
log.error("Failed to start video capturing session: \(error)")
225+
}
223226
}
224-
225-
publishOptions
226-
.forEach {
227-
self.addTransceiverIfRequired(
228-
for: $0,
229-
with: self
230-
.primaryTrack
231-
.clone(from: self.peerConnectionFactory)
232-
)
227+
228+
// Clone tracks and setup transceivers (don't block UI thread)
229+
Task { [weak self] in
230+
guard let self else { return }
231+
232+
// Clone tracks in parallel
233+
await withTaskGroup(of: (PublishOptions.VideoPublishOptions, RTCVideoTrack)?.self) { group in
234+
for option in self.publishOptions {
235+
group.addTask { [weak self] in
236+
guard let self else { return nil }
237+
238+
let clonedTrack = await Task.detached {
239+
self.primaryTrack.clone(from: self.peerConnectionFactory)
240+
}.value
241+
242+
return (option, clonedTrack)
243+
}
244+
}
245+
246+
// Add transceivers as clones complete (off main thread - thread safe!)
247+
for await result in group {
248+
guard let (option, track) = result else { continue }
249+
self.addTransceiverIfRequired(for: option, with: track)
250+
}
233251
}
234-
235-
let activePublishOptions = Set(self.publishOptions)
236-
237-
transceiverStorage
238-
.forEach {
252+
253+
// Update transceiver states (off main thread - thread safe!)
254+
let activePublishOptions = Set(self.publishOptions)
255+
self.transceiverStorage.forEach {
239256
if activePublishOptions.contains($0.key) {
240257
$0.value.track.isEnabled = true
241258
$0.value.transceiver.sender.track = $0.value.track
@@ -244,15 +261,16 @@ final class LocalVideoMediaAdapter: LocalMediaAdapting, @unchecked Sendable {
244261
$0.value.transceiver.sender.track = nil
245262
}
246263
}
247-
248-
log.debug(
249-
"""
250-
Local videoTracks are now published
251-
primary: \(primaryTrack.trackId) isEnabled:\(primaryTrack.isEnabled)
252-
clones: \(transceiverStorage.map(\.value.track.trackId).joined(separator: ","))
253-
""",
254-
subsystems: .webRTC
255-
)
264+
265+
log.debug(
266+
"""
267+
Local videoTracks are now published
268+
primary: \(self.primaryTrack.trackId) isEnabled:\(self.primaryTrack.isEnabled)
269+
clones: \(self.transceiverStorage.map(\.value.track.trackId).joined(separator: ","))
270+
""",
271+
subsystems: .webRTC
272+
)
273+
}
256274
}
257275
}
258276

@@ -271,7 +289,7 @@ final class LocalVideoMediaAdapter: LocalMediaAdapting, @unchecked Sendable {
271289
transceiverStorage
272290
.forEach { $0.value.track.isEnabled = false }
273291

274-
Task { @MainActor [weak self] in
292+
Task { [weak self] in
275293
do {
276294
try await self?.stopVideoCapturingSession()
277295
} catch {

0 commit comments

Comments
 (0)