Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changes/objc-async-checked-continuation
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
patch type="fixed" "Wrap ObjC completion-handler async calls in explicit checked continuations (fixes mixed Swift 5/6 continuation-bridge EXC_BAD_ACCESS)"
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ private static let playAndRecordOptions: AVAudioSession.CategoryOptions = [.mixW
- Only `Task.discarding` preserves caller actor isolation in its closure; other `Support/Async/` helpers (`Task.retrying`, `AsyncSerialDelegate.notifyAsync`, etc.) run their bodies nonisolated — hop explicitly with `await MainActor.run { ... }` when UI-bound work is needed
- Use async primitives in `Support/Async` and `Support/Schedulers` when operation order matters
- Prefer native Swift async/await over `Combine` for new code
- Until the minimum supported compiler is Swift 6.3, wrap calls to imported Objective-C completion-handler methods made via their synthesized `async` overload in an explicit `withCheckedThrowingContinuation`, since the bare auto-bridge hits a mixed Swift 5/6 thunk-coalescing crash (swiftlang/swift#81846) fixed in 6.3

### Error Handling

Expand Down
7 changes: 6 additions & 1 deletion Sources/LiveKit/Core/Transport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,12 @@ actor Transport: NSObject, Loggable {
guard let self else { return }

do {
try await _pc.add(iceCandidate.toRTCType())
let rtcCandidate = iceCandidate.toRTCType()
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
self._pc.add(rtcCandidate) { error in
if let error { continuation.resume(throwing: error) } else { continuation.resume() }
}
}
} catch {
log("Failed to add(iceCandidate:) with error: \(error)", .error)
}
Expand Down
12 changes: 10 additions & 2 deletions Sources/LiveKit/Track/Capturers/CameraCapturer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,11 @@ public class CameraCapturer: VideoCapturer, @unchecked Sendable {

log("starting camera capturer device: \(device), format: \(selectedFormat), fps: \(selectedFps)(\(fpsRange))", .info)

try await capturer.startCapture(with: device, format: selectedFormat.format, fps: selectedFps)
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
capturer.startCapture(with: device, format: selectedFormat.format, fps: selectedFps) { error in
if let error { continuation.resume(throwing: error) } else { continuation.resume() }
}
}

// Update internal vars
_cameraCapturerState.mutate {
Expand All @@ -305,7 +309,11 @@ public class CameraCapturer: VideoCapturer, @unchecked Sendable {
// Already stopped
guard didStop else { return false }

await capturer.stopCapture()
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
capturer.stopCapture {
continuation.resume()
}
}

// Update internal vars
set(dimensions: nil)
Expand Down
14 changes: 9 additions & 5 deletions Sources/LiveKit/Track/Capturers/InAppCapturer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,15 @@ public class InAppScreenCapturer: VideoCapturer, @unchecked Sendable {
guard didStart else { return false }

// TODO: force pixel format kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
try await RPScreenRecorder.shared().startCapture { [weak self] sampleBuffer, type, _ in
guard let self else { return }
// Only process .video
if type == .video {
capture(sampleBuffer: sampleBuffer, capturer: capturer, options: options)
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
RPScreenRecorder.shared().startCapture { [weak self] sampleBuffer, type, _ in
guard let self else { return }
// Only process .video
if type == .video {
capture(sampleBuffer: sampleBuffer, capturer: capturer, options: options)
}
} completionHandler: { error in
if let error { continuation.resume(throwing: error) } else { continuation.resume() }
}
}

Expand Down
Loading