diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5d7c2fc..9a74103 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,14 +19,14 @@ jobs: fail-fast: false matrix: include: - - platform: "iOS Simulator,name=iPhone 17 Pro,OS=26.4.1" + - platform: "iOS Simulator,name=iPhone 17 Pro,OS=26.5" - platform: "macOS" steps: - uses: actions/checkout@v6 - uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: 26.4 + xcode-version: 26.5 - name: Resolve Packages run: | diff --git a/.gitignore b/.gitignore index c8e815a..fdda6c6 100644 --- a/.gitignore +++ b/.gitignore @@ -92,3 +92,6 @@ iOSInjectionProject/ # Ignore license files *.license + +# Claude Code +.claude/ diff --git a/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved b/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4e6169b..2f915e7 100644 --- a/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,6 +1,15 @@ { - "originHash" : "aea8f28b98f95e948f09d2d3f56fa02d451e94fb164d44b0b7fa44e15cf7fae5", + "originHash" : "619822ad96a3acbd3c79418fb8ac2ae63c9e2b1a552f2363e0e88475c819709a", "pins" : [ + { + "identity" : "components-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/livekit/components-swift", + "state" : { + "revision" : "9f28db3ae5d2b51f3033ea725c952ddacb5657d0", + "version" : "0.1.7" + } + }, { "identity" : "keychainaccess", "kind" : "remoteSourceControl", @@ -60,8 +69,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/livekit/webrtc-xcframework.git", "state" : { - "revision" : "6a5f458cf55a598e4590cb30b739ab545fdbfcb1", - "version" : "144.7559.6" + "revision" : "53d268c89d242791f0771fb022fe4b423f2263d9", + "version" : "144.7559.8" } } ], diff --git a/LiveKitExample.xcodeproj/project.pbxproj b/LiveKitExample.xcodeproj/project.pbxproj index b068543..9f51c09 100644 --- a/LiveKitExample.xcodeproj/project.pbxproj +++ b/LiveKitExample.xcodeproj/project.pbxproj @@ -36,6 +36,7 @@ 7BBEBA832D791CB300586EC4 /* CIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBEBA822D791CAF00586EC4 /* CIImage.swift */; }; 7BBEBA892D79219600586EC4 /* LKButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBEBA882D79219600586EC4 /* LKButton.swift */; }; 7BBEBA8B2D7921AA00586EC4 /* LKTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBEBA8A2D7921AA00586EC4 /* LKTextField.swift */; }; + 7BBEBA9A2D79219600586EC4 /* View+OnChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBEBA9B2D79219600586EC4 /* View+OnChange.swift */; }; B5BCF77E2CFE7FDE00BCD4D8 /* BroadcastExt.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 683F05F3273F96B20080C7AC /* BroadcastExt.appex */; platformFilters = (ios, tvos, xros, ); settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; B5BCF7842CFE859A00BCD4D8 /* LiveKit in Frameworks */ = {isa = PBXBuildFile; productRef = B5BCF7832CFE859A00BCD4D8 /* LiveKit */; }; B5C2EF162D0114C800FAC766 /* LiveKitComponents in Frameworks */ = {isa = PBXBuildFile; productRef = B5C2EF152D0114C800FAC766 /* LiveKitComponents */; }; @@ -101,6 +102,7 @@ 7BBEBA822D791CAF00586EC4 /* CIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImage.swift; sourceTree = ""; }; 7BBEBA882D79219600586EC4 /* LKButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LKButton.swift; sourceTree = ""; }; 7BBEBA8A2D7921AA00586EC4 /* LKTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LKTextField.swift; sourceTree = ""; }; + 7BBEBA9B2D79219600586EC4 /* View+OnChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+OnChange.swift"; sourceTree = ""; }; 9E7835E62751A71500559DEC /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.0.sdk/System/Library/Frameworks/CoreGraphics.framework; sourceTree = DEVELOPER_DIR; }; D7AA477A285A0FFC00EB41AE /* SampleHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleHandler.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -247,6 +249,7 @@ 7BBEBA822D791CAF00586EC4 /* CIImage.swift */, 68A50ECB2C4C1ED500D2DE17 /* Binding+OptionSet.swift */, 68A50ECC2C4C1ED500D2DE17 /* Bundle.swift */, + 7BBEBA9B2D79219600586EC4 /* View+OnChange.swift */, ); path = Extensions; sourceTree = ""; @@ -398,6 +401,7 @@ 68A50EE82C4C1ED500D2DE17 /* ScreenShareSourcePickerView.swift in Sources */, 68A50EEA2C4C1ED500D2DE17 /* Participant+Helpers.swift in Sources */, 68A50EEB2C4C1ED500D2DE17 /* Binding+OptionSet.swift in Sources */, + 7BBEBA9A2D79219600586EC4 /* View+OnChange.swift in Sources */, 6888FBE12C66B7B400AB93C1 /* ImmersiveView.swift in Sources */, 68A50EEC2C4C1ED500D2DE17 /* ParticipantView.swift in Sources */, 7BBEBA832D791CB300586EC4 /* CIImage.swift in Sources */, @@ -646,7 +650,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 20260504; + CURRENT_PROJECT_VERSION = 20260609; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 76TVFCUKK7; @@ -668,7 +672,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MARKETING_VERSION = 2.14.0; + MARKETING_VERSION = 2.15.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -715,7 +719,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 20260504; + CURRENT_PROJECT_VERSION = 20260609; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 76TVFCUKK7; @@ -732,7 +736,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MARKETING_VERSION = 2.14.0; + MARKETING_VERSION = 2.15.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES; @@ -797,7 +801,7 @@ repositoryURL = "https://github.com/livekit/client-sdk-swift"; requirement = { kind = exactVersion; - version = 2.14.1; + version = 2.15.0; }; }; B5C2EF142D0114C800FAC766 /* XCRemoteSwiftPackageReference "components-swift" */ = { diff --git a/LiveKitExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/LiveKitExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4b7c70e..7acafab 100644 --- a/LiveKitExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/LiveKitExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/livekit/client-sdk-swift", "state" : { - "revision" : "326eefab4e61ac56e1330edf0ce0a33e8eb7bde7", - "version" : "2.14.1" + "revision" : "9859018394f1b34e3e04ae859debce96d855e1d8", + "version" : "2.15.0" } }, { @@ -60,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/livekit/webrtc-xcframework.git", "state" : { - "revision" : "6a5f458cf55a598e4590cb30b739ab545fdbfcb1", - "version" : "144.7559.6" + "revision" : "53d268c89d242791f0771fb022fe4b423f2263d9", + "version" : "144.7559.8" } } ], diff --git a/Multiplatform/Extensions/View+OnChange.swift b/Multiplatform/Extensions/View+OnChange.swift new file mode 100644 index 0000000..b777f6e --- /dev/null +++ b/Multiplatform/Extensions/View+OnChange.swift @@ -0,0 +1,27 @@ +/* + * 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 SwiftUI + +#if !os(tvOS) && !os(visionOS) +extension View { + @available(iOS, introduced: 14.0, obsoleted: 17.0) + @available(macOS, introduced: 11.0, obsoleted: 14.0) + func onChange(of value: some Equatable, _ action: @escaping () -> Void) -> some View { + onChange(of: value) { _ in action() } + } +} +#endif diff --git a/Multiplatform/Views/ConnectView.swift b/Multiplatform/Views/ConnectView.swift index c17904e..f27e612 100644 --- a/Multiplatform/Views/ConnectView.swift +++ b/Multiplatform/Views/ConnectView.swift @@ -97,8 +97,12 @@ struct ConnectView: View { LKButton(title: "Connect") { Task { @MainActor in - let room = try await roomCtx.connect() - appCtx.connectionHistory.update(room: room, e2ee: roomCtx.isE2eeEnabled, e2eeKey: roomCtx.e2eeKey) + do { + let room = try await roomCtx.connect() + appCtx.connectionHistory.update(room: room, e2ee: roomCtx.isE2eeEnabled, e2eeKey: roomCtx.e2eeKey) + } catch { + print("Failed to connect: \(error)") + } } } @@ -107,8 +111,12 @@ struct ConnectView: View { ForEach(appCtx.connectionHistory.sortedByUpdated) { entry in Button { Task { @MainActor in - let room = try await roomCtx.connect(entry: entry) - appCtx.connectionHistory.update(room: room, e2ee: roomCtx.isE2eeEnabled, e2eeKey: roomCtx.e2eeKey) + do { + let room = try await roomCtx.connect(entry: entry) + appCtx.connectionHistory.update(room: room, e2ee: roomCtx.isE2eeEnabled, e2eeKey: roomCtx.e2eeKey) + } catch { + print("Failed to connect: \(error)") + } } } label: { Image(systemSymbol: .boltFill) diff --git a/Multiplatform/Views/MessagesPanel.swift b/Multiplatform/Views/MessagesPanel.swift index 8b42f9f..178abea 100644 --- a/Multiplatform/Views/MessagesPanel.swift +++ b/Multiplatform/Views/MessagesPanel.swift @@ -37,9 +37,9 @@ struct MessagesPanel: View { .onAppear(perform: { scrollToBottom(scrollView) }) - .onChange(of: roomCtx.messages, perform: { _ in + .onChange(of: roomCtx.messages) { scrollToBottom(scrollView) - }) + } .frame( minWidth: 0, maxWidth: .infinity, diff --git a/Multiplatform/Views/ParticipantView.swift b/Multiplatform/Views/ParticipantView.swift index c320e32..969f986 100644 --- a/Multiplatform/Views/ParticipantView.swift +++ b/Multiplatform/Views/ParticipantView.swift @@ -141,11 +141,11 @@ struct ParticipantView: View { Menu { if case .subscribed = remotePub.subscriptionState { Button("Unsubscribe") { - Task { try await remotePub.set(subscribed: false) } + Task { try? await remotePub.set(subscribed: false) } } } else if case .unsubscribed = remotePub.subscriptionState { Button("Subscribe") { - Task { try await remotePub.set(subscribed: true) } + Task { try? await remotePub.set(subscribed: true) } } } } label: { @@ -185,11 +185,11 @@ struct ParticipantView: View { Menu { if case .subscribed = remotePub.subscriptionState { Button("Unsubscribe") { - Task { try await remotePub.set(subscribed: false) } + Task { try? await remotePub.set(subscribed: false) } } } else if case .unsubscribed = remotePub.subscriptionState { Button("Subscribe") { - Task { try await remotePub.set(subscribed: true) } + Task { try? await remotePub.set(subscribed: true) } } } } label: { @@ -221,11 +221,13 @@ struct ParticipantView: View { .foregroundColor(Color.white) } + #if !os(tvOS) ForEach(remoteAudioTracks) { remoteAudioTrack in RemoteAudioVolumeControl(track: remoteAudioTrack, showsPercentage: geometry.size.width > 180) .fixedSize() } + #endif if participant.connectionQuality == .excellent { Image(systemSymbol: .wifi) @@ -267,6 +269,7 @@ struct ParticipantView: View { } } +#if !os(tvOS) struct RemoteAudioVolumeControl: View { let track: RemoteAudioTrack let showsPercentage: Bool @@ -375,6 +378,7 @@ private extension View { #endif } } +#endif struct StatsView: View { private let track: Track diff --git a/Multiplatform/Views/PublishOptionsView.swift b/Multiplatform/Views/PublishOptionsView.swift index ca0eb8b..2718630 100644 --- a/Multiplatform/Views/PublishOptionsView.swift +++ b/Multiplatform/Views/PublishOptionsView.swift @@ -71,8 +71,8 @@ struct PublishOptionsView: View { ForEach(VideoCodec.all) { Text($0.id.uppercased()).tag($0 as VideoCodec?) } - }.onChange(of: preferredVideoCodec) { newValue in - if newValue?.isSVC ?? false { + }.onChange(of: preferredVideoCodec) { + if preferredVideoCodec?.isSVC ?? false { preferredBackupVideoCodec = .vp8 } else { preferredBackupVideoCodec = nil @@ -160,9 +160,11 @@ struct PublishOptionsView: View { } .onAppear(perform: { Task { - devices = try await CameraCapturer.captureDevices() - #if !os(macOS) - .singleDeviceforEachPosition() + let all = await (try? CameraCapturer.captureDevices()) ?? [] + #if os(macOS) + devices = all + #else + devices = all.singleDeviceforEachPosition() #endif } }) diff --git a/Multiplatform/Views/RoomContextView.swift b/Multiplatform/Views/RoomContextView.swift index e1275a1..906751f 100644 --- a/Multiplatform/Views/RoomContextView.swift +++ b/Multiplatform/Views/RoomContextView.swift @@ -59,8 +59,12 @@ struct RoomContextView: View { roomCtx.isE2eeEnabled = e2ee roomCtx.e2eeKey = e2eeKey if !roomCtx.token.isEmpty { - let room = try await roomCtx.connect() - appCtx.connectionHistory.update(room: room, e2ee: e2ee, e2eeKey: e2eeKey) + do { + let room = try await roomCtx.connect() + appCtx.connectionHistory.update(room: room, e2ee: e2ee, e2eeKey: e2eeKey) + } catch { + print("Failed to connect: \(error)") + } } } }) diff --git a/Multiplatform/Views/RoomSwitchView.swift b/Multiplatform/Views/RoomSwitchView.swift index 93dd85b..8a8049a 100644 --- a/Multiplatform/Views/RoomSwitchView.swift +++ b/Multiplatform/Views/RoomSwitchView.swift @@ -55,16 +55,14 @@ struct RoomSwitchView: View { } .preferredColorScheme(.dark) .navigationTitle(navigatonTitle) - .onChange(of: shouldShowRoomView) { newValue in - #if os(visionOS) - Task { - if newValue { + #if os(visionOS) + .task(id: shouldShowRoomView) { + if shouldShowRoomView { await openImmersiveSpace(id: "ImmersiveSpace") } else { await dismissImmersiveSpace() } } - #endif - } + #endif } } diff --git a/Multiplatform/Views/RoomView.swift b/Multiplatform/Views/RoomView.swift index 16a01fe..a87183b 100644 --- a/Multiplatform/Views/RoomView.swift +++ b/Multiplatform/Views/RoomView.swift @@ -207,7 +207,7 @@ struct RoomView: View { .padding(5) .onAppear { Task { @MainActor in - canSwitchCameraPosition = try await CameraCapturer.canSwitchPosition() + canSwitchCameraPosition = await (try? CameraCapturer.canSwitchPosition()) ?? false } Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { _ in Task { @MainActor in @@ -277,7 +277,7 @@ struct RoomView: View { Task { isARCameraPublishingBusy = true defer { Task { @MainActor in isARCameraPublishingBusy = false } } - try await roomCtx.setARCamera(isEnabled: true) + _ = try? await roomCtx.setARCamera(isEnabled: true) } }, @@ -298,7 +298,7 @@ struct RoomView: View { if let track = room.localParticipant.firstCameraVideoTrack as? LocalVideoTrack, let cameraCapturer = track.capturer as? CameraCapturer { - try await cameraCapturer.switchCameraPosition() + _ = try? await cameraCapturer.switchCameraPosition() } } } @@ -307,7 +307,7 @@ struct RoomView: View { Task { isCameraPublishingBusy = true defer { Task { @MainActor in isCameraPublishingBusy = false } } - try await room.localParticipant.setCamera(enabled: !isCameraEnabled) + _ = try? await room.localParticipant.setCamera(enabled: !isCameraEnabled) } } } label: { @@ -323,7 +323,7 @@ struct RoomView: View { Task { isCameraPublishingBusy = true defer { Task { @MainActor in isCameraPublishingBusy = false } } - try await room.localParticipant.setCamera(enabled: false) + _ = try? await room.localParticipant.setCamera(enabled: false) } } else { publishOptionsPickerPresented = true @@ -345,9 +345,9 @@ struct RoomView: View { cameraPublishOptions = publishOptions Task { defer { Task { @MainActor in isCameraPublishingBusy = false } } - try await room.localParticipant.setCamera(enabled: true, - captureOptions: captureOptions, - publishOptions: publishOptions) + _ = try? await room.localParticipant.setCamera(enabled: true, + captureOptions: captureOptions, + publishOptions: publishOptions) } } .padding() @@ -360,7 +360,7 @@ struct RoomView: View { isMicrophonePublishingBusy = true defer { Task { @MainActor in isMicrophonePublishingBusy = false } } let options = AudioCaptureOptions(noiseSuppression: false, highpassFilter: false) - try await room.localParticipant.setMicrophone(enabled: !isMicrophoneEnabled, captureOptions: options) + _ = try? await room.localParticipant.setMicrophone(enabled: !isMicrophoneEnabled, captureOptions: options) } }, label: { @@ -375,7 +375,7 @@ struct RoomView: View { Task { isScreenSharePublishingBusy = true defer { Task { @MainActor in isScreenSharePublishingBusy = false } } - try await room.localParticipant.setScreenShare(enabled: !isScreenShareEnabled) + _ = try? await room.localParticipant.setScreenShare(enabled: !isScreenShareEnabled) } }, label: { @@ -392,7 +392,7 @@ struct RoomView: View { Task { isScreenSharePublishingBusy = true defer { Task { @MainActor in isScreenSharePublishingBusy = false } } - try await roomCtx.setScreenShareMacOS(isEnabled: false) + try? await roomCtx.setScreenShareMacOS(isEnabled: false) } } else { screenPickerPresented = true @@ -409,7 +409,7 @@ struct RoomView: View { Task { isScreenSharePublishingBusy = true defer { Task { @MainActor in isScreenSharePublishingBusy = false } } - try await roomCtx.setScreenShareMacOS(isEnabled: true, screenShareSource: source) + try? await roomCtx.setScreenShareMacOS(isEnabled: true, screenShareSource: source) } screenPickerPresented = false }.padding() @@ -492,28 +492,28 @@ struct RoomView: View { Menu("Simulate scenario") { Button("Quick reconnect") { - Task { try await room.debug_simulate(scenario: .quickReconnect) } + Task { try? await room.debug_simulate(scenario: .quickReconnect) } } Button("Full reconnect") { - Task { try await room.debug_simulate(scenario: .fullReconnect) } + Task { try? await room.debug_simulate(scenario: .fullReconnect) } } Button("Node failure") { - Task { try await room.debug_simulate(scenario: .nodeFailure) } + Task { try? await room.debug_simulate(scenario: .nodeFailure) } } Button("Server leave") { - Task { try await room.debug_simulate(scenario: .serverLeave) } + Task { try? await room.debug_simulate(scenario: .serverLeave) } } Button("Migration") { - Task { try await room.debug_simulate(scenario: .migration) } + Task { try? await room.debug_simulate(scenario: .migration) } } Button("Speaker update") { - Task { try await room.debug_simulate(scenario: .speakerUpdate(seconds: 3)) } + Task { try? await room.debug_simulate(scenario: .speakerUpdate(seconds: 3)) } } Button("Force TCP") { - Task { try await room.debug_simulate(scenario: .forceTCP) } + Task { try? await room.debug_simulate(scenario: .forceTCP) } } Button("Force TLS") { - Task { try await room.debug_simulate(scenario: .forceTLS) } + Task { try? await room.debug_simulate(scenario: .forceTLS) } } } } @@ -522,13 +522,13 @@ struct RoomView: View { Menu("Track permissions") { Button("Allow all") { Task { - try await room.localParticipant + try? await room.localParticipant .setTrackSubscriptionPermissions(allParticipantsAllowed: true) } } Button("Disallow all") { Task { - try await room.localParticipant + try? await room.localParticipant .setTrackSubscriptionPermissions(allParticipantsAllowed: false) } } diff --git a/Multiplatform/Views/ScreenShareSourcePickerView.swift b/Multiplatform/Views/ScreenShareSourcePickerView.swift index 01e6da6..68e52c0 100644 --- a/Multiplatform/Views/ScreenShareSourcePickerView.swift +++ b/Multiplatform/Views/ScreenShareSourcePickerView.swift @@ -30,14 +30,14 @@ final class ScreenShareSourcePickerCtrl: ObservableObject { guard oldValue != mode else { return } Task { [weak self] in guard let self else { return } - try await restartTracks() + try? await restartTracks() } } } init() { Task { - try await restartTracks() + try? await restartTracks() } }