diff --git a/.changes/sendable-extension b/.changes/sendable-extension new file mode 100644 index 000000000..b2dd5251d --- /dev/null +++ b/.changes/sendable-extension @@ -0,0 +1 @@ +patch type="changed" "Removed '@unchecked Sendable' extensions on common types" diff --git a/Sources/LiveKit/Extensions/Sendable.swift b/Sources/LiveKit/Extensions/Sendable.swift index e7ebcc194..7cd1187dd 100644 --- a/Sources/LiveKit/Extensions/Sendable.swift +++ b/Sources/LiveKit/Extensions/Sendable.swift @@ -48,8 +48,6 @@ extension LKRTCCallbackLogger: @unchecked Swift.Sendable {} // MARK: Collections -extension NSHashTable: @unchecked Swift.Sendable {} // cannot specify Obj-C generics -extension NSMapTable: @unchecked Swift.Sendable {} // cannot specify Obj-C generics #if swift(<6.2) extension Dictionary: Swift.Sendable where Key: Sendable, Value: Sendable {} #endif @@ -58,4 +56,3 @@ extension Dictionary: Swift.Sendable where Key: Sendable, Value: Sendable {} extension AVCaptureDevice: @unchecked Swift.Sendable {} extension AVCaptureDevice.Format: @unchecked Swift.Sendable {} -extension CVPixelBuffer: @unchecked Swift.Sendable {} diff --git a/Sources/LiveKit/Support/MapTable.swift b/Sources/LiveKit/Support/MapTable.swift new file mode 100644 index 000000000..737cea08b --- /dev/null +++ b/Sources/LiveKit/Support/MapTable.swift @@ -0,0 +1,54 @@ +/* + * Copyright 2025 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// A thin unchecked sendable wrapper around NSMapTable. +final class MapTable: @unchecked Sendable where KeyType: AnyObject, ObjectType: AnyObject { + init(_ mapTable: NSMapTable) { + self.mapTable = mapTable + } + + class func weakToStrongObjects() -> MapTable { + .init(.weakToStrongObjects()) + } + + func object(forKey aKey: KeyType?) -> ObjectType? { + mapTable.object(forKey: aKey) + } + + func removeObject(forKey aKey: KeyType?) { + mapTable.removeObject(forKey: aKey) + } + + func setObject(_ anObject: ObjectType?, forKey aKey: KeyType?) { + mapTable.setObject(anObject, forKey: aKey) + } + + var count: Int { + mapTable.count + } + + func objectEnumerator() -> NSEnumerator? { + mapTable.objectEnumerator() + } + + func removeAllObjects() { + mapTable.removeAllObjects() + } + + private let mapTable: NSMapTable +} diff --git a/Sources/LiveKit/Track/Track.swift b/Sources/LiveKit/Track/Track.swift index 9fa6bba33..017605945 100644 --- a/Sources/LiveKit/Track/Track.swift +++ b/Sources/LiveKit/Track/Track.swift @@ -128,7 +128,7 @@ public class Track: NSObject, @unchecked Sendable, Loggable { var rtpReceiver: LKRTCRtpReceiver? // All VideoRendererAdapters attached to this track, key/value for direct removal. - var videoRendererAdapters = NSMapTable.weakToStrongObjects() + var videoRendererAdapters = MapTable.weakToStrongObjects() } let _state: StateSync diff --git a/Sources/LiveKit/Types/Options/CameraCaptureOptions.swift b/Sources/LiveKit/Types/Options/CameraCaptureOptions.swift index ff4c3c8f3..84642b894 100644 --- a/Sources/LiveKit/Types/Options/CameraCaptureOptions.swift +++ b/Sources/LiveKit/Types/Options/CameraCaptureOptions.swift @@ -25,7 +25,7 @@ public final class CameraCaptureOptions: NSObject, VideoCaptureOptions, Sendable public let deviceType: AVCaptureDevice.DeviceType? #endif - /// Exact devce to use. + /// Exact device to use. @objc public let device: AVCaptureDevice? diff --git a/Sources/LiveKit/VideoProcessors/BackgroundBlurVideoProcessor.swift b/Sources/LiveKit/VideoProcessors/BackgroundBlurVideoProcessor.swift index 6edb302da..6907dd1f5 100644 --- a/Sources/LiveKit/VideoProcessors/BackgroundBlurVideoProcessor.swift +++ b/Sources/LiveKit/VideoProcessors/BackgroundBlurVideoProcessor.swift @@ -133,6 +133,10 @@ public final class BackgroundBlurVideoProcessor: NSObject, @unchecked Sendable, private func cacheMask(inputBuffer: CVPixelBuffer, inputDimensions: CGSize) { guard frameCount % segmentationFrameInterval == 0 else { return } + struct PixelBufferHolder: @unchecked Sendable { + let buffer: CVPixelBuffer + } + let inputBuffer = PixelBufferHolder(buffer: inputBuffer) segmentationQueue.async { #if LK_SIGNPOSTS os_signpost(.begin, log: self.signpostLog, name: #function) @@ -140,7 +144,7 @@ public final class BackgroundBlurVideoProcessor: NSObject, @unchecked Sendable, os_signpost(.end, log: self.signpostLog, name: #function) } #endif - try? self.segmentationRequestHandler.perform([self.segmentationRequest], on: inputBuffer) + try? self.segmentationRequestHandler.perform([self.segmentationRequest], on: inputBuffer.buffer) guard let maskPixelBuffer = self.segmentationRequest.results?.first?.pixelBuffer else { return } let maskImage = CIImage(cvPixelBuffer: maskPixelBuffer)