From 598ebb1887adfe92ccb155cea82877d921c811cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Wed, 10 Jun 2026 10:09:45 +0200 Subject: [PATCH 1/4] Fix Xcode 27 beta build errors The macOS 27 / iOS 27 SDKs re-annotate several APIs with higher availability than the OS versions they actually ship in, which the Swift importer enforces as hard errors: - AVAudioNode.auAudioUnit: the Swift overlay is now introduced macOS 13.0 (the Objective-C property is still macos(10.13)) - SCStreamConfiguration.width/height: bumped from macos(12.3) to macos(13.0) - RPBroadcastSampleHandler: deprecated in iOS 27, which breaks the LKObjCHelpers precompiled module under -Werror Route AVAudioNode.maximumFramesToRender through LKObjCHelpers via an extension, where the original ObjC availability still applies, so it works on every SDK/OS version with no version check and no behavior change. Screen capture sets width/height through the same helper on Xcode 27 (#if compiler(>=6.4)) and directly on older toolchains. Also restores the macOS maximumFramesToRender matching that #1034 had to drop. Closes #1035 Closes #1037 Co-Authored-By: Claude Opus 4.8 (1M context) --- .changes/xcode-27-beta-build | 1 + Sources/LKObjCHelpers/LKObjCHelpers.m | 23 +++++++++++++ Sources/LKObjCHelpers/include/LKObjCHelpers.h | 22 ++++++++++++ .../LiveKit/Audio/MixerEngineObserver.swift | 34 +++++++++---------- Sources/LiveKit/Audio/PlayerNodePool.swift | 4 +-- Sources/LiveKit/Audio/SoundPlayer.swift | 2 +- Sources/LiveKit/Extensions/AVAudioNode.swift | 32 +++++++++++++++++ .../Track/Capturers/MacOSScreenCapturer.swift | 10 ++++++ 8 files changed, 108 insertions(+), 20 deletions(-) create mode 100644 .changes/xcode-27-beta-build create mode 100644 Sources/LiveKit/Extensions/AVAudioNode.swift diff --git a/.changes/xcode-27-beta-build b/.changes/xcode-27-beta-build new file mode 100644 index 000000000..c98bf5095 --- /dev/null +++ b/.changes/xcode-27-beta-build @@ -0,0 +1 @@ +patch type="fixed" "Fixed Xcode 27 beta build errors" diff --git a/Sources/LKObjCHelpers/LKObjCHelpers.m b/Sources/LKObjCHelpers/LKObjCHelpers.m index e240da3a0..8e36ac2d3 100644 --- a/Sources/LKObjCHelpers/LKObjCHelpers.m +++ b/Sources/LKObjCHelpers/LKObjCHelpers.m @@ -4,6 +4,8 @@ @implementation LKObjCHelpers NS_ASSUME_NONNULL_BEGIN +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + (void)finishBroadcastWithoutError:(RPBroadcastSampleHandler *)handler API_AVAILABLE(ios(10.0), macCatalyst(13.1), macos(11.0), tvos(10.0)) { // Call finishBroadcastWithError with nil error, which ends the broadcast without an error popup // This is unsupported/undocumented but appears to work and is preferable to an error dialog with a cryptic default message @@ -13,6 +15,7 @@ + (void)finishBroadcastWithoutError:(RPBroadcastSampleHandler *)handler API_AVAI [handler finishBroadcastWithError:nil]; #pragma clang diagnostic pop } +#pragma clang diagnostic pop + (BOOL)catchException:(void(^)(void))tryBlock error:(__autoreleasing NSError **)error { @try { @@ -25,6 +28,26 @@ + (BOOL)catchException:(void(^)(void))tryBlock error:(__autoreleasing NSError ** } } +// MARK: - Xcode 27 availability workarounds + ++ (AUAudioFrameCount)maximumFramesToRenderForNode:(AVAudioNode *)node { + return node.AUAudioUnit.maximumFramesToRender; +} + ++ (void)setMaximumFramesToRender:(AUAudioFrameCount)maximumFramesToRender forNode:(AVAudioNode *)node { + node.AUAudioUnit.maximumFramesToRender = maximumFramesToRender; +} + +#if TARGET_OS_OSX ++ (void)setWidth:(size_t)width height:(size_t)height onConfiguration:(SCStreamConfiguration *)configuration { + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wunguarded-availability-new" + configuration.width = width; + configuration.height = height; + #pragma clang diagnostic pop +} +#endif + NS_ASSUME_NONNULL_END @end diff --git a/Sources/LKObjCHelpers/include/LKObjCHelpers.h b/Sources/LKObjCHelpers/include/LKObjCHelpers.h index 714072661..6dca93c45 100644 --- a/Sources/LKObjCHelpers/include/LKObjCHelpers.h +++ b/Sources/LKObjCHelpers/include/LKObjCHelpers.h @@ -1,10 +1,32 @@ #import #import +#import +#import + +#if TARGET_OS_OSX +#import +#endif @interface LKObjCHelpers : NSObject +#pragma clang diagnostic push +// RPBroadcastSampleHandler is deprecated in the iOS 27 SDK; suppress so the module still builds (#1037). +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + (void)finishBroadcastWithoutError:(RPBroadcastSampleHandler *)handler API_AVAILABLE(ios(10.0), macCatalyst(13.1), macos(11.0), tvos(10.0)); +#pragma clang diagnostic pop + (BOOL)catchException:(void(^)(void))tryBlock error:(__autoreleasing NSError **)error; +// MARK: - Xcode 27 availability workarounds +// The macOS 27 SDK bumped these APIs past the OS versions they actually ship in (only the Swift +// importer enforces it). Reaching them from ObjC keeps full behavior on every SDK/OS version (#1035). + ++ (AUAudioFrameCount)maximumFramesToRenderForNode:(AVAudioNode *)node; + ++ (void)setMaximumFramesToRender:(AUAudioFrameCount)maximumFramesToRender forNode:(AVAudioNode *)node; + +#if TARGET_OS_OSX ++ (void)setWidth:(size_t)width height:(size_t)height onConfiguration:(SCStreamConfiguration *)configuration API_AVAILABLE(macos(12.3)); +#endif + @end diff --git a/Sources/LiveKit/Audio/MixerEngineObserver.swift b/Sources/LiveKit/Audio/MixerEngineObserver.swift index e5417acf0..3e4e2c29e 100644 --- a/Sources/LiveKit/Audio/MixerEngineObserver.swift +++ b/Sources/LiveKit/Audio/MixerEngineObserver.swift @@ -105,7 +105,6 @@ public final class MixerEngineObserver: AudioEngineObserver, Loggable { ($0.appNode, $0.appMixerNode, $0.micNode, $0.micMixerNode, $0.soundPlayerNodes) } - #if os(iOS) || os(visionOS) || os(tvOS) // Match the outputNode's maximumFramesToRender so our nodes can handle the same // buffer sizes the engine expects, preventing kAudioUnitErr_TooManyFramesToProcess (-10874). // Must be set before render resources are allocated (i.e. before attach/connect/start). @@ -118,28 +117,29 @@ public final class MixerEngineObserver: AudioEngineObserver, Loggable { // must move into the WebRTC layer (audio_engine_device.mm). // See: https://developer.apple.com/documentation/audiotoolbox/auaudiounit/maximumframestorender // See: https://developer.apple.com/forums/thread/111968 - let maxFrames = engine.outputNode.auAudioUnit.maximumFramesToRender + let maxFrames = engine.outputNode.maximumFramesToRender log("maximumFramesToRender before: " + - "appNode=\(appNode.auAudioUnit.maximumFramesToRender), " + - "appMixerNode=\(appMixerNode.auAudioUnit.maximumFramesToRender), " + - "micNode=\(micNode.auAudioUnit.maximumFramesToRender), " + - "micMixerNode=\(micMixerNode.auAudioUnit.maximumFramesToRender), " + - "soundPlayerMixerNode=\(soundPlayerNodes.mixerNode.auAudioUnit.maximumFramesToRender)", .debug) - - appNode.auAudioUnit.maximumFramesToRender = maxFrames - appMixerNode.auAudioUnit.maximumFramesToRender = maxFrames - micNode.auAudioUnit.maximumFramesToRender = maxFrames - micMixerNode.auAudioUnit.maximumFramesToRender = maxFrames + "appNode=\(appNode.maximumFramesToRender), " + + "appMixerNode=\(appMixerNode.maximumFramesToRender), " + + "micNode=\(micNode.maximumFramesToRender), " + + "micMixerNode=\(micMixerNode.maximumFramesToRender), " + + "soundPlayerMixerNode=\(soundPlayerNodes.mixerNode.maximumFramesToRender)", .debug) + + appNode.maximumFramesToRender = maxFrames + appMixerNode.maximumFramesToRender = maxFrames + micNode.maximumFramesToRender = maxFrames + micMixerNode.maximumFramesToRender = maxFrames soundPlayerNodes.setMaximumFramesToRender(maxFrames) log("maximumFramesToRender setting to \(maxFrames): " + - "appNode=\(appNode.auAudioUnit.maximumFramesToRender), " + - "appMixerNode=\(appMixerNode.auAudioUnit.maximumFramesToRender), " + - "micNode=\(micNode.auAudioUnit.maximumFramesToRender), " + - "micMixerNode=\(micMixerNode.auAudioUnit.maximumFramesToRender), " + - "soundPlayerMixerNode=\(soundPlayerNodes.mixerNode.auAudioUnit.maximumFramesToRender)", .debug) + "appNode=\(appNode.maximumFramesToRender), " + + "appMixerNode=\(appMixerNode.maximumFramesToRender), " + + "micNode=\(micNode.maximumFramesToRender), " + + "micMixerNode=\(micMixerNode.maximumFramesToRender), " + + "soundPlayerMixerNode=\(soundPlayerNodes.mixerNode.maximumFramesToRender)", .debug) + #if os(iOS) || os(visionOS) || os(tvOS) let config = LKRTCAudioSessionConfiguration.webRTC() log("webRTCPreferredFrames=\(AVAudioFrameCount(config.sampleRate * config.ioBufferDuration))", .debug) #endif diff --git a/Sources/LiveKit/Audio/PlayerNodePool.swift b/Sources/LiveKit/Audio/PlayerNodePool.swift index 8511ecc3a..4d28e6351 100644 --- a/Sources/LiveKit/Audio/PlayerNodePool.swift +++ b/Sources/LiveKit/Audio/PlayerNodePool.swift @@ -122,9 +122,9 @@ class AVAudioPlayerNodePool: @unchecked Sendable, Loggable { func setMaximumFramesToRender(_ maxFrames: AUAudioFrameCount) { executionQueue.sync { - mixerNode.auAudioUnit.maximumFramesToRender = maxFrames + mixerNode.maximumFramesToRender = maxFrames for item in items { - item.node.auAudioUnit.maximumFramesToRender = maxFrames + item.node.maximumFramesToRender = maxFrames } } } diff --git a/Sources/LiveKit/Audio/SoundPlayer.swift b/Sources/LiveKit/Audio/SoundPlayer.swift index 1f6733a5e..25581dab9 100644 --- a/Sources/LiveKit/Audio/SoundPlayer.swift +++ b/Sources/LiveKit/Audio/SoundPlayer.swift @@ -172,7 +172,7 @@ extension SoundPlayer { playerNodePool.stop() engine.stop() engine.disconnect(playerNodePool) - playerNodePool.setMaximumFramesToRender(engine.outputNode.auAudioUnit.maximumFramesToRender) + playerNodePool.setMaximumFramesToRender(engine.outputNode.maximumFramesToRender) engine.connect(playerNodePool, to: engine.mainMixerNode, format: outputFormat, playerNodeFormat: playerNodeFormat) try engine.start() diff --git a/Sources/LiveKit/Extensions/AVAudioNode.swift b/Sources/LiveKit/Extensions/AVAudioNode.swift new file mode 100644 index 000000000..94e799eab --- /dev/null +++ b/Sources/LiveKit/Extensions/AVAudioNode.swift @@ -0,0 +1,32 @@ +/* + * Copyright 2026 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 AVFAudio + +#if !COCOAPODS +internal import LKObjCHelpers +#endif + +extension AVAudioNode { + /// The underlying audio unit's `maximumFramesToRender`. + /// + /// Routed through ``LKObjCHelpers`` so it stays reachable across SDK versions: the macOS 27 SDK's + /// Swift overlay bumped `auAudioUnit` to macOS 13.0, but the Objective-C property is still macOS 10.13. + var maximumFramesToRender: AUAudioFrameCount { + get { LKObjCHelpers.maximumFramesToRender(for: self) } + set { LKObjCHelpers.setMaximumFramesToRender(newValue, for: self) } + } +} diff --git a/Sources/LiveKit/Track/Capturers/MacOSScreenCapturer.swift b/Sources/LiveKit/Track/Capturers/MacOSScreenCapturer.swift index 02b2a73eb..685f3b94e 100644 --- a/Sources/LiveKit/Track/Capturers/MacOSScreenCapturer.swift +++ b/Sources/LiveKit/Track/Capturers/MacOSScreenCapturer.swift @@ -25,6 +25,10 @@ import ScreenCaptureKit internal import LiveKitWebRTC +#if compiler(>=6.4) && !COCOAPODS +internal import LKObjCHelpers +#endif + #if os(macOS) @available(macOS 12.3, *) @@ -89,8 +93,14 @@ public class MacOSScreenCapturer: VideoCapturer, @unchecked Sendable { let mainDisplay = CGMainDisplayID() // try to capture in max resolution + #if compiler(>=6.4) + LKObjCHelpers.setWidth(CGDisplayPixelsWide(mainDisplay) * 2, + height: CGDisplayPixelsHigh(mainDisplay) * 2, + on: configuration) + #else configuration.width = CGDisplayPixelsWide(mainDisplay) * 2 configuration.height = CGDisplayPixelsHigh(mainDisplay) * 2 + #endif configuration.scalesToFit = false configuration.minimumFrameInterval = CMTime(value: 1, timescale: CMTimeScale(options.fps)) From 162799dc4c960208f1e5bc244d93b1830ea04278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:07:14 +0200 Subject: [PATCH 2/4] Prefer withAUAudioUnit for maximumFramesToRender where available On the Xcode 27 SDK, use the non-deprecated withAUAudioUnit (macOS/iOS/tvOS/visionOS 27+) and fall back to the LKObjCHelpers ObjC workaround only on earlier OSes. Toolchains before Swift 6.4 keep the direct auAudioUnit property. Co-Authored-By: Claude Opus 4.8 (1M context) --- Sources/LiveKit/Extensions/AVAudioNode.swift | 31 ++++++++++++++++---- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/Sources/LiveKit/Extensions/AVAudioNode.swift b/Sources/LiveKit/Extensions/AVAudioNode.swift index 94e799eab..840c38b46 100644 --- a/Sources/LiveKit/Extensions/AVAudioNode.swift +++ b/Sources/LiveKit/Extensions/AVAudioNode.swift @@ -16,17 +16,38 @@ import AVFAudio -#if !COCOAPODS +#if compiler(>=6.4) && !COCOAPODS internal import LKObjCHelpers #endif extension AVAudioNode { /// The underlying audio unit's `maximumFramesToRender`. /// - /// Routed through ``LKObjCHelpers`` so it stays reachable across SDK versions: the macOS 27 SDK's - /// Swift overlay bumped `auAudioUnit` to macOS 13.0, but the Objective-C property is still macOS 10.13. + /// The Xcode 27 SDK restricts `auAudioUnit` to macOS 13.0 and deprecates it in favor of + /// `withAUAudioUnit`, so that is used where available; older OSes fall back to ``LKObjCHelpers``, + /// which keeps the property's original Objective-C availability (macOS 10.13). See #1035. var maximumFramesToRender: AUAudioFrameCount { - get { LKObjCHelpers.maximumFramesToRender(for: self) } - set { LKObjCHelpers.setMaximumFramesToRender(newValue, for: self) } + get { + #if compiler(>=6.4) + if #available(macOS 27.0, iOS 27.0, tvOS 27.0, visionOS 27.0, *) { + return withAUAudioUnit { $0.maximumFramesToRender } + } else { + return LKObjCHelpers.maximumFramesToRender(for: self) + } + #else + return auAudioUnit.maximumFramesToRender + #endif + } + set { + #if compiler(>=6.4) + if #available(macOS 27.0, iOS 27.0, tvOS 27.0, visionOS 27.0, *) { + withAUAudioUnit { $0.maximumFramesToRender = newValue } + } else { + LKObjCHelpers.setMaximumFramesToRender(newValue, for: self) + } + #else + auAudioUnit.maximumFramesToRender = newValue + #endif + } } } From d0afa8bdc96c24c3be3e255e9e62deb15a3557a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Wed, 10 Jun 2026 12:08:21 +0200 Subject: [PATCH 3/4] Silence pre-existing build warnings surfaced on Xcode 27 / newer SDKs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BroadcastUploader: bare `Task { try await … }` → `Task.discarding` (#NoUseUnstructuredThrowingTask is active under Swift 6.4) - BroadcastAudioCodec: qualify the retroactive `AudioStreamBasicDescription` `Codable` conformance as `Swift.Codable`, matching the `Sendable.swift` convention for suppressing the imported-conformance warning - Utils.modelIdentifier: `kIOMasterPortDefault` → `kIOMainPortDefault`, guarded with the NULL default port (0) before macOS 12 Co-Authored-By: Claude Opus 4.8 (1M context) --- Sources/LiveKit/Broadcast/IPC/BroadcastAudioCodec.swift | 2 +- Sources/LiveKit/Broadcast/IPC/BroadcastUploader.swift | 4 ++-- Sources/LiveKit/Support/Utils.swift | 7 ++++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Sources/LiveKit/Broadcast/IPC/BroadcastAudioCodec.swift b/Sources/LiveKit/Broadcast/IPC/BroadcastAudioCodec.swift index 566170574..8532f8e0b 100644 --- a/Sources/LiveKit/Broadcast/IPC/BroadcastAudioCodec.swift +++ b/Sources/LiveKit/Broadcast/IPC/BroadcastAudioCodec.swift @@ -93,7 +93,7 @@ struct BroadcastAudioCodec { } } -extension AudioStreamBasicDescription: Codable { +extension AudioStreamBasicDescription: Swift.Codable { public func encode(to encoder: any Encoder) throws { var container = encoder.unkeyedContainer() try container.encode(mSampleRate) diff --git a/Sources/LiveKit/Broadcast/IPC/BroadcastUploader.swift b/Sources/LiveKit/Broadcast/IPC/BroadcastUploader.swift index 62101f3d5..83774d4c7 100644 --- a/Sources/LiveKit/Broadcast/IPC/BroadcastUploader.swift +++ b/Sources/LiveKit/Broadcast/IPC/BroadcastUploader.swift @@ -83,7 +83,7 @@ final class BroadcastUploader: Sendable, Loggable { let rotation = VideoRotation(sampleBuffer.replayKitOrientation ?? .up) do { let (metadata, imageData) = try imageCodec.encode(sampleBuffer) - Task { + Task.discarding { let header = BroadcastIPCHeader.image(metadata, rotation) try await channel.send(header: header, payload: imageData) state.mutate { $0.isUploadingImage = false } @@ -95,7 +95,7 @@ final class BroadcastUploader: Sendable, Loggable { case .audioApp: guard state.shouldUploadAudio else { return } let (metadata, audioData) = try audioCodec.encode(sampleBuffer) - Task { + Task.discarding { let header = BroadcastIPCHeader.audio(metadata) try await channel.send(header: header, payload: audioData) } diff --git a/Sources/LiveKit/Support/Utils.swift b/Sources/LiveKit/Support/Utils.swift index 89b0cf289..8f875c5f7 100644 --- a/Sources/LiveKit/Support/Utils.swift +++ b/Sources/LiveKit/Support/Utils.swift @@ -96,7 +96,12 @@ class Utils: Loggable { } return identifier #elseif os(macOS) - let service = IOServiceGetMatchingService(kIOMasterPortDefault, + let mainPort: mach_port_t = if #available(macOS 12.0, *) { + kIOMainPortDefault + } else { + 0 // kIOMainPortDefault (macOS 12+) is a synonym for the NULL default port + } + let service = IOServiceGetMatchingService(mainPort, IOServiceMatching("IOPlatformExpertDevice")) defer { IOObjectRelease(service) } From bf7db9a3aa6a8bdb57fc1e7f90524a09e1c6d6e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Wed, 10 Jun 2026 12:30:06 +0200 Subject: [PATCH 4/4] =?UTF-8?q?Use=20auAudioUnit=20directly=20on=20macOS?= =?UTF-8?q?=2013=E2=80=9326=20in=20maximumFramesToRender?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Narrows the LKObjCHelpers fallback to macOS < 13: withAUAudioUnit on OS 27+, the auAudioUnit property on macOS 13–26 (available and not yet deprecated there), and the ObjC shim only below macOS 13. Co-Authored-By: Claude Opus 4.8 (1M context) --- Sources/LiveKit/Extensions/AVAudioNode.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Sources/LiveKit/Extensions/AVAudioNode.swift b/Sources/LiveKit/Extensions/AVAudioNode.swift index 840c38b46..b3b35d3d6 100644 --- a/Sources/LiveKit/Extensions/AVAudioNode.swift +++ b/Sources/LiveKit/Extensions/AVAudioNode.swift @@ -24,13 +24,16 @@ extension AVAudioNode { /// The underlying audio unit's `maximumFramesToRender`. /// /// The Xcode 27 SDK restricts `auAudioUnit` to macOS 13.0 and deprecates it in favor of - /// `withAUAudioUnit`, so that is used where available; older OSes fall back to ``LKObjCHelpers``, - /// which keeps the property's original Objective-C availability (macOS 10.13). See #1035. + /// `withAUAudioUnit`, so this prefers `withAUAudioUnit` on OS 27+, uses the `auAudioUnit` property + /// on macOS 13–26, and falls back to ``LKObjCHelpers`` (original ObjC availability, macOS 10.13) + /// below macOS 13. See #1035. var maximumFramesToRender: AUAudioFrameCount { get { #if compiler(>=6.4) if #available(macOS 27.0, iOS 27.0, tvOS 27.0, visionOS 27.0, *) { return withAUAudioUnit { $0.maximumFramesToRender } + } else if #available(macOS 13.0, *) { + return auAudioUnit.maximumFramesToRender } else { return LKObjCHelpers.maximumFramesToRender(for: self) } @@ -42,6 +45,8 @@ extension AVAudioNode { #if compiler(>=6.4) if #available(macOS 27.0, iOS 27.0, tvOS 27.0, visionOS 27.0, *) { withAUAudioUnit { $0.maximumFramesToRender = newValue } + } else if #available(macOS 13.0, *) { + auAudioUnit.maximumFramesToRender = newValue } else { LKObjCHelpers.setMaximumFramesToRender(newValue, for: self) }