From 801c61b2dbf79de110bf7bf22ee251e938aac005 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Fri, 13 Oct 2023 12:37:59 +0800 Subject: [PATCH 01/32] disable audio modules for now --- .../xcshareddata/swiftpm/Package.resolved | 18 ++++----- LiveKitExample.xcodeproj/project.pbxproj | 2 + Shared/Controllers/AppContext.swift | 40 +++++++++---------- Shared/RoomView.swift | 12 +++--- 4 files changed, 37 insertions(+), 35 deletions(-) diff --git a/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved b/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved index 416a83e..c821d92 100644 --- a/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -28,15 +28,6 @@ "version": "4.1.1" } }, - { - "package": "WebRTC", - "repositoryURL": "https://github.com/webrtc-sdk/Specs.git", - "state": { - "branch": null, - "revision": "4fa8d6d647fc759cdd0265fd413d2f28ea2e0e08", - "version": "114.5735.8" - } - }, { "package": "swift-log", "repositoryURL": "https://github.com/apple/swift-log.git", @@ -54,6 +45,15 @@ "revision": "3c54ab05249f59f2c6641dd2920b8358ea9ed127", "version": "1.24.0" } + }, + { + "package": "WebRTC", + "repositoryURL": "https://github.com/livekit/webrtc-xcframework-static.git", + "state": { + "branch": null, + "revision": "05fb98380b8c2c43041050be6170ffa3dbc64174", + "version": "114.5735.9" + } } ] }, diff --git a/LiveKitExample.xcodeproj/project.pbxproj b/LiveKitExample.xcodeproj/project.pbxproj index 610e89b..341e507 100644 --- a/LiveKitExample.xcodeproj/project.pbxproj +++ b/LiveKitExample.xcodeproj/project.pbxproj @@ -548,6 +548,7 @@ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; + OTHER_LDFLAGS = "-ObjC"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -604,6 +605,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; + OTHER_LDFLAGS = "-ObjC"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; diff --git a/Shared/Controllers/AppContext.swift b/Shared/Controllers/AppContext.swift index 43076b2..3a49675 100644 --- a/Shared/Controllers/AppContext.swift +++ b/Shared/Controllers/AppContext.swift @@ -38,19 +38,19 @@ final class AppContext: ObservableObject { didSet { store.value.connectionHistory = connectionHistory } } - @Published var outputDevice: RTCIODevice = RTCIODevice.defaultDevice(with: .output) { - didSet { - print("didSet outputDevice: \(String(describing: outputDevice))") - Room.audioDeviceModule.outputDevice = outputDevice - } - } +// @Published var outputDevice: LKRTCIODevice = LKRTCIODevice.defaultDevice(with: .output) { +// didSet { +// print("didSet outputDevice: \(String(describing: outputDevice))") +// Room.audioDeviceModule.outputDevice = outputDevice +// } +// } - @Published var inputDevice: RTCIODevice = RTCIODevice.defaultDevice(with: .input) { - didSet { - print("didSet inputDevice: \(String(describing: inputDevice))") - Room.audioDeviceModule.inputDevice = inputDevice - } - } +// @Published var inputDevice: LKRTCIODevice = LKRTCIODevice.defaultDevice(with: .input) { +// didSet { +// print("didSet inputDevice: \(String(describing: inputDevice))") +// Room.audioDeviceModule.inputDevice = inputDevice +// } +// } @Published var preferSpeakerOutput: Bool = true { didSet { AudioManager.shared.preferSpeakerOutput = preferSpeakerOutput } @@ -66,13 +66,13 @@ final class AppContext: ObservableObject { self.videoViewMirrored = store.value.videoViewMirrored self.connectionHistory = store.value.connectionHistory - Room.audioDeviceModule.setDevicesUpdatedHandler { - print("devices did update") - // force UI update for outputDevice / inputDevice - DispatchQueue.main.async { - self.outputDevice = Room.audioDeviceModule.outputDevice - self.inputDevice = Room.audioDeviceModule.inputDevice - } - } +// Room.audioDeviceModule.setDevicesUpdatedHandler { +// print("devices did update") +// // force UI update for outputDevice / inputDevice +// DispatchQueue.main.async { +// self.outputDevice = Room.audioDeviceModule.outputDevice +// self.inputDevice = Room.audioDeviceModule.inputDevice +// } +// } } } diff --git a/Shared/RoomView.swift b/Shared/RoomView.swift index c702fd7..566ce46 100644 --- a/Shared/RoomView.swift +++ b/Shared/RoomView.swift @@ -22,12 +22,12 @@ extension CIImage { } } -extension RTCIODevice: Identifiable { - - public var id: String { - deviceId - } -} +//extension RTCIODevice: Identifiable { +// +// public var id: String { +// deviceId +// } +//} #if os(macOS) // keeps weak reference to NSWindow From 20157fc2dd7f1b602ce981646a7d1c4a8c1baab1 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Fri, 13 Oct 2023 12:38:59 +0800 Subject: [PATCH 02/32] Update RoomView.swift --- Shared/RoomView.swift | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Shared/RoomView.swift b/Shared/RoomView.swift index 566ce46..21bc830 100644 --- a/Shared/RoomView.swift +++ b/Shared/RoomView.swift @@ -443,17 +443,17 @@ struct RoomView: View { Group { // - Picker("Output device", selection: $appCtx.outputDevice) { - ForEach(Room.audioDeviceModule.outputDevices) { device in - Text(device.isDefault ? "Default" : "\(device.name)").tag(device) - } - } - - Picker("Input device", selection: $appCtx.inputDevice) { - ForEach(Room.audioDeviceModule.inputDevices) { device in - Text(device.isDefault ? "Default" : "\(device.name)").tag(device) - } - } +// Picker("Output device", selection: $appCtx.outputDevice) { +// ForEach(Room.audioDeviceModule.outputDevices) { device in +// Text(device.isDefault ? "Default" : "\(device.name)").tag(device) +// } +// } + +// Picker("Input device", selection: $appCtx.inputDevice) { +// ForEach(Room.audioDeviceModule.inputDevices) { device in +// Text(device.isDefault ? "Default" : "\(device.name)").tag(device) +// } +// } } #endif From 97a7209d2caa13101da748c0ed3b382c4e2f3e2d Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 26 Oct 2023 17:46:41 +0900 Subject: [PATCH 03/32] update --- .../xcshareddata/swiftpm/Package.resolved | 18 ++++----- Shared/Controllers/AppContext.swift | 40 +++++++++---------- Shared/RoomView.swift | 22 +++++----- 3 files changed, 39 insertions(+), 41 deletions(-) diff --git a/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved b/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved index c821d92..416a83e 100644 --- a/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -28,6 +28,15 @@ "version": "4.1.1" } }, + { + "package": "WebRTC", + "repositoryURL": "https://github.com/webrtc-sdk/Specs.git", + "state": { + "branch": null, + "revision": "4fa8d6d647fc759cdd0265fd413d2f28ea2e0e08", + "version": "114.5735.8" + } + }, { "package": "swift-log", "repositoryURL": "https://github.com/apple/swift-log.git", @@ -45,15 +54,6 @@ "revision": "3c54ab05249f59f2c6641dd2920b8358ea9ed127", "version": "1.24.0" } - }, - { - "package": "WebRTC", - "repositoryURL": "https://github.com/livekit/webrtc-xcframework-static.git", - "state": { - "branch": null, - "revision": "05fb98380b8c2c43041050be6170ffa3dbc64174", - "version": "114.5735.9" - } } ] }, diff --git a/Shared/Controllers/AppContext.swift b/Shared/Controllers/AppContext.swift index 3a49675..eb957e1 100644 --- a/Shared/Controllers/AppContext.swift +++ b/Shared/Controllers/AppContext.swift @@ -38,19 +38,19 @@ final class AppContext: ObservableObject { didSet { store.value.connectionHistory = connectionHistory } } -// @Published var outputDevice: LKRTCIODevice = LKRTCIODevice.defaultDevice(with: .output) { -// didSet { -// print("didSet outputDevice: \(String(describing: outputDevice))") -// Room.audioDeviceModule.outputDevice = outputDevice -// } -// } + @Published var outputDevice: AudioDevice = AudioManager.shared.defaultOutputDevice { + didSet { + print("didSet outputDevice: \(String(describing: outputDevice))") + AudioManager.shared.outputDevice = outputDevice + } + } -// @Published var inputDevice: LKRTCIODevice = LKRTCIODevice.defaultDevice(with: .input) { -// didSet { -// print("didSet inputDevice: \(String(describing: inputDevice))") -// Room.audioDeviceModule.inputDevice = inputDevice -// } -// } + @Published var inputDevice: AudioDevice = AudioManager.shared.defaultInputDevice { + didSet { + print("didSet inputDevice: \(String(describing: inputDevice))") + AudioManager.shared.inputDevice = inputDevice + } + } @Published var preferSpeakerOutput: Bool = true { didSet { AudioManager.shared.preferSpeakerOutput = preferSpeakerOutput } @@ -66,13 +66,13 @@ final class AppContext: ObservableObject { self.videoViewMirrored = store.value.videoViewMirrored self.connectionHistory = store.value.connectionHistory -// Room.audioDeviceModule.setDevicesUpdatedHandler { -// print("devices did update") -// // force UI update for outputDevice / inputDevice -// DispatchQueue.main.async { -// self.outputDevice = Room.audioDeviceModule.outputDevice -// self.inputDevice = Room.audioDeviceModule.inputDevice -// } -// } + AudioManager.shared.onDeviceUpdate = { audioManager in + print("devices did update") + // force UI update for outputDevice / inputDevice + DispatchQueue.main.async { + self.outputDevice = audioManager.outputDevice + self.inputDevice = audioManager.inputDevice + } + } } } diff --git a/Shared/RoomView.swift b/Shared/RoomView.swift index 21bc830..8f6f063 100644 --- a/Shared/RoomView.swift +++ b/Shared/RoomView.swift @@ -442,18 +442,16 @@ struct RoomView: View { #if os(macOS) Group { - // -// Picker("Output device", selection: $appCtx.outputDevice) { -// ForEach(Room.audioDeviceModule.outputDevices) { device in -// Text(device.isDefault ? "Default" : "\(device.name)").tag(device) -// } -// } - -// Picker("Input device", selection: $appCtx.inputDevice) { -// ForEach(Room.audioDeviceModule.inputDevices) { device in -// Text(device.isDefault ? "Default" : "\(device.name)").tag(device) -// } -// } + Picker("Output device", selection: $appCtx.outputDevice) { + ForEach(AudioManager.shared.outputDevices) { device in + Text(device.isDefault ? "Default" : "\(device.name)").tag(device) + } + } + Picker("Input device", selection: $appCtx.inputDevice) { + ForEach(AudioManager.shared.inputDevices) { device in + Text(device.isDefault ? "Default" : "\(device.name)").tag(device) + } + } } #endif From 198b4371657473d28b6c376fa195bfb517b6c2b4 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 2 Nov 2023 02:06:35 +0800 Subject: [PATCH 04/32] v2 --- .../xcshareddata/swiftpm/Package.resolved | 18 +++--- Shared/Controllers/RoomContext.swift | 4 +- Shared/ParticipantView.swift | 55 +++++++++---------- Shared/RoomView.swift | 24 ++++++-- 4 files changed, 55 insertions(+), 46 deletions(-) diff --git a/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved b/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved index 416a83e..c821d92 100644 --- a/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -28,15 +28,6 @@ "version": "4.1.1" } }, - { - "package": "WebRTC", - "repositoryURL": "https://github.com/webrtc-sdk/Specs.git", - "state": { - "branch": null, - "revision": "4fa8d6d647fc759cdd0265fd413d2f28ea2e0e08", - "version": "114.5735.8" - } - }, { "package": "swift-log", "repositoryURL": "https://github.com/apple/swift-log.git", @@ -54,6 +45,15 @@ "revision": "3c54ab05249f59f2c6641dd2920b8358ea9ed127", "version": "1.24.0" } + }, + { + "package": "WebRTC", + "repositoryURL": "https://github.com/livekit/webrtc-xcframework-static.git", + "state": { + "branch": null, + "revision": "05fb98380b8c2c43041050be6170ffa3dbc64174", + "version": "114.5735.9" + } } ] }, diff --git a/Shared/Controllers/RoomContext.swift b/Shared/Controllers/RoomContext.swift index 4c367bc..593c756 100644 --- a/Shared/Controllers/RoomContext.swift +++ b/Shared/Controllers/RoomContext.swift @@ -128,7 +128,6 @@ final class RoomContext: ObservableObject { ), adaptiveStream: adaptiveStream, dynacast: dynacast, - reportStats: reportStats, e2eeOptions: e2eeOptions ) @@ -226,8 +225,7 @@ extension RoomContext: RoomDelegate { } } - func room(_ room: Room, - participant: RemoteParticipant?, didReceive data: Data) { + func room(_ room: Room, particiepant: RemoteParticipant?, didReceiveData data: Data, topic: String) { do { let roomMessage = try jsonDecoder.decode(ExampleRoomMessage.self, from: data) diff --git a/Shared/ParticipantView.swift b/Shared/ParticipantView.swift index 165db94..42302a4 100644 --- a/Shared/ParticipantView.swift +++ b/Shared/ParticipantView.swift @@ -12,7 +12,7 @@ struct ParticipantView: View { @State private var isRendering: Bool = false @State private var dimensions: Dimensions? - @State private var videoTrackStats: TrackStats? + @State private var videoTrackStats: TrackStatistics? func bgView(systemSymbol: SFSymbol, geometry: GeometryProxy) -> some View { Image(systemSymbol: systemSymbol) @@ -47,7 +47,7 @@ struct ParticipantView: View { debugMode: appCtx.showInformationOverlay, isRendering: $isRendering, dimensions: $dimensions, - trackStats: $videoTrackStats) + trackStatistics: $videoTrackStats) if !isRendering { ProgressView().progressViewStyle(CircularProgressViewStyle()) @@ -276,28 +276,27 @@ struct StatsView: View { Text("Unknown").fontWeight(.bold) } - if let trackStats = viewModel.stats { - - if trackStats.bpsSent != 0 { - - HStack(spacing: 3) { - if let codecName = trackStats.codecName { - Text(codecName.uppercased()).fontWeight(.bold) - } - Image(systemSymbol: .arrowUpCircle) - Text(trackStats.formattedBpsSent()) - } - } - - if trackStats.bpsReceived != 0 { - HStack(spacing: 3) { - if let codecName = trackStats.codecName { - Text(codecName.uppercased()).fontWeight(.bold) - } - Image(systemSymbol: .arrowDownCircle) - Text(trackStats.formattedBpsReceived()) - } - } + if let trackStats = viewModel.statistics { + +// if trackStats.bpsSent != 0 { +// HStack(spacing: 3) { +// if let codecName = trackStats.codecName { +// Text(codecName.uppercased()).fontWeight(.bold) +// } +// Image(systemSymbol: .arrowUpCircle) +// Text(trackStats.formattedBpsSent()) +// } +// } +// +// if trackStats.bpsReceived != 0 { +// HStack(spacing: 3) { +// if let codecName = trackStats.codecName { +// Text(codecName.uppercased()).fontWeight(.bold) +// } +// Image(systemSymbol: .arrowDownCircle) +// Text(trackStats.formattedBpsReceived()) +// } +// } } } .font(.system(size: 10)) @@ -314,13 +313,13 @@ extension StatsView { class DelegateObserver: ObservableObject, TrackDelegate { private let track: Track @Published var dimensions: Dimensions? - @Published var stats: TrackStats? + @Published var statistics: TrackStatistics? init(track: Track) { self.track = track dimensions = track.dimensions - stats = track.stats + statistics = track.statistics track.add(delegate: self) } @@ -331,9 +330,9 @@ extension StatsView { } } - func track(_ track: Track, didUpdate stats: TrackStats) { + func track(_ track: Track, didUpdateStatistics statistics: TrackStatistics) { Task.detached { @MainActor in - self.stats = stats + self.statistics = statistics } } } diff --git a/Shared/RoomView.swift b/Shared/RoomView.swift index 8f6f063..7827742 100644 --- a/Shared/RoomView.swift +++ b/Shared/RoomView.swift @@ -469,35 +469,47 @@ struct RoomView: View { Menu { Button { - room.sendSimulate(scenario: .nodeFailure) + Task { + try await room.sendSimulate(scenario: .nodeFailure) + } } label: { Text("Node failure") } Button { - room.sendSimulate(scenario: .serverLeave) + Task { + try await room.sendSimulate(scenario: .serverLeave) + } } label: { Text("Server leave") } Button { - room.sendSimulate(scenario: .migration) + Task { + try await room.sendSimulate(scenario: .migration) + } } label: { Text("Migration") } Button { - room.sendSimulate(scenario: .speakerUpdate(seconds: 3)) + Task { + try await room.sendSimulate(scenario: .speakerUpdate(seconds: 3)) + } } label: { Text("Speaker update") } Button { - room.sendSimulate(scenario: .forceTCP) + Task { + try await room.sendSimulate(scenario: .forceTCP) + } } label: { Text("Force TCP") } Button { - room.sendSimulate(scenario: .forceTLS) + Task { + try await room.sendSimulate(scenario: .forceTLS) + } } label: { Text("Force TLS") } From 125e35e5522e099b9f3304d6d7d883598de15b27 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 2 Nov 2023 22:52:18 +0800 Subject: [PATCH 05/32] fix screen share --- .../xcshareddata/swiftpm/Package.resolved | 13 +---- Shared/Controllers/RoomContext.swift | 19 ++++--- Shared/ParticipantView.swift | 54 ++++++++++-------- Shared/RoomView.swift | 55 ++++++++++--------- .../Views/ScreenShareSourcePickerView.swift | 45 +++++++-------- 5 files changed, 95 insertions(+), 91 deletions(-) diff --git a/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved b/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved index c821d92..a039db4 100644 --- a/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -10,15 +10,6 @@ "version": "4.2.2" } }, - { - "package": "Promises", - "repositoryURL": "https://github.com/google/promises.git", - "state": { - "branch": null, - "revision": "e70e889c0196c76d22759eb50d6a0270ca9f1d9e", - "version": "2.3.1" - } - }, { "package": "SFSafeSymbols", "repositoryURL": "https://github.com/SFSafeSymbols/SFSafeSymbols", @@ -42,8 +33,8 @@ "repositoryURL": "https://github.com/apple/swift-protobuf.git", "state": { "branch": null, - "revision": "3c54ab05249f59f2c6641dd2920b8358ea9ed127", - "version": "1.24.0" + "revision": "07f7f26ded8df9645c072f220378879c4642e063", + "version": "1.25.1" } }, { diff --git a/Shared/Controllers/RoomContext.swift b/Shared/Controllers/RoomContext.swift index 593c756..2da21fa 100644 --- a/Shared/Controllers/RoomContext.swift +++ b/Shared/Controllers/RoomContext.swift @@ -131,14 +131,15 @@ final class RoomContext: ObservableObject { e2eeOptions: e2eeOptions ) - return try await room.connect(url, - token, - connectOptions: connectOptions, - roomOptions: roomOptions) + try await room.connect(url, + token, + connectOptions: connectOptions, + roomOptions: roomOptions) + return room } - func disconnect() async throws { - try await room.disconnect() + func disconnect() async { + await room.disconnect() } func sendMessage() { @@ -171,6 +172,8 @@ final class RoomContext: ObservableObject { #if os(macOS) weak var screenShareTrack: LocalTrackPublication? + + @available(macOS 12.3, *) func setScreenShareMacOS(enabled: Bool, screenShareSource: MacOSScreenCaptureSource? = nil) async throws { guard let localParticipant = room.localParticipant else { @@ -180,7 +183,7 @@ final class RoomContext: ObservableObject { if enabled, let screenShareSource = screenShareSource { let track = LocalVideoTrack.createMacOSScreenShareTrack(source: screenShareSource) - screenShareTrack = try await localParticipant.publishVideo(track) + screenShareTrack = try await localParticipant.publish(videoTrack: track) } if !enabled, let screenShareTrack = screenShareTrack { @@ -225,7 +228,7 @@ extension RoomContext: RoomDelegate { } } - func room(_ room: Room, particiepant: RemoteParticipant?, didReceiveData data: Data, topic: String) { + func room(_ room: Room, participant: RemoteParticipant?, didReceiveData data: Data, topic: String) { do { let roomMessage = try jsonDecoder.decode(ExampleRoomMessage.self, from: data) diff --git a/Shared/ParticipantView.swift b/Shared/ParticipantView.swift index 42302a4..288617c 100644 --- a/Shared/ParticipantView.swift +++ b/Shared/ParticipantView.swift @@ -118,13 +118,17 @@ struct ParticipantView: View { Menu { if case .subscribed = remotePub.subscriptionState { Button { - remotePub.set(subscribed: false) + Task { + try await remotePub.set(subscribed: false) + } } label: { Text("Unsubscribe") } } else if case .unsubscribed = remotePub.subscriptionState { Button { - remotePub.set(subscribed: true) + Task { + try await remotePub.set(subscribed: true) + } } label: { Text("Subscribe") } @@ -166,13 +170,17 @@ struct ParticipantView: View { Menu { if case .subscribed = remotePub.subscriptionState { Button { - remotePub.set(subscribed: false) + Task { + try await remotePub.set(subscribed: false) + } } label: { Text("Unsubscribe") } } else if case .unsubscribed = remotePub.subscriptionState { Button { - remotePub.set(subscribed: true) + Task { + try await remotePub.set(subscribed: true) + } } label: { Text("Subscribe") } @@ -278,25 +286,25 @@ struct StatsView: View { if let trackStats = viewModel.statistics { -// if trackStats.bpsSent != 0 { -// HStack(spacing: 3) { -// if let codecName = trackStats.codecName { -// Text(codecName.uppercased()).fontWeight(.bold) -// } -// Image(systemSymbol: .arrowUpCircle) -// Text(trackStats.formattedBpsSent()) -// } -// } -// -// if trackStats.bpsReceived != 0 { -// HStack(spacing: 3) { -// if let codecName = trackStats.codecName { -// Text(codecName.uppercased()).fontWeight(.bold) -// } -// Image(systemSymbol: .arrowDownCircle) -// Text(trackStats.formattedBpsReceived()) -// } -// } + // if trackStats.bpsSent != 0 { + // HStack(spacing: 3) { + // if let codecName = trackStats.codecName { + // Text(codecName.uppercased()).fontWeight(.bold) + // } + // Image(systemSymbol: .arrowUpCircle) + // Text(trackStats.formattedBpsSent()) + // } + // } + // + // if trackStats.bpsReceived != 0 { + // HStack(spacing: 3) { + // if let codecName = trackStats.codecName { + // Text(codecName.uppercased()).fontWeight(.bold) + // } + // Image(systemSymbol: .arrowDownCircle) + // Text(trackStats.formattedBpsReceived()) + // } + // } } } .font(.system(size: 10)) diff --git a/Shared/RoomView.swift b/Shared/RoomView.swift index 7827742..750d8f8 100644 --- a/Shared/RoomView.swift +++ b/Shared/RoomView.swift @@ -22,13 +22,6 @@ extension CIImage { } } -//extension RTCIODevice: Identifiable { -// -// public var id: String { -// deviceId -// } -//} - #if os(macOS) // keeps weak reference to NSWindow class WindowAccess: ObservableObject { @@ -369,15 +362,17 @@ struct RoomView: View { .disabled(isScreenSharePublishingBusy) #elseif os(macOS) Button(action: { - if isScreenShareEnabled { - // turn off screen share - Task { - isScreenSharePublishingBusy = true - defer { Task { @MainActor in isScreenSharePublishingBusy = false } } - try await roomCtx.setScreenShareMacOS(enabled: false) + if #available(macOS 12.3, *) { + if isScreenShareEnabled { + // Turn off screen share + Task { + isScreenSharePublishingBusy = true + defer { Task { @MainActor in isScreenSharePublishingBusy = false } } + try await roomCtx.setScreenShareMacOS(enabled: false) + } + } else { + screenPickerPresented = true } - } else { - screenPickerPresented = true } }, label: { @@ -385,14 +380,16 @@ struct RoomView: View { .renderingMode(isScreenShareEnabled ? .original : .template) .foregroundColor(isScreenShareEnabled ? Color.green : Color.white) }).popover(isPresented: $screenPickerPresented) { - ScreenShareSourcePickerView { source in - Task { - isScreenSharePublishingBusy = true - defer { Task { @MainActor in isScreenSharePublishingBusy = false } } - try await roomCtx.setScreenShareMacOS(enabled: true, screenShareSource: source) - } - screenPickerPresented = false - }.padding() + if #available(macOS 12.3, *) { + ScreenShareSourcePickerView { source in + Task { + isScreenSharePublishingBusy = true + defer { Task { @MainActor in isScreenSharePublishingBusy = false } } + try await roomCtx.setScreenShareMacOS(enabled: true, screenShareSource: source) + } + screenPickerPresented = false + }.padding() + } } .disabled(isScreenSharePublishingBusy) #endif @@ -459,7 +456,7 @@ struct RoomView: View { Button { Task { - try await room.localParticipant?.unpublishAll() + await room.localParticipant?.unpublishAll() } } label: { Text("Unpublish all") @@ -520,13 +517,17 @@ struct RoomView: View { Group { Menu { Button { - room.localParticipant?.setTrackSubscriptionPermissions(allParticipantsAllowed: true) + Task { + try await room.localParticipant?.setTrackSubscriptionPermissions(allParticipantsAllowed: true) + } } label: { Text("Allow all") } Button { - room.localParticipant?.setTrackSubscriptionPermissions(allParticipantsAllowed: false) + Task { + try await room.localParticipant?.setTrackSubscriptionPermissions(allParticipantsAllowed: false) + } } label: { Text("Disallow all") } @@ -545,7 +546,7 @@ struct RoomView: View { // Disconnect Button(action: { Task { - try await roomCtx.disconnect() + await roomCtx.disconnect() } }, label: { diff --git a/Shared/Views/ScreenShareSourcePickerView.swift b/Shared/Views/ScreenShareSourcePickerView.swift index b72018d..3f77929 100644 --- a/Shared/Views/ScreenShareSourcePickerView.swift +++ b/Shared/Views/ScreenShareSourcePickerView.swift @@ -5,6 +5,7 @@ import LiveKit #if os(macOS) +@available(macOS 12.3, *) class ScreenShareSourcePickerCtrl: ObservableObject { @Published var tracks = [LocalVideoTrack]() @@ -12,45 +13,44 @@ class ScreenShareSourcePickerCtrl: ObservableObject { didSet { guard oldValue != mode else { return } Task { - await restartTracks() + try await restartTracks() } } } - private func restartTracks() async { + private func restartTracks() async throws { - Task { - // stop in parallel - await withThrowingTaskGroup(of: Void.self) { group in - for track in tracks { - group.addTask { - try await track.stop() - } + // stop in parallel + await withThrowingTaskGroup(of: Void.self) { group in + for track in tracks { + group.addTask { + try await track.stop() } } + } - let sources = try await MacOSScreenCapturer.sources(for: (mode == .display ? .display : .window)) - let options = ScreenShareCaptureOptions(dimensions: .h360_43, fps: 5) - let _newTracks = sources.map { LocalVideoTrack.createMacOSScreenShareTrack(source: $0, options: options) } + let sources = try await MacOSScreenCapturer.sources(for: (self.mode == .display ? .display : .window)) + let options = ScreenShareCaptureOptions(dimensions: .h360_43, fps: 5) + let _newTracks = sources.map { LocalVideoTrack.createMacOSScreenShareTrack(source: $0, options: options) } - Task { @MainActor in - self.tracks = _newTracks - } + Task { @MainActor in + self.tracks = _newTracks + } - // start in parallel - await withThrowingTaskGroup(of: Void.self) { group in - for track in _newTracks { - group.addTask { - try await track.start() - } + // start in parallel + await withThrowingTaskGroup(of: Void.self) { group in + for track in _newTracks { + group.addTask { + try await track.start() } } } + } init() { Task { - await restartTracks() + try await restartTracks() } } @@ -76,6 +76,7 @@ class ScreenShareSourcePickerCtrl: ObservableObject { typealias OnPickScreenShareSource = (MacOSScreenCaptureSource) -> Void +@available(macOS 12.3, *) struct ScreenShareSourcePickerView: View { public enum Mode { From 5185d848f1a2234b9a710567bcf0f01360938fa1 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Sat, 4 Nov 2023 00:07:43 +0800 Subject: [PATCH 06/32] remove try --- Shared/Controllers/RoomContext.swift | 4 ++-- Shared/LiveKitExample.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Shared/Controllers/RoomContext.swift b/Shared/Controllers/RoomContext.swift index 2da21fa..d0481dd 100644 --- a/Shared/Controllers/RoomContext.swift +++ b/Shared/Controllers/RoomContext.swift @@ -217,8 +217,8 @@ extension RoomContext: RoomDelegate { } } - func room(_ room: Room, - participantDidLeave participant: RemoteParticipant) { + func room(_ room: Room, participantDidLeave participant: RemoteParticipant) { + DispatchQueue.main.async { // self.participants.removeValue(forKey: participant.sid) if let focusParticipant = self.focusParticipant, diff --git a/Shared/LiveKitExample.swift b/Shared/LiveKitExample.swift index 0f44f58..258b3a3 100644 --- a/Shared/LiveKitExample.swift +++ b/Shared/LiveKitExample.swift @@ -58,7 +58,7 @@ struct RoomContextView: View { .onDisappear { print("\(String(describing: type(of: self))) onDisappear") Task { - try await roomCtx.disconnect() + await roomCtx.disconnect() } } .onOpenURL(perform: { url in From c2711552ff4ae5da020b42e59a1b3cdb3a71d232 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Sat, 4 Nov 2023 00:12:42 +0800 Subject: [PATCH 07/32] upgrade xcode settings --- LiveKitExample.xcodeproj/project.pbxproj | 4 +++- .../xcshareddata/xcschemes/BroadcastExt.xcscheme | 2 +- .../xcshareddata/xcschemes/Example (iOS Debug).xcscheme | 2 +- .../xcshareddata/xcschemes/Example (iOS Release).xcscheme | 2 +- .../xcshareddata/xcschemes/Example (macOS Debug).xcscheme | 2 +- .../xcshareddata/xcschemes/Example (macOS Release).xcscheme | 2 +- 6 files changed, 8 insertions(+), 6 deletions(-) diff --git a/LiveKitExample.xcodeproj/project.pbxproj b/LiveKitExample.xcodeproj/project.pbxproj index 341e507..f5f42b9 100644 --- a/LiveKitExample.xcodeproj/project.pbxproj +++ b/LiveKitExample.xcodeproj/project.pbxproj @@ -317,7 +317,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1330; - LastUpgradeCheck = 1420; + LastUpgradeCheck = 1500; TargetAttributes = { 683F05F2273F96B20080C7AC = { CreatedOnToolsVersion = 13.1; @@ -531,6 +531,7 @@ ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -594,6 +595,7 @@ ENABLE_BITCODE = NO; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_PREPROCESSOR_DEFINITIONS = "GL_SILENCE_DEPRECATION=1"; diff --git a/LiveKitExample.xcodeproj/xcshareddata/xcschemes/BroadcastExt.xcscheme b/LiveKitExample.xcodeproj/xcshareddata/xcschemes/BroadcastExt.xcscheme index aa392ca..50099a4 100644 --- a/LiveKitExample.xcodeproj/xcshareddata/xcschemes/BroadcastExt.xcscheme +++ b/LiveKitExample.xcodeproj/xcshareddata/xcschemes/BroadcastExt.xcscheme @@ -1,6 +1,6 @@ Date: Tue, 7 Nov 2023 02:03:15 +0800 Subject: [PATCH 08/32] update --- .swift-version | 1 + .swiftformat | 2 + Shared/ConnectView.swift | 28 +- Shared/Controllers/AppContext.swift | 33 +- Shared/Controllers/RoomContext.swift | 112 +++-- Shared/Custom.swift | 65 +-- Shared/ExampleObservableRoom.swift | 33 +- Shared/LiveKitExample.swift | 68 +-- Shared/ParticipantView.swift | 78 ++-- Shared/RoomView.swift | 417 +++++++++--------- Shared/Support/Bundle.swift | 34 +- Shared/Support/ConnectionHistory.swift | 45 +- Shared/Support/SecureStore.swift | 34 +- .../Views/ScreenShareSourcePickerView.swift | 203 +++++---- iOS/BroadcastExt/LoggingOSLog.swift | 39 +- iOS/BroadcastExt/SampleHandler.swift | 28 +- 16 files changed, 687 insertions(+), 533 deletions(-) create mode 100644 .swift-version create mode 100644 .swiftformat diff --git a/.swift-version b/.swift-version new file mode 100644 index 0000000..15df682 --- /dev/null +++ b/.swift-version @@ -0,0 +1 @@ +5.7 # Xcode 14 diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 0000000..54ffea4 --- /dev/null +++ b/.swiftformat @@ -0,0 +1,2 @@ +--exclude Sources/LiveKit/Protos +--header "/*\n * Copyright {year} LiveKit\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */" diff --git a/Shared/ConnectView.swift b/Shared/ConnectView.swift index 5364297..6bdd66d 100644 --- a/Shared/ConnectView.swift +++ b/Shared/ConnectView.swift @@ -1,10 +1,25 @@ +/* + * Copyright 2023 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 -import SwiftUI import LiveKit import SFSafeSymbols +import SwiftUI struct ConnectView: View { - @EnvironmentObject var appCtx: AppContext @EnvironmentObject var roomCtx: RoomContext @EnvironmentObject var room: Room @@ -13,7 +28,6 @@ struct ConnectView: View { GeometryReader { geometry in ScrollView { VStack(alignment: .center, spacing: 40.0) { - VStack(spacing: 10) { Image("logo") .resizable() @@ -135,13 +149,11 @@ struct ConnectView: View { } Spacer() - } - } } .padding() - .frame(width: geometry.size.width) // Make the scroll view full-width + .frame(width: geometry.size.width) // Make the scroll view full-width .frame(minHeight: geometry.size.height) // Set the content’s min height to the parent } } @@ -151,8 +163,8 @@ struct ConnectView: View { .alert(isPresented: $roomCtx.shouldShowDisconnectReason) { Alert(title: Text("Disconnected"), message: Text("Reason: " + (roomCtx.latestError != nil - ? String(describing: roomCtx.latestError!) - : "Unknown"))) + ? String(describing: roomCtx.latestError!) + : "Unknown"))) } } } diff --git a/Shared/Controllers/AppContext.swift b/Shared/Controllers/AppContext.swift index eb957e1..66dbb40 100644 --- a/Shared/Controllers/AppContext.swift +++ b/Shared/Controllers/AppContext.swift @@ -1,7 +1,23 @@ -import SwiftUI +/* + * Copyright 2023 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 Combine import LiveKit +import SwiftUI import WebRTC -import Combine extension ObservableObject where Self.ObjectWillChangePublisher == ObservableObjectPublisher { func notify() { @@ -11,7 +27,6 @@ extension ObservableObject where Self.ObjectWillChangePublisher == ObservableObj // This class contains the logic to control behavior of the whole app. final class AppContext: ObservableObject { - private let store: ValueStore @Published var videoViewVisible: Bool = true { @@ -59,12 +74,12 @@ final class AppContext: ObservableObject { public init(store: ValueStore) { self.store = store - self.videoViewVisible = store.value.videoViewVisible - self.showInformationOverlay = store.value.showInformationOverlay - self.preferSampleBufferRendering = store.value.preferSampleBufferRendering - self.videoViewMode = store.value.videoViewMode - self.videoViewMirrored = store.value.videoViewMirrored - self.connectionHistory = store.value.connectionHistory + videoViewVisible = store.value.videoViewVisible + showInformationOverlay = store.value.showInformationOverlay + preferSampleBufferRendering = store.value.preferSampleBufferRendering + videoViewMode = store.value.videoViewMode + videoViewMirrored = store.value.videoViewMirrored + connectionHistory = store.value.connectionHistory AudioManager.shared.onDeviceUpdate = { audioManager in print("devices did update") diff --git a/Shared/Controllers/RoomContext.swift b/Shared/Controllers/RoomContext.swift index d0481dd..bfabbb9 100644 --- a/Shared/Controllers/RoomContext.swift +++ b/Shared/Controllers/RoomContext.swift @@ -1,10 +1,25 @@ -import SwiftUI +/* + * Copyright 2023 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 LiveKit +import SwiftUI import WebRTC // This class contains the logic to control behavior of the whole app. final class RoomContext: ObservableObject { - let jsonEncoder = JSONEncoder() let jsonDecoder = JSONDecoder() @@ -52,7 +67,7 @@ final class RoomContext: ObservableObject { // ConnectOptions @Published var autoSubscribe: Bool = true { - didSet { store.value.autoSubscribe = autoSubscribe} + didSet { store.value.autoSubscribe = autoSubscribe } } @Published var publish: Bool = false { @@ -70,33 +85,32 @@ final class RoomContext: ObservableObject { self.store = store room.add(delegate: self) - self.url = store.value.url - self.token = store.value.token - self.e2ee = store.value.e2ee - self.e2eeKey = store.value.e2eeKey - self.simulcast = store.value.simulcast - self.adaptiveStream = store.value.adaptiveStream - self.dynacast = store.value.dynacast - self.reportStats = store.value.reportStats - self.autoSubscribe = store.value.autoSubscribe - self.publish = store.value.publishMode + url = store.value.url + token = store.value.token + e2ee = store.value.e2ee + e2eeKey = store.value.e2eeKey + simulcast = store.value.simulcast + adaptiveStream = store.value.adaptiveStream + dynacast = store.value.dynacast + reportStats = store.value.reportStats + autoSubscribe = store.value.autoSubscribe + publish = store.value.publishMode #if os(iOS) - UIApplication.shared.isIdleTimerDisabled = true + UIApplication.shared.isIdleTimerDisabled = true #endif } deinit { #if os(iOS) - UIApplication.shared.isIdleTimerDisabled = false + UIApplication.shared.isIdleTimerDisabled = false #endif print("RoomContext.deinit") } @MainActor func connect(entry: ConnectionHistory? = nil) async throws -> Room { - - if let entry = entry { + if let entry { url = entry.url token = entry.token e2ee = entry.e2ee @@ -131,8 +145,8 @@ final class RoomContext: ObservableObject { e2eeOptions: e2eeOptions ) - try await room.connect(url, - token, + try await room.connect(url: url, + token: token, connectOptions: connectOptions, roomOptions: roomOptions) return room @@ -143,18 +157,12 @@ final class RoomContext: ObservableObject { } func sendMessage() { - - guard let localParticipant = room.localParticipant else { - print("LocalParticipant doesn't exist") - return - } - // Make sure the message is not empty guard !textFieldString.isEmpty else { return } let roomMessage = ExampleRoomMessage(messageId: UUID().uuidString, - senderSid: localParticipant.sid, - senderIdentity: localParticipant.identity, + senderSid: room.localParticipant.sid, + senderIdentity: room.localParticipant.identity, text: textFieldString) textFieldString = "" messages.append(roomMessage) @@ -162,48 +170,39 @@ final class RoomContext: ObservableObject { Task { do { let json = try jsonEncoder.encode(roomMessage) - try await localParticipant.publish(data: json) - } catch let error { + try await room.localParticipant.publish(data: json) + } catch { print("Failed to encode data \(error)") } - } } #if os(macOS) - weak var screenShareTrack: LocalTrackPublication? - - @available(macOS 12.3, *) - func setScreenShareMacOS(enabled: Bool, screenShareSource: MacOSScreenCaptureSource? = nil) async throws { + weak var screenShareTrack: LocalTrackPublication? - guard let localParticipant = room.localParticipant else { - print("LocalParticipant doesn't exist") - return - } - - if enabled, let screenShareSource = screenShareSource { - let track = LocalVideoTrack.createMacOSScreenShareTrack(source: screenShareSource) - screenShareTrack = try await localParticipant.publish(videoTrack: track) - } + @available(macOS 12.3, *) + func setScreenShareMacOS(enabled: Bool, screenShareSource: MacOSScreenCaptureSource? = nil) async throws { + if enabled, let screenShareSource { + let track = LocalVideoTrack.createMacOSScreenShareTrack(source: screenShareSource) + screenShareTrack = try await room.localParticipant.publish(videoTrack: track) + } - if !enabled, let screenShareTrack = screenShareTrack { - try await localParticipant.unpublish(publication: screenShareTrack) + if !enabled, let screenShareTrack { + try await room.localParticipant.unpublish(publication: screenShareTrack) + } } - } #endif } extension RoomContext: RoomDelegate { - - func room(_ room: Room, publication: TrackPublication, didUpdateE2EEState e2eeState: E2EEState) { + func room(_: Room, publication: TrackPublication, didUpdateE2EEState e2eeState: E2EEState) { print("Did update e2eeState = [\(e2eeState.toString())] for publication \(publication.sid)") } - func room(_ room: Room, didUpdate connectionState: ConnectionState, oldValue: ConnectionState) { - + func room(_: Room, didUpdate connectionState: ConnectionState, oldValue: ConnectionState) { print("Did update connectionState \(oldValue) -> \(connectionState)") - if case .disconnected(let reason) = connectionState, reason != .user { + if case let .disconnected(reason) = connectionState, reason != .user { latestError = reason DispatchQueue.main.async { self.shouldShowDisconnectReason = true @@ -217,19 +216,18 @@ extension RoomContext: RoomDelegate { } } - func room(_ room: Room, participantDidLeave participant: RemoteParticipant) { - + func room(_: Room, participantDidLeave participant: RemoteParticipant) { DispatchQueue.main.async { // self.participants.removeValue(forKey: participant.sid) if let focusParticipant = self.focusParticipant, - focusParticipant.sid == participant.sid { + focusParticipant.sid == participant.sid + { self.focusParticipant = nil } } } - func room(_ room: Room, participant: RemoteParticipant?, didReceiveData data: Data, topic: String) { - + func room(_: Room, participant _: RemoteParticipant?, didReceiveData data: Data, topic _: String) { do { let roomMessage = try jsonDecoder.decode(ExampleRoomMessage.self, from: data) // Update UI from main queue @@ -243,7 +241,7 @@ extension RoomContext: RoomDelegate { } } - } catch let error { + } catch { print("Failed to decode data \(error)") } } diff --git a/Shared/Custom.swift b/Shared/Custom.swift index cb6716e..cead250 100644 --- a/Shared/Custom.swift +++ b/Shared/Custom.swift @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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 extension Color { @@ -13,6 +29,7 @@ struct LazyView: View { init(_ build: @autoclosure @escaping () -> Content) { self.build = build } + var body: Content { build() } @@ -20,49 +37,45 @@ struct LazyView: View { // Default button style for this example struct LKButton: View { - let title: String let action: () -> Void var body: some View { - Button(action: action, label: { - Text(title.uppercased()) - .fontWeight(.bold) - .padding(.horizontal, 12) - .padding(.vertical, 10) - } - ) - .background(Color.lkRed) - .cornerRadius(8) + Text(title.uppercased()) + .fontWeight(.bold) + .padding(.horizontal, 12) + .padding(.vertical, 10) + }) + .background(Color.lkRed) + .cornerRadius(8) } } #if os(iOS) -extension LKTextField.`Type` { - func toiOSType() -> UIKeyboardType { - switch self { - case .default: return .default - case .URL: return .URL - case .ascii: return .asciiCapable + extension LKTextField.`Type` { + func toiOSType() -> UIKeyboardType { + switch self { + case .default: return .default + case .URL: return .URL + case .ascii: return .asciiCapable + } } } -} #endif #if os(macOS) -// Avoid showing focus border around textfield for macOS -extension NSTextField { - open override var focusRingType: NSFocusRingType { - get { .none } - set { } + // Avoid showing focus border around textfield for macOS + extension NSTextField { + override open var focusRingType: NSFocusRingType { + get { .none } + set {} + } } -} #endif struct LKTextField: View { - enum `Type` { case `default` case URL @@ -88,8 +101,8 @@ struct LKTextField: View { // #endif .padding() .overlay(RoundedRectangle(cornerRadius: 10.0) - .strokeBorder(Color.white.opacity(0.3), - style: StrokeStyle(lineWidth: 1.0))) + .strokeBorder(Color.white.opacity(0.3), + style: StrokeStyle(lineWidth: 1.0))) }.frame(maxWidth: .infinity) } diff --git a/Shared/ExampleObservableRoom.swift b/Shared/ExampleObservableRoom.swift index 1aae0d7..eaedf01 100644 --- a/Shared/ExampleObservableRoom.swift +++ b/Shared/ExampleObservableRoom.swift @@ -1,22 +1,37 @@ -import SwiftUI -import LiveKit +/* + * Copyright 2023 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 AVFoundation +import LiveKit +import SwiftUI -import WebRTC import CoreImage.CIFilterBuiltins import ReplayKit +import WebRTC -extension Participant { - - public var mainVideoPublication: TrackPublication? { +public extension Participant { + var mainVideoPublication: TrackPublication? { firstScreenSharePublication ?? firstCameraPublication } - public var mainVideoTrack: VideoTrack? { + var mainVideoTrack: VideoTrack? { firstScreenShareVideoTrack ?? firstCameraVideoTrack } - public var subVideoTrack: VideoTrack? { + var subVideoTrack: VideoTrack? { firstScreenShareVideoTrack != nil ? firstCameraVideoTrack : nil } } @@ -31,7 +46,7 @@ struct ExampleRoomMessage: Identifiable, Equatable, Hashable, Codable { let messageId: String let senderSid: String - let senderIdentity: String + let senderIdentity: String? let text: String static func == (lhs: Self, rhs: Self) -> Bool { diff --git a/Shared/LiveKitExample.swift b/Shared/LiveKitExample.swift index 258b3a3..6d1732a 100644 --- a/Shared/LiveKitExample.swift +++ b/Shared/LiveKitExample.swift @@ -1,14 +1,29 @@ -import SwiftUI -import Logging -import LiveKit +/* + * Copyright 2023 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 KeychainAccess +import LiveKit +import Logging +import SwiftUI let sync = ValueStore(store: Keychain(service: "io.livekit.example.SwiftSDK.1"), key: "preferences", default: Preferences()) struct RoomSwitchView: View { - @EnvironmentObject var appCtx: AppContext @EnvironmentObject var roomCtx: RoomContext @EnvironmentObject var room: Room @@ -20,8 +35,8 @@ struct RoomSwitchView: View { func computeTitle() -> String { if shouldShowRoomView { let elements = [room.name, - room.localParticipant?.name, - room.localParticipant?.identity] + room.localParticipant.name, + room.localParticipant.identity] return elements.compactMap { $0 }.filter { !$0.isEmpty }.joined(separator: " ") } @@ -45,7 +60,6 @@ struct RoomSwitchView: View { // Attaches RoomContext and Room to the environment struct RoomContextView: View { - @EnvironmentObject var appCtx: AppContext @StateObject var roomCtx = RoomContext(store: sync) @@ -129,11 +143,9 @@ extension Decimal { @main struct LiveKitExample: App { - @StateObject var appCtx = AppContext(store: sync) func nearestSafeScale(for target: Int, scale: Double) -> Decimal { - let p = Decimal(sign: .plus, exponent: -3, significand: 1) let t = Decimal(target) var s = Decimal(scale).rounded(3, .down) @@ -146,11 +158,11 @@ struct LiveKitExample: App { } init() { - LoggingSystem.bootstrap({ + LoggingSystem.bootstrap { var logHandler = StreamLogHandler.standardOutput(label: $0) logHandler.logLevel = .debug return logHandler - }) + } } var body: some Scene { @@ -160,33 +172,33 @@ struct LiveKitExample: App { } .handlesExternalEvents(matching: Set(arrayLiteral: "*")) #if os(macOS) - .windowStyle(.hiddenTitleBar) - .windowToolbarStyle(.unifiedCompact) + .windowStyle(.hiddenTitleBar) + .windowToolbarStyle(.unifiedCompact) #endif } } #if os(macOS) -extension View { - func withHostingWindow(_ callback: @escaping (NSWindow) -> Void) -> some View { - self.background(HostingWindowFinder(callback: callback)) + extension View { + func withHostingWindow(_ callback: @escaping (NSWindow) -> Void) -> some View { + background(HostingWindowFinder(callback: callback)) + } } -} -struct HostingWindowFinder: NSViewRepresentable { - var callback: (NSWindow) -> Void + struct HostingWindowFinder: NSViewRepresentable { + var callback: (NSWindow) -> Void - func makeNSView(context: Context) -> NSView { - let view = NSView() - DispatchQueue.main.async { [weak view] in - if let window = view?.window { - self.callback(window) + func makeNSView(context _: Context) -> NSView { + let view = NSView() + DispatchQueue.main.async { [weak view] in + if let window = view?.window { + callback(window) + } } + return view } - return view - } - func updateNSView(_ uiView: NSView, context: Context) {} -} + func updateNSView(_: NSView, context _: Context) {} + } #endif diff --git a/Shared/ParticipantView.swift b/Shared/ParticipantView.swift index 288617c..a3f296a 100644 --- a/Shared/ParticipantView.swift +++ b/Shared/ParticipantView.swift @@ -1,9 +1,25 @@ -import SwiftUI +/* + * Copyright 2023 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 LiveKit import SFSafeSymbols +import SwiftUI struct ParticipantView: View { - @ObservedObject var participant: Participant @EnvironmentObject var appCtx: AppContext @@ -38,7 +54,8 @@ struct ParticipantView: View { if let publication = participant.mainVideoPublication, !publication.muted, let track = publication.track as? VideoTrack, - appCtx.videoViewVisible { + appCtx.videoViewVisible + { ZStack(alignment: .topLeading) { SwiftUIVideoView(track, layoutMode: videoViewMode, @@ -55,7 +72,8 @@ struct ParticipantView: View { } } } else if let publication = participant.mainVideoPublication as? RemoteTrackPublication, - case .notAllowed = publication.subscriptionState { + case .notAllowed = publication.subscriptionState + { // Show no permission icon bgView(systemSymbol: .exclamationmarkCircle, geometry: geometry) } else { @@ -64,18 +82,19 @@ struct ParticipantView: View { } if appCtx.showInformationOverlay { - VStack(alignment: .leading, spacing: 5) { // Video stats if let publication = participant.mainVideoPublication, !publication.muted, - let track = publication.track as? VideoTrack { + let track = publication.track as? VideoTrack + { StatsView(track: track) } // Audio stats if let publication = participant.firstAudioPublication, !publication.muted, - let track = publication.track as? AudioTrack { + let track = publication.track as? AudioTrack + { StatsView(track: track) } } @@ -87,7 +106,6 @@ struct ParticipantView: View { maxHeight: .infinity, alignment: .topLeading ) - } VStack(alignment: .trailing, spacing: 0) { @@ -95,24 +113,23 @@ struct ParticipantView: View { if let subVideoTrack = participant.subVideoTrack { SwiftUIVideoView(subVideoTrack, layoutMode: .fill, - mirrorMode: appCtx.videoViewMirrored ? .mirror : .auto - ) - .background(Color.black) - .aspectRatio(contentMode: .fit) - .frame(width: min(geometry.size.width, geometry.size.height) * 0.3) - .cornerRadius(8) - .padding() + mirrorMode: appCtx.videoViewMirrored ? .mirror : .auto) + .background(Color.black) + .aspectRatio(contentMode: .fit) + .frame(width: min(geometry.size.width, geometry.size.height) * 0.3) + .cornerRadius(8) + .padding() } // Bottom user info bar HStack { - Text("\(participant.identity)") // (\(participant.publish ?? "-")) + Text(participant.identity ?? "") // (\(participant.publish ?? "-")) .lineLimit(1) .truncationMode(.tail) if let publication = participant.mainVideoPublication, - !publication.muted { - + !publication.muted + { // is remote if let remotePub = publication as? RemoteTrackPublication { Menu { @@ -132,7 +149,6 @@ struct ParticipantView: View { } label: { Text("Subscribe") } - } } label: { if case .subscribed = remotePub.subscriptionState { @@ -163,8 +179,8 @@ struct ParticipantView: View { } if let publication = participant.firstAudioPublication, - !publication.muted { - + !publication.muted + { // is remote if let remotePub = publication as? RemoteTrackPublication { Menu { @@ -184,7 +200,6 @@ struct ParticipantView: View { } label: { Text("Subscribe") } - } } label: { if case .subscribed = remotePub.subscriptionState { @@ -234,8 +249,8 @@ struct ParticipantView: View { } }.padding(5) - .frame(minWidth: 0, maxWidth: .infinity) - .background(Color.black.opacity(0.5)) + .frame(minWidth: 0, maxWidth: .infinity) + .background(Color.black.opacity(0.5)) } } .cornerRadius(8) @@ -247,15 +262,14 @@ struct ParticipantView: View { : nil ) }.gesture(TapGesture() - .onEnded { _ in - // Pass the tap event - onTap?(participant) - }) + .onEnded { _ in + // Pass the tap event + onTap?(participant) + }) } } struct StatsView: View { - @ObservedObject private var viewModel: DelegateObserver private let track: Track @@ -285,7 +299,6 @@ struct StatsView: View { } if let trackStats = viewModel.statistics { - // if trackStats.bpsSent != 0 { // HStack(spacing: 3) { // if let codecName = trackStats.codecName { @@ -317,7 +330,6 @@ struct StatsView: View { } extension StatsView { - class DelegateObserver: ObservableObject, TrackDelegate { private let track: Track @Published var dimensions: Dimensions? @@ -332,13 +344,13 @@ extension StatsView { track.add(delegate: self) } - func track(_ track: VideoTrack, didUpdate dimensions: Dimensions?) { + func track(_: VideoTrack, didUpdate dimensions: Dimensions?) { Task.detached { @MainActor in self.dimensions = dimensions } } - func track(_ track: Track, didUpdateStatistics statistics: TrackStatistics) { + func track(_: Track, didUpdateStatistics statistics: TrackStatistics) { Task.detached { @MainActor in self.statistics = statistics } diff --git a/Shared/RoomView.swift b/Shared/RoomView.swift index 750d8f8..6bb2aff 100644 --- a/Shared/RoomView.swift +++ b/Shared/RoomView.swift @@ -1,66 +1,80 @@ -import SwiftUI +/* + * Copyright 2023 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 LiveKit import SFSafeSymbols +import SwiftUI import WebRTC #if !os(macOS) -let adaptiveMin = 170.0 -let toolbarPlacement: ToolbarItemPlacement = .bottomBar + let adaptiveMin = 170.0 + let toolbarPlacement: ToolbarItemPlacement = .bottomBar #else -let adaptiveMin = 300.0 -let toolbarPlacement: ToolbarItemPlacement = .primaryAction + let adaptiveMin = 300.0 + let toolbarPlacement: ToolbarItemPlacement = .primaryAction #endif extension CIImage { // helper to create a `CIImage` for both platforms convenience init(named name: String) { #if !os(macOS) - self.init(cgImage: UIImage(named: name)!.cgImage!) + self.init(cgImage: UIImage(named: name)!.cgImage!) #else - self.init(data: NSImage(named: name)!.tiffRepresentation!)! + self.init(data: NSImage(named: name)!.tiffRepresentation!)! #endif } } #if os(macOS) -// keeps weak reference to NSWindow -class WindowAccess: ObservableObject { - - private weak var window: NSWindow? - - deinit { - // reset changed properties - DispatchQueue.main.async { [weak window] in - window?.level = .normal + // keeps weak reference to NSWindow + class WindowAccess: ObservableObject { + private weak var window: NSWindow? + + deinit { + // reset changed properties + DispatchQueue.main.async { [weak window] in + window?.level = .normal + } } - } - @Published public var pinned: Bool = false { - didSet { - guard oldValue != pinned else { return } - self.level = pinned ? .floating : .normal + @Published public var pinned: Bool = false { + didSet { + guard oldValue != pinned else { return } + level = pinned ? .floating : .normal + } } - } - private var level: NSWindow.Level { - get { window?.level ?? .normal } - set { - DispatchQueue.main.async { - self.window?.level = newValue - self.objectWillChange.send() + private var level: NSWindow.Level { + get { window?.level ?? .normal } + set { + DispatchQueue.main.async { + self.window?.level = newValue + self.objectWillChange.send() + } } } - } - public func set(window: NSWindow?) { - self.window = window - DispatchQueue.main.async { self.objectWillChange.send() } + public func set(window: NSWindow?) { + self.window = window + DispatchQueue.main.async { self.objectWillChange.send() } + } } -} #endif struct RoomView: View { - @EnvironmentObject var appCtx: AppContext @EnvironmentObject var roomCtx: RoomContext @EnvironmentObject var room: Room @@ -71,14 +85,13 @@ struct RoomView: View { @State private var screenPickerPresented = false #if os(macOS) - @ObservedObject private var windowAccess = WindowAccess() + @ObservedObject private var windowAccess = WindowAccess() #endif @State private var showConnectionTime = true func messageView(_ message: ExampleRoomMessage) -> some View { - - let isMe = message.senderSid == room.localParticipant?.sid + let isMe = message.senderSid == room.localParticipant.sid return HStack { if isMe { @@ -97,7 +110,7 @@ struct RoomView: View { Spacer() } }.padding(.vertical, 5) - .padding(.horizontal, 10) + .padding(.horizontal, 10) } func scrollToBottom(_ scrollView: ScrollViewProxy) { @@ -108,7 +121,6 @@ struct RoomView: View { } func messagesView(geometry: GeometryProxy) -> some View { - VStack(spacing: 0) { ScrollViewReader { scrollView in ScrollView(.vertical, showsIndicators: true) { @@ -137,7 +149,6 @@ struct RoomView: View { ) } HStack(spacing: 0) { - TextField("Enter message", text: $roomCtx.textFieldString) .textFieldStyle(PlainTextFieldStyle()) .disableAutocorrection(true) @@ -158,7 +169,6 @@ struct RoomView: View { .foregroundColor(roomCtx.textFieldString.isEmpty ? nil : Color.lkRed) } .buttonStyle(.borderless) - } .padding() .background(Color.lkGray2) @@ -180,9 +190,7 @@ struct RoomView: View { } func content(geometry: GeometryProxy) -> some View { - VStack { - if showConnectionTime { Text("Connected (\([room.serverRegion, "\(String(describing: room.connectStopwatch.total().rounded(to: 2)))s"].compactMap { $0 }.joined(separator: ", ")))") .multilineTextAlignment(.center) @@ -198,16 +206,16 @@ struct RoomView: View { } HorVStack(axis: geometry.isTall ? .vertical : .horizontal, spacing: 5) { - Group { if let focusParticipant = roomCtx.focusParticipant { ZStack(alignment: .bottomTrailing) { ParticipantView(participant: focusParticipant, - videoViewMode: appCtx.videoViewMode) { _ in + videoViewMode: appCtx.videoViewMode) + { _ in roomCtx.focusParticipant = nil } .overlay(RoundedRectangle(cornerRadius: 5) - .stroke(Color.lkRed.opacity(0.7), lineWidth: 5.0)) + .stroke(Color.lkRed.opacity(0.7), lineWidth: 5.0)) Text("SELECTED") .font(.system(size: 10)) .fontWeight(.bold) @@ -224,9 +232,9 @@ struct RoomView: View { // Array([room.allParticipants.values, room.allParticipants.values].joined()) ParticipantLayout(sortedParticipants(), spacing: 5) { participant in ParticipantView(participant: participant, - videoViewMode: appCtx.videoViewMode) { participant in + videoViewMode: appCtx.videoViewMode) + { participant in roomCtx.focusParticipant = participant - } } } @@ -247,24 +255,19 @@ struct RoomView: View { } var body: some View { - GeometryReader { geometry in content(geometry: geometry) } .toolbar { ToolbarItemGroup(placement: toolbarPlacement) { - - // Text("(\(room.room.remoteParticipants.count)) ") - #if os(macOS) - if let name = room.name { - Text(name) - .fontWeight(.bold) - } + if let name = room.name { + Text(name) + .fontWeight(.bold) + } + + Text(room.localParticipant.identity ?? "") - if let identity = room.localParticipant?.identity { - Text(identity) - } #endif // #if os(macOS) @@ -285,18 +288,19 @@ struct RoomView: View { Spacer() Group { - let isCameraEnabled = room.localParticipant?.isCameraEnabled() ?? false - let isMicrophoneEnabled = room.localParticipant?.isMicrophoneEnabled() ?? false - let isScreenShareEnabled = room.localParticipant?.isScreenShareEnabled() ?? false + let isCameraEnabled = room.localParticipant.isCameraEnabled() + let isMicrophoneEnabled = room.localParticipant.isMicrophoneEnabled() + let isScreenShareEnabled = room.localParticipant.isScreenShareEnabled() - if (isCameraEnabled) && CameraCapturer.canSwitchPosition() { + if isCameraEnabled, CameraCapturer.canSwitchPosition() { Menu { Button("Switch position") { Task { isCameraPublishingBusy = true defer { Task { @MainActor in isCameraPublishingBusy = false } } - if let track = room.localParticipant?.firstCameraVideoTrack as? LocalVideoTrack, - let cameraCapturer = track.capturer as? CameraCapturer { + if let track = room.localParticipant.firstCameraVideoTrack as? LocalVideoTrack, + let cameraCapturer = track.capturer as? CameraCapturer + { try await cameraCapturer.switchCameraPosition() } } @@ -305,7 +309,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: { @@ -317,113 +321,112 @@ struct RoomView: View { } else { // Toggle camera enabled Button(action: { - Task { - isCameraPublishingBusy = true - defer { Task { @MainActor in isCameraPublishingBusy = false } } - try await room.localParticipant?.setCamera(enabled: !isCameraEnabled) - } - }, - label: { - Image(systemSymbol: .videoFill) - .renderingMode(isCameraEnabled ? .original : .template) - }) - // disable while publishing/un-publishing - .disabled(isCameraPublishingBusy) + Task { + isCameraPublishingBusy = true + defer { Task { @MainActor in isCameraPublishingBusy = false } } + try await room.localParticipant.setCamera(enabled: !isCameraEnabled) + } + }, + label: { + Image(systemSymbol: .videoFill) + .renderingMode(isCameraEnabled ? .original : .template) + }) + // disable while publishing/un-publishing + .disabled(isCameraPublishingBusy) } // Toggle microphone enabled Button(action: { - Task { - isMicrophonePublishingBusy = true - defer { Task { @MainActor in isMicrophonePublishingBusy = false } } - try await room.localParticipant?.setMicrophone(enabled: !isMicrophoneEnabled) - } - }, - label: { - Image(systemSymbol: .micFill) - .renderingMode(isMicrophoneEnabled ? .original : .template) - }) - // disable while publishing/un-publishing - .disabled(isMicrophonePublishingBusy) + Task { + isMicrophonePublishingBusy = true + defer { Task { @MainActor in isMicrophonePublishingBusy = false } } + try await room.localParticipant.setMicrophone(enabled: !isMicrophoneEnabled) + } + }, + label: { + Image(systemSymbol: .micFill) + .renderingMode(isMicrophoneEnabled ? .original : .template) + }) + // disable while publishing/un-publishing + .disabled(isMicrophonePublishingBusy) #if os(iOS) - Button(action: { - Task { - isScreenSharePublishingBusy = true - defer { Task { @MainActor in isScreenSharePublishingBusy = false } } - try await room.localParticipant?.setScreenShare(enabled: !isScreenShareEnabled) - } - }, - label: { - Image(systemSymbol: .rectangleFillOnRectangleFill) - .renderingMode(isScreenShareEnabled ? .original : .template) - }) - // disable while publishing/un-publishing - .disabled(isScreenSharePublishingBusy) + Button(action: { + Task { + isScreenSharePublishingBusy = true + defer { Task { @MainActor in isScreenSharePublishingBusy = false } } + try await room.localParticipant?.setScreenShare(enabled: !isScreenShareEnabled) + } + }, + label: { + Image(systemSymbol: .rectangleFillOnRectangleFill) + .renderingMode(isScreenShareEnabled ? .original : .template) + }) + // disable while publishing/un-publishing + .disabled(isScreenSharePublishingBusy) #elseif os(macOS) - Button(action: { - if #available(macOS 12.3, *) { - if isScreenShareEnabled { - // Turn off screen share - Task { - isScreenSharePublishingBusy = true - defer { Task { @MainActor in isScreenSharePublishingBusy = false } } - try await roomCtx.setScreenShareMacOS(enabled: false) - } - } else { - screenPickerPresented = true + Button(action: { + if #available(macOS 12.3, *) { + if isScreenShareEnabled { + // Turn off screen share + Task { + isScreenSharePublishingBusy = true + defer { Task { @MainActor in isScreenSharePublishingBusy = false } } + try await roomCtx.setScreenShareMacOS(enabled: false) + } + } else { + screenPickerPresented = true + } + } + }, + label: { + Image(systemSymbol: .rectangleFillOnRectangleFill) + .renderingMode(isScreenShareEnabled ? .original : .template) + .foregroundColor(isScreenShareEnabled ? Color.green : Color.white) + }).popover(isPresented: $screenPickerPresented) { + if #available(macOS 12.3, *) { + ScreenShareSourcePickerView { source in + Task { + isScreenSharePublishingBusy = true + defer { Task { @MainActor in isScreenSharePublishingBusy = false } } + try await roomCtx.setScreenShareMacOS(enabled: true, screenShareSource: source) + } + screenPickerPresented = false + }.padding() } } - }, - label: { - Image(systemSymbol: .rectangleFillOnRectangleFill) - .renderingMode(isScreenShareEnabled ? .original : .template) - .foregroundColor(isScreenShareEnabled ? Color.green : Color.white) - }).popover(isPresented: $screenPickerPresented) { - if #available(macOS 12.3, *) { - ScreenShareSourcePickerView { source in - Task { - isScreenSharePublishingBusy = true - defer { Task { @MainActor in isScreenSharePublishingBusy = false } } - try await roomCtx.setScreenShareMacOS(enabled: true, screenShareSource: source) - } - screenPickerPresented = false - }.padding() - } - } - .disabled(isScreenSharePublishingBusy) + .disabled(isScreenSharePublishingBusy) #endif // Toggle messages view (chat example) Button(action: { - withAnimation { - roomCtx.showMessagesView.toggle() - } - }, - label: { - Image(systemSymbol: .messageFill) - .renderingMode(roomCtx.showMessagesView ? .original : .template) - }) + withAnimation { + roomCtx.showMessagesView.toggle() + } + }, + label: { + Image(systemSymbol: .messageFill) + .renderingMode(roomCtx.showMessagesView ? .original : .template) + }) } // Spacer() #if os(iOS) - SwiftUIAudioRoutePickerButton() + SwiftUIAudioRoutePickerButton() #endif Menu { - #if os(macOS) - Button { - if let url = URL(string: "livekit://") { - NSWorkspace.shared.open(url) + Button { + if let url = URL(string: "livekit://") { + NSWorkspace.shared.open(url) + } + } label: { + Text("New window") } - } label: { - Text("New window") - } - Divider() + Divider() #endif @@ -438,25 +441,25 @@ struct RoomView: View { #if os(macOS) - Group { - Picker("Output device", selection: $appCtx.outputDevice) { - ForEach(AudioManager.shared.outputDevices) { device in - Text(device.isDefault ? "Default" : "\(device.name)").tag(device) + Group { + Picker("Output device", selection: $appCtx.outputDevice) { + ForEach(AudioManager.shared.outputDevices) { device in + Text(device.isDefault ? "Default" : "\(device.name)").tag(device) + } } - } - Picker("Input device", selection: $appCtx.inputDevice) { - ForEach(AudioManager.shared.inputDevices) { device in - Text(device.isDefault ? "Default" : "\(device.name)").tag(device) + Picker("Input device", selection: $appCtx.inputDevice) { + ForEach(AudioManager.shared.inputDevices) { device in + Text(device.isDefault ? "Default" : "\(device.name)").tag(device) + } } } - } #endif Divider() Button { Task { - await room.localParticipant?.unpublishAll() + await room.localParticipant.unpublishAll() } } label: { Text("Unpublish all") @@ -518,7 +521,7 @@ struct RoomView: View { Menu { Button { Task { - try await room.localParticipant?.setTrackSubscriptionPermissions(allParticipantsAllowed: true) + try await room.localParticipant.setTrackSubscriptionPermissions(allParticipantsAllowed: true) } } label: { Text("Allow all") @@ -526,7 +529,7 @@ struct RoomView: View { Button { Task { - try await room.localParticipant?.setTrackSubscriptionPermissions(allParticipantsAllowed: false) + try await room.localParticipant.setTrackSubscriptionPermissions(allParticipantsAllowed: false) } } label: { Text("Disallow all") @@ -545,14 +548,14 @@ struct RoomView: View { // Disconnect Button(action: { - Task { - await roomCtx.disconnect() - } - }, - label: { - Image(systemSymbol: .xmarkCircleFill) - .renderingMode(.original) - }) + Task { + await roomCtx.disconnect() + } + }, + label: { + Image(systemSymbol: .xmarkCircleFill) + .renderingMode(.original) + }) } } // #if os(macOS) @@ -563,7 +566,7 @@ struct RoomView: View { Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { _ in DispatchQueue.main.async { withAnimation { - self.showConnectionTime = false + showConnectionTime = false } } } @@ -572,7 +575,6 @@ struct RoomView: View { } struct ParticipantLayout: View { - let views: [AnyView] let spacing: CGFloat @@ -580,9 +582,10 @@ struct ParticipantLayout: View { _ data: Data, id: KeyPath = \.self, spacing: CGFloat, - @ViewBuilder content: @escaping (Data.Element) -> Content) { + @ViewBuilder content: @escaping (Data.Element) -> Content + ) { self.spacing = spacing - self.views = data.map { AnyView(content($0[keyPath: id])) } + views = data.map { AnyView(content($0[keyPath: id])) } } func computeColumn(with geometry: GeometryProxy) -> (x: Int, y: Int) { @@ -593,16 +596,16 @@ struct ParticipantLayout: View { } func grid(axis: Axis, geometry: GeometryProxy) -> some View { - ScrollView([ axis == .vertical ? .vertical : .horizontal ]) { + ScrollView([axis == .vertical ? .vertical : .horizontal]) { HorVGrid(axis: axis, columns: [GridItem(.flexible())], spacing: spacing) { - ForEach(0..: View { } else if geometry.size.height <= 300 { grid(axis: .horizontal, geometry: geometry) } else { - let verticalWhenTall: Axis = geometry.isTall ? .vertical : .horizontal let horizontalWhenTall: Axis = geometry.isTall ? .horizontal : .vertical @@ -623,35 +625,34 @@ struct ParticipantLayout: View { // simply return first view case 1: views[0] case 3: HorVStack(axis: verticalWhenTall, spacing: spacing) { - views[0] - HorVStack(axis: horizontalWhenTall, spacing: spacing) { - views[1] - views[2] - } - } - case 5: HorVStack(axis: verticalWhenTall, spacing: spacing) { - views[0] - if geometry.isTall { - HStack(spacing: spacing) { + views[0] + HorVStack(axis: horizontalWhenTall, spacing: spacing) { views[1] views[2] } - HStack(spacing: spacing) { - views[3] - views[4] - - } - } else { - VStack(spacing: spacing) { - views[1] - views[3] - } - VStack(spacing: spacing) { - views[2] - views[4] + } + case 5: HorVStack(axis: verticalWhenTall, spacing: spacing) { + views[0] + if geometry.isTall { + HStack(spacing: spacing) { + views[1] + views[2] + } + HStack(spacing: spacing) { + views[3] + views[4] + } + } else { + VStack(spacing: spacing) { + views[1] + views[3] + } + VStack(spacing: spacing) { + views[2] + views[4] + } } } - } // case 6: // if geometry.isTall { // VStack { @@ -685,9 +686,9 @@ struct ParticipantLayout: View { default: let c = computeColumn(with: geometry) VStack(spacing: spacing) { - ForEach(0...(c.y - 1), id: \.self) { y in + ForEach(0 ... (c.y - 1), id: \.self) { y in HStack(spacing: spacing) { - ForEach(0...(c.x - 1), id: \.self) { x in + ForEach(0 ... (c.x - 1), id: \.self) { x in let index = (y * c.x) + x if index < views.count { views[index] @@ -696,7 +697,6 @@ struct ParticipantLayout: View { } } } - } } } @@ -714,8 +714,8 @@ struct HorVStack: View { horizontalAlignment: HorizontalAlignment = .center, verticalAlignment: VerticalAlignment = .center, spacing: CGFloat? = nil, - @ViewBuilder content: @escaping () -> Content) { - + @ViewBuilder content: @escaping () -> Content) + { self.axis = axis self.horizontalAlignment = horizontalAlignment self.verticalAlignment = verticalAlignment @@ -743,8 +743,8 @@ struct HorVGrid: View { init(axis: Axis = .horizontal, columns: [GridItem], spacing: CGFloat? = nil, - @ViewBuilder content: @escaping () -> Content) { - + @ViewBuilder content: @escaping () -> Content) + { self.axis = axis self.spacing = spacing self.columns = columns @@ -763,7 +763,6 @@ struct HorVGrid: View { } extension GeometryProxy { - public var isTall: Bool { size.height > size.width } diff --git a/Shared/Support/Bundle.swift b/Shared/Support/Bundle.swift index d8811ad..548dfa1 100644 --- a/Shared/Support/Bundle.swift +++ b/Shared/Support/Bundle.swift @@ -1,14 +1,30 @@ +/* + * Copyright 2023 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 -extension Bundle { - public var appName: String { getInfo("CFBundleName") } - public var displayName: String {getInfo("CFBundleDisplayName")} - public var language: String {getInfo("CFBundleDevelopmentRegion")} - public var identifier: String {getInfo("CFBundleIdentifier")} +public extension Bundle { + var appName: String { getInfo("CFBundleName") } + var displayName: String { getInfo("CFBundleDisplayName") } + var language: String { getInfo("CFBundleDevelopmentRegion") } + var identifier: String { getInfo("CFBundleIdentifier") } - public var appBuild: String { getInfo("CFBundleVersion") } - public var appVersionLong: String { getInfo("CFBundleShortVersionString") } - public var appVersionShort: String { getInfo("CFBundleShortVersion") } + var appBuild: String { getInfo("CFBundleVersion") } + var appVersionLong: String { getInfo("CFBundleShortVersionString") } + var appVersionShort: String { getInfo("CFBundleShortVersion") } - fileprivate func getInfo(_ str: String) -> String { infoDictionary?[str] as? String ?? "⚠️" } + private func getInfo(_ str: String) -> String { infoDictionary?[str] as? String ?? "⚠️" } } diff --git a/Shared/Support/ConnectionHistory.swift b/Shared/Support/ConnectionHistory.swift index 043e9e5..7bb8608 100644 --- a/Shared/Support/ConnectionHistory.swift +++ b/Shared/Support/ConnectionHistory.swift @@ -1,8 +1,23 @@ -import SwiftUI +/* + * Copyright 2023 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 LiveKit +import SwiftUI struct ConnectionHistory: Codable { - let updated: Date let url: String let token: String @@ -11,43 +26,37 @@ struct ConnectionHistory: Codable { let roomSid: String? let roomName: String? let participantSid: String - let participantIdentity: String + let participantIdentity: String? let participantName: String? } extension ConnectionHistory: Identifiable { - var id: Int { - self.hashValue + hashValue } } extension ConnectionHistory: Hashable, Equatable { - func hash(into hasher: inout Hasher) { hasher.combine(url) hasher.combine(token) } static func == (lhs: ConnectionHistory, rhs: ConnectionHistory) -> Bool { - return lhs.url == rhs.url && lhs.token == rhs.token + lhs.url == rhs.url && lhs.token == rhs.token } } -extension Sequence where Element == ConnectionHistory { - +extension Sequence { var sortedByUpdated: [ConnectionHistory] { Array(self).sorted { $0.updated > $1.updated } } } -extension Set where Element == ConnectionHistory { - +extension Set { mutating func update(room: Room, e2ee: Bool, e2eeKey: String) { - guard let url = room.url, - let token = room.token, - let localParticipant = room.localParticipant else { return } + let token = room.token else { return } let element = ConnectionHistory( updated: Date(), @@ -57,11 +66,11 @@ extension Set where Element == ConnectionHistory { e2eeKey: e2eeKey, roomSid: room.sid, roomName: room.name, - participantSid: localParticipant.sid, - participantIdentity: localParticipant.identity, - participantName: localParticipant.name + participantSid: room.localParticipant.sid, + participantIdentity: room.localParticipant.identity, + participantName: room.localParticipant.name ) - self.update(with: element) + update(with: element) } } diff --git a/Shared/Support/SecureStore.swift b/Shared/Support/SecureStore.swift index 8d2985f..0fdd120 100644 --- a/Shared/Support/SecureStore.swift +++ b/Shared/Support/SecureStore.swift @@ -1,7 +1,23 @@ -import SwiftUI -import KeychainAccess +/* + * Copyright 2023 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 Combine +import KeychainAccess import LiveKit +import SwiftUI struct Preferences: Codable, Equatable { var url = "" @@ -33,7 +49,6 @@ let encoder = JSONEncoder() let decoder = JSONDecoder() class ValueStore: ObservableObject { - private let store: Keychain private let key: String private let message = "" @@ -52,14 +67,15 @@ class ValueStore: ObservableObject { .synchronizable(true) } - public init(store: Keychain, key: String, `default`: T) { + public init(store: Keychain, key: String, default: T) { self.store = store self.key = key - self.value = `default` + value = `default` if let data = try? storeWithOptions.getData(key), - let result = try? decoder.decode(T.self, from: data) { - self.value = result + let result = try? decoder.decode(T.self, from: data) + { + value = result } } @@ -77,8 +93,8 @@ class ValueStore: ObservableObject { public func sync() { do { let data = try encoder.encode(value) - try self.storeWithOptions.set(data, key: key) - } catch let error { + try storeWithOptions.set(data, key: key) + } catch { print("Failed to write in Keychain, error: \(error)") } } diff --git a/Shared/Views/ScreenShareSourcePickerView.swift b/Shared/Views/ScreenShareSourcePickerView.swift index 3f77929..f4f1014 100644 --- a/Shared/Views/ScreenShareSourcePickerView.swift +++ b/Shared/Views/ScreenShareSourcePickerView.swift @@ -1,139 +1,150 @@ -import Foundation +/* + * Copyright 2023 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 CoreGraphics -import SwiftUI +import Foundation import LiveKit +import SwiftUI #if os(macOS) -@available(macOS 12.3, *) -class ScreenShareSourcePickerCtrl: ObservableObject { - - @Published var tracks = [LocalVideoTrack]() - @Published var mode: ScreenShareSourcePickerView.Mode = .display { - didSet { - guard oldValue != mode else { return } - Task { - try await restartTracks() + @available(macOS 12.3, *) + class ScreenShareSourcePickerCtrl: ObservableObject { + @Published var tracks = [LocalVideoTrack]() + @Published var mode: ScreenShareSourcePickerView.Mode = .display { + didSet { + guard oldValue != mode else { return } + Task { + try await restartTracks() + } } } - } - - private func restartTracks() async throws { - // stop in parallel - await withThrowingTaskGroup(of: Void.self) { group in - for track in tracks { - group.addTask { - try await track.stop() + private func restartTracks() async throws { + // stop in parallel + await withThrowingTaskGroup(of: Void.self) { group in + for track in tracks { + group.addTask { + try await track.stop() + } } } - } - let sources = try await MacOSScreenCapturer.sources(for: (self.mode == .display ? .display : .window)) - let options = ScreenShareCaptureOptions(dimensions: .h360_43, fps: 5) - let _newTracks = sources.map { LocalVideoTrack.createMacOSScreenShareTrack(source: $0, options: options) } + let sources = try await MacOSScreenCapturer.sources(for: mode == .display ? .display : .window) + let options = ScreenShareCaptureOptions(dimensions: .h360_43, fps: 5) + let _newTracks = sources.map { LocalVideoTrack.createMacOSScreenShareTrack(source: $0, options: options) } - Task { @MainActor in - self.tracks = _newTracks - } + Task { @MainActor in + self.tracks = _newTracks + } - // start in parallel - await withThrowingTaskGroup(of: Void.self) { group in - for track in _newTracks { - group.addTask { - try await track.start() + // start in parallel + await withThrowingTaskGroup(of: Void.self) { group in + for track in _newTracks { + group.addTask { + try await track.start() + } } } } - } - - init() { - Task { - try await restartTracks() + init() { + Task { + try await restartTracks() + } } - } - deinit { + deinit { + print("\(type(of: self)) deinit") - print("\(type(of: self)) deinit") + // copy + let _tracks = tracks - // copy - let _tracks = tracks - - Task { - // stop in parallel - await withThrowingTaskGroup(of: Void.self) { group in - for track in _tracks { - group.addTask { - try await track.stop() + Task { + // stop in parallel + await withThrowingTaskGroup(of: Void.self) { group in + for track in _tracks { + group.addTask { + try await track.stop() + } } } } } } -} - -typealias OnPickScreenShareSource = (MacOSScreenCaptureSource) -> Void -@available(macOS 12.3, *) -struct ScreenShareSourcePickerView: View { + typealias OnPickScreenShareSource = (MacOSScreenCaptureSource) -> Void - public enum Mode { - case display - case window - } + @available(macOS 12.3, *) + struct ScreenShareSourcePickerView: View { + public enum Mode { + case display + case window + } - @ObservedObject var ctrl = ScreenShareSourcePickerCtrl() + @ObservedObject var ctrl = ScreenShareSourcePickerCtrl() - let onPickScreenShareSource: OnPickScreenShareSource? + let onPickScreenShareSource: OnPickScreenShareSource? - private var columns = [ - GridItem(.fixed(250)), - GridItem(.fixed(250)) - ] + private var columns = [ + GridItem(.fixed(250)), + GridItem(.fixed(250)), + ] - init(onPickScreenShareSource: OnPickScreenShareSource? = nil) { - self.onPickScreenShareSource = onPickScreenShareSource - } - - var body: some View { + init(onPickScreenShareSource: OnPickScreenShareSource? = nil) { + self.onPickScreenShareSource = onPickScreenShareSource + } - VStack { - Picker("", selection: $ctrl.mode) { - Text("Entire Screen").tag(ScreenShareSourcePickerView.Mode.display) - Text("Application Window").tag(ScreenShareSourcePickerView.Mode.window) - } - .pickerStyle(SegmentedPickerStyle()) - - ScrollView(.vertical, showsIndicators: true) { - LazyVGrid(columns: columns, - alignment: .center, - spacing: 10) { - - ForEach(ctrl.tracks) { track in - ZStack { - SwiftUIVideoView(track, layoutMode: .fit) - .aspectRatio(1, contentMode: .fit) - .onTapGesture { - guard let capturer = track.capturer as? MacOSScreenCapturer, - let source = capturer.captureSource else { return } - onPickScreenShareSource?(source) + var body: some View { + VStack { + Picker("", selection: $ctrl.mode) { + Text("Entire Screen").tag(ScreenShareSourcePickerView.Mode.display) + Text("Application Window").tag(ScreenShareSourcePickerView.Mode.window) + } + .pickerStyle(SegmentedPickerStyle()) + + ScrollView(.vertical, showsIndicators: true) { + LazyVGrid(columns: columns, + alignment: .center, + spacing: 10) + { + ForEach(ctrl.tracks) { track in + ZStack { + SwiftUIVideoView(track, layoutMode: .fit) + .aspectRatio(1, contentMode: .fit) + .onTapGesture { + guard let capturer = track.capturer as? MacOSScreenCapturer, + let source = capturer.captureSource else { return } + onPickScreenShareSource?(source) + } + + if let capturer = track.capturer as? MacOSScreenCapturer, + let source = capturer.captureSource as? MacOSWindow, + let appName = source.owningApplication?.applicationName + { + Text(appName) + .shadow(color: .black, radius: 1) } - - if let capturer = track.capturer as? MacOSScreenCapturer, - let source = capturer.captureSource as? MacOSWindow, - let appName = source.owningApplication?.applicationName { - Text(appName) - .shadow(color: .black, radius: 1) } } } } + .frame(minHeight: 350) } - .frame(minHeight: 350) } } -} #endif diff --git a/iOS/BroadcastExt/LoggingOSLog.swift b/iOS/BroadcastExt/LoggingOSLog.swift index a8aa4d6..d7a5619 100644 --- a/iOS/BroadcastExt/LoggingOSLog.swift +++ b/iOS/BroadcastExt/LoggingOSLog.swift @@ -1,26 +1,41 @@ +/* + * Copyright 2023 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 import Logging import os public struct LoggingOSLog: LogHandler { - public var logLevel: Logging.Logger.Level = .debug private let oslogger: OSLog public init(label: String) { - self.oslogger = OSLog(subsystem: label, category: "") + oslogger = OSLog(subsystem: label, category: "") } - public init(label: String, log: OSLog) { - self.oslogger = log + public init(label _: String, log: OSLog) { + oslogger = log } - public func log(level: Logging.Logger.Level, message: Logging.Logger.Message, metadata: Logging.Logger.Metadata?, file: String, function: String, line: UInt) { - var combinedPrettyMetadata = self.prettyMetadata + public func log(level _: Logging.Logger.Level, message: Logging.Logger.Message, metadata: Logging.Logger.Metadata?, file _: String, function _: String, line _: UInt) { + var combinedPrettyMetadata = prettyMetadata if let metadataOverride = metadata, !metadataOverride.isEmpty { - combinedPrettyMetadata = self.prettify( + combinedPrettyMetadata = prettify( self.metadata.merging(metadataOverride) { - return $1 + $1 } ) } @@ -29,13 +44,13 @@ public struct LoggingOSLog: LogHandler { if combinedPrettyMetadata != nil { formedMessage += " -- " + combinedPrettyMetadata! } - os_log("%{public}@", log: self.oslogger, type: OSLogType.from(loggerLevel: .info), formedMessage as NSString) + os_log("%{public}@", log: oslogger, type: OSLogType.from(loggerLevel: .info), formedMessage as NSString) } private var prettyMetadata: String? public var metadata = Logger.Metadata() { didSet { - self.prettyMetadata = self.prettify(self.metadata) + prettyMetadata = prettify(metadata) } } @@ -44,10 +59,10 @@ public struct LoggingOSLog: LogHandler { /// - metadataKey: the key for the metadata item. public subscript(metadataKey metadataKey: String) -> Logging.Logger.Metadata.Value? { get { - return self.metadata[metadataKey] + metadata[metadataKey] } set { - self.metadata[metadataKey] = newValue + metadata[metadataKey] = newValue } } diff --git a/iOS/BroadcastExt/SampleHandler.swift b/iOS/BroadcastExt/SampleHandler.swift index 74eddc7..796ae63 100644 --- a/iOS/BroadcastExt/SampleHandler.swift +++ b/iOS/BroadcastExt/SampleHandler.swift @@ -1,9 +1,18 @@ -// -// NewSampleHandler.swift -// BroadcastExt -// -// Created by David Liu on 6/15/22. -// +/* + * Copyright 2023 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 import LiveKit @@ -12,13 +21,12 @@ import OSLog private let broadcastLogger = OSLog(subsystem: "io.livekit.example.SwiftSDK", category: "Broadcast") class SampleHandler: LKSampleHandler { - - public override init() { + override public init() { // Turn on logging for the Broadcast Extension - LoggingSystem.bootstrap({ label in + LoggingSystem.bootstrap { label in var logHandler = LoggingOSLog(label: label, log: broadcastLogger) logHandler.logLevel = .debug return logHandler - }) + } } } From abb32a40a7f5698fac4585081da242e519ccc602 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Tue, 7 Nov 2023 02:06:06 +0800 Subject: [PATCH 09/32] refactor --- LiveKitExample.xcodeproj/project.pbxproj | 18 +++++++---- Shared/ParticipantView.swift | 1 - .../ExampleRoomMessage.swift} | 22 ------------- Shared/Support/Participant+Helpers.swift | 31 +++++++++++++++++++ 4 files changed, 43 insertions(+), 29 deletions(-) rename Shared/{ExampleObservableRoom.swift => Support/ExampleRoomMessage.swift} (68%) create mode 100644 Shared/Support/Participant+Helpers.swift diff --git a/LiveKitExample.xcodeproj/project.pbxproj b/LiveKitExample.xcodeproj/project.pbxproj index f5f42b9..1eb6448 100644 --- a/LiveKitExample.xcodeproj/project.pbxproj +++ b/LiveKitExample.xcodeproj/project.pbxproj @@ -9,8 +9,10 @@ /* Begin PBXBuildFile section */ 680FE2F227A8EF7700B6F6DB /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = 680FE2F127A8EF7700B6F6DB /* SFSafeSymbols */; }; 680FE2F427A8EFF700B6F6DB /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = 680FE2F327A8EFF700B6F6DB /* SFSafeSymbols */; }; - 6816B1A8272D45DF005ADB85 /* ExampleObservableRoom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6816B1A7272D45DF005ADB85 /* ExampleObservableRoom.swift */; }; - 6816B1A9272D45DF005ADB85 /* ExampleObservableRoom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6816B1A7272D45DF005ADB85 /* ExampleObservableRoom.swift */; }; + 6816968E2AF96240008ED486 /* Participant+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6816968D2AF96240008ED486 /* Participant+Helpers.swift */; }; + 6816968F2AF96240008ED486 /* Participant+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6816968D2AF96240008ED486 /* Participant+Helpers.swift */; }; + 6816B1A8272D45DF005ADB85 /* ExampleRoomMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6816B1A7272D45DF005ADB85 /* ExampleRoomMessage.swift */; }; + 6816B1A9272D45DF005ADB85 /* ExampleRoomMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6816B1A7272D45DF005ADB85 /* ExampleRoomMessage.swift */; }; 6816B1B0272D9198005ADB85 /* ParticipantView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6816B1AF272D9198005ADB85 /* ParticipantView.swift */; }; 6816B1B1272D9198005ADB85 /* ParticipantView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6816B1AF272D9198005ADB85 /* ParticipantView.swift */; }; 681A0AB727D888D80097E3F4 /* LiveKit in Frameworks */ = {isa = PBXBuildFile; productRef = 681A0AB627D888D80097E3F4 /* LiveKit */; }; @@ -72,7 +74,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 6816B1A7272D45DF005ADB85 /* ExampleObservableRoom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleObservableRoom.swift; sourceTree = ""; }; + 6816968D2AF96240008ED486 /* Participant+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Participant+Helpers.swift"; sourceTree = ""; }; + 6816B1A7272D45DF005ADB85 /* ExampleRoomMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleRoomMessage.swift; sourceTree = ""; }; 6816B1AF272D9198005ADB85 /* ParticipantView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParticipantView.swift; sourceTree = ""; }; 681E3F38271FC772007BB547 /* RoomContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomContext.swift; sourceTree = ""; }; 681E3F3E271FC795007BB547 /* Custom.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Custom.swift; sourceTree = ""; }; @@ -148,9 +151,11 @@ 683720D427A0640D007DA986 /* Support */ = { isa = PBXGroup; children = ( + 6816B1A7272D45DF005ADB85 /* ExampleRoomMessage.swift */, 683720D127A06404007DA986 /* ConnectionHistory.swift */, 6847616327B44A1A001611BE /* Bundle.swift */, 68816CC427B4DCD500E24622 /* SecureStore.swift */, + 6816968D2AF96240008ED486 /* Participant+Helpers.swift */, ); path = Support; sourceTree = ""; @@ -214,7 +219,6 @@ 681E3F42271FC7AD007BB547 /* ConnectView.swift */, 681E3F41271FC7AC007BB547 /* RoomView.swift */, 68B3853C271E780600711D5F /* LiveKitExample.swift */, - 6816B1A7272D45DF005ADB85 /* ExampleObservableRoom.swift */, 6816B1AF272D9198005ADB85 /* ParticipantView.swift */, 68B3853E271E780700711D5F /* Assets.xcassets */, ); @@ -403,10 +407,11 @@ 681E3F43271FC7AD007BB547 /* RoomView.swift in Sources */, 6816B1B0272D9198005ADB85 /* ParticipantView.swift in Sources */, 68816CC527B4DCD500E24622 /* SecureStore.swift in Sources */, + 6816968E2AF96240008ED486 /* Participant+Helpers.swift in Sources */, 68B3854C271E780700711D5F /* LiveKitExample.swift in Sources */, 681E3F3F271FC795007BB547 /* Custom.swift in Sources */, 6847616427B44A1A001611BE /* Bundle.swift in Sources */, - 6816B1A8272D45DF005ADB85 /* ExampleObservableRoom.swift in Sources */, + 6816B1A8272D45DF005ADB85 /* ExampleRoomMessage.swift in Sources */, 6884B77C2750507400732D47 /* ScreenShareSourcePickerView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -422,10 +427,11 @@ 681E3F44271FC7AD007BB547 /* RoomView.swift in Sources */, 6816B1B1272D9198005ADB85 /* ParticipantView.swift in Sources */, 68816CC627B4DCD500E24622 /* SecureStore.swift in Sources */, + 6816968F2AF96240008ED486 /* Participant+Helpers.swift in Sources */, 68B3854D271E780700711D5F /* LiveKitExample.swift in Sources */, 681E3F40271FC795007BB547 /* Custom.swift in Sources */, 6847616527B44A1A001611BE /* Bundle.swift in Sources */, - 6816B1A9272D45DF005ADB85 /* ExampleObservableRoom.swift in Sources */, + 6816B1A9272D45DF005ADB85 /* ExampleRoomMessage.swift in Sources */, 6884B77D2750507400732D47 /* ScreenShareSourcePickerView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Shared/ParticipantView.swift b/Shared/ParticipantView.swift index a3f296a..5a02aa3 100644 --- a/Shared/ParticipantView.swift +++ b/Shared/ParticipantView.swift @@ -9,7 +9,6 @@ * * 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. diff --git a/Shared/ExampleObservableRoom.swift b/Shared/Support/ExampleRoomMessage.swift similarity index 68% rename from Shared/ExampleObservableRoom.swift rename to Shared/Support/ExampleRoomMessage.swift index eaedf01..b97bc50 100644 --- a/Shared/ExampleObservableRoom.swift +++ b/Shared/Support/ExampleRoomMessage.swift @@ -14,28 +14,6 @@ * limitations under the License. */ -import AVFoundation -import LiveKit -import SwiftUI - -import CoreImage.CIFilterBuiltins -import ReplayKit -import WebRTC - -public extension Participant { - var mainVideoPublication: TrackPublication? { - firstScreenSharePublication ?? firstCameraPublication - } - - var mainVideoTrack: VideoTrack? { - firstScreenShareVideoTrack ?? firstCameraVideoTrack - } - - var subVideoTrack: VideoTrack? { - firstScreenShareVideoTrack != nil ? firstCameraVideoTrack : nil - } -} - struct ExampleRoomMessage: Identifiable, Equatable, Hashable, Codable { // Identifiable protocol needs param named id var id: String { diff --git a/Shared/Support/Participant+Helpers.swift b/Shared/Support/Participant+Helpers.swift new file mode 100644 index 0000000..a7a1c9d --- /dev/null +++ b/Shared/Support/Participant+Helpers.swift @@ -0,0 +1,31 @@ +/* + * Copyright 2023 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 LiveKit + +public extension Participant { + var mainVideoPublication: TrackPublication? { + firstScreenSharePublication ?? firstCameraPublication + } + + var mainVideoTrack: VideoTrack? { + firstScreenShareVideoTrack ?? firstCameraVideoTrack + } + + var subVideoTrack: VideoTrack? { + firstScreenShareVideoTrack != nil ? firstCameraVideoTrack : nil + } +} From 9b75dfa0f92642f01a9aceb0276aa8f8b70173ad Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 9 Nov 2023 21:13:18 +0800 Subject: [PATCH 10/32] cancel connect button --- Shared/ConnectView.swift | 8 +++++++- Shared/Controllers/RoomContext.swift | 20 ++++++++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/Shared/ConnectView.swift b/Shared/ConnectView.swift index 6bdd66d..9047246 100644 --- a/Shared/ConnectView.swift +++ b/Shared/ConnectView.swift @@ -96,7 +96,13 @@ struct ConnectView: View { }.frame(maxWidth: 350) if case .connecting = room.connectionState { - ProgressView() + HStack(alignment: .center) { + ProgressView() + + LKButton(title: "Cancel") { + roomCtx.cancelConnect() + } + } } else { HStack(alignment: .center) { Spacer() diff --git a/Shared/Controllers/RoomContext.swift b/Shared/Controllers/RoomContext.swift index bfabbb9..f118f0d 100644 --- a/Shared/Controllers/RoomContext.swift +++ b/Shared/Controllers/RoomContext.swift @@ -81,6 +81,8 @@ final class RoomContext: ObservableObject { @Published var textFieldString: String = "" + var _connectTask: Task? + public init(store: ValueStore) { self.store = store room.add(delegate: self) @@ -108,6 +110,10 @@ final class RoomContext: ObservableObject { print("RoomContext.deinit") } + func cancelConnect() { + _connectTask?.cancel() + } + @MainActor func connect(entry: ConnectionHistory? = nil) async throws -> Room { if let entry { @@ -145,10 +151,16 @@ final class RoomContext: ObservableObject { e2eeOptions: e2eeOptions ) - try await room.connect(url: url, - token: token, - connectOptions: connectOptions, - roomOptions: roomOptions) + let connectTask = Task { + try await room.connect(url: url, + token: token, + connectOptions: connectOptions, + roomOptions: roomOptions) + } + + _connectTask = connectTask + try await connectTask.value + return room } From b2d3a0802097ad9851a2b5c09db6461be834294c Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 9 Nov 2023 21:23:36 +0800 Subject: [PATCH 11/32] use main actor instead of .main --- Shared/Controllers/AppContext.swift | 11 +++-------- Shared/Controllers/RoomContext.swift | 25 ++++++++++++------------- Shared/LiveKitExample.swift | 4 ++-- Shared/RoomView.swift | 14 ++++++++------ 4 files changed, 25 insertions(+), 29 deletions(-) diff --git a/Shared/Controllers/AppContext.swift b/Shared/Controllers/AppContext.swift index 66dbb40..c630cc8 100644 --- a/Shared/Controllers/AppContext.swift +++ b/Shared/Controllers/AppContext.swift @@ -19,12 +19,6 @@ import LiveKit import SwiftUI import WebRTC -extension ObservableObject where Self.ObjectWillChangePublisher == ObservableObjectPublisher { - func notify() { - DispatchQueue.main.async { self.objectWillChange.send() } - } -} - // This class contains the logic to control behavior of the whole app. final class AppContext: ObservableObject { private let store: ValueStore @@ -81,10 +75,11 @@ final class AppContext: ObservableObject { videoViewMirrored = store.value.videoViewMirrored connectionHistory = store.value.connectionHistory - AudioManager.shared.onDeviceUpdate = { audioManager in + AudioManager.shared.onDeviceUpdate = { [weak self] audioManager in + guard let self else { return } print("devices did update") // force UI update for outputDevice / inputDevice - DispatchQueue.main.async { + Task { @MainActor in self.outputDevice = audioManager.outputDevice self.inputDevice = audioManager.inputDevice } diff --git a/Shared/Controllers/RoomContext.swift b/Shared/Controllers/RoomContext.swift index f118f0d..60640f8 100644 --- a/Shared/Controllers/RoomContext.swift +++ b/Shared/Controllers/RoomContext.swift @@ -216,24 +216,23 @@ extension RoomContext: RoomDelegate { if case let .disconnected(reason) = connectionState, reason != .user { latestError = reason - DispatchQueue.main.async { - self.shouldShowDisconnectReason = true + + Task { @MainActor in + shouldShowDisconnectReason = true // Reset state - self.focusParticipant = nil - self.showMessagesView = false - self.textFieldString = "" - self.messages.removeAll() + focusParticipant = nil + showMessagesView = false + textFieldString = "" + messages.removeAll() // self.objectWillChange.send() } } } func room(_: Room, participantDidLeave participant: RemoteParticipant) { - DispatchQueue.main.async { + Task { @MainActor in // self.participants.removeValue(forKey: participant.sid) - if let focusParticipant = self.focusParticipant, - focusParticipant.sid == participant.sid - { + if let focusParticipant, focusParticipant.sid == participant.sid { self.focusParticipant = nil } } @@ -243,13 +242,13 @@ extension RoomContext: RoomDelegate { do { let roomMessage = try jsonDecoder.decode(ExampleRoomMessage.self, from: data) // Update UI from main queue - DispatchQueue.main.async { + Task { @MainActor in withAnimation { // Add messages to the @Published messages property // which will trigger the UI to update - self.messages.append(roomMessage) + messages.append(roomMessage) // Show the messages view when new messages arrive - self.showMessagesView = true + showMessagesView = true } } diff --git a/Shared/LiveKitExample.swift b/Shared/LiveKitExample.swift index 6d1732a..fbf4aec 100644 --- a/Shared/LiveKitExample.swift +++ b/Shared/LiveKitExample.swift @@ -191,8 +191,8 @@ struct LiveKitExample: App { func makeNSView(context _: Context) -> NSView { let view = NSView() - DispatchQueue.main.async { [weak view] in - if let window = view?.window { + Task { @MainActor in + if let window = view.window { callback(window) } } diff --git a/Shared/RoomView.swift b/Shared/RoomView.swift index 6bb2aff..a99b0e3 100644 --- a/Shared/RoomView.swift +++ b/Shared/RoomView.swift @@ -45,7 +45,7 @@ extension CIImage { deinit { // reset changed properties - DispatchQueue.main.async { [weak window] in + Task { @MainActor in window?.level = .normal } } @@ -60,16 +60,18 @@ extension CIImage { private var level: NSWindow.Level { get { window?.level ?? .normal } set { - DispatchQueue.main.async { - self.window?.level = newValue - self.objectWillChange.send() + Task { @MainActor in + window?.level = newValue + objectWillChange.send() } } } public func set(window: NSWindow?) { self.window = window - DispatchQueue.main.async { self.objectWillChange.send() } + Task { @MainActor in + objectWillChange.send() + } } } #endif @@ -564,7 +566,7 @@ struct RoomView: View { .onAppear { // Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { _ in - DispatchQueue.main.async { + Task { @MainActor in withAnimation { showConnectionTime = false } From 309843a54d6bd28e9ba7bd2a07daa9e31ff59090 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 9 Nov 2023 21:36:47 +0800 Subject: [PATCH 12/32] fix crash This reverts commit b2d3a0802097ad9851a2b5c09db6461be834294c. --- Shared/LiveKitExample.swift | 4 ++-- Shared/RoomView.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Shared/LiveKitExample.swift b/Shared/LiveKitExample.swift index fbf4aec..6d1732a 100644 --- a/Shared/LiveKitExample.swift +++ b/Shared/LiveKitExample.swift @@ -191,8 +191,8 @@ struct LiveKitExample: App { func makeNSView(context _: Context) -> NSView { let view = NSView() - Task { @MainActor in - if let window = view.window { + DispatchQueue.main.async { [weak view] in + if let window = view?.window { callback(window) } } diff --git a/Shared/RoomView.swift b/Shared/RoomView.swift index a99b0e3..d30a93c 100644 --- a/Shared/RoomView.swift +++ b/Shared/RoomView.swift @@ -45,7 +45,7 @@ extension CIImage { deinit { // reset changed properties - Task { @MainActor in + DispatchQueue.main.async { [weak window] in window?.level = .normal } } From 8b53ad982f45b91b482cf3ead839471d8d3eebc0 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 16 Nov 2023 08:59:01 +0800 Subject: [PATCH 13/32] fix ios compile --- .periphery.yml | 6 ++++++ Shared/RoomView.swift | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .periphery.yml diff --git a/.periphery.yml b/.periphery.yml new file mode 100644 index 0000000..d403777 --- /dev/null +++ b/.periphery.yml @@ -0,0 +1,6 @@ +retain_objc_accessible: true +schemes: +- Example (macOS Debug) +targets: +- LiveKitExample (macOS) +workspace: LiveKitExample-dev.xcworkspace diff --git a/Shared/RoomView.swift b/Shared/RoomView.swift index d30a93c..6c155e4 100644 --- a/Shared/RoomView.swift +++ b/Shared/RoomView.swift @@ -357,7 +357,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: { From e67d220f422e855cd8f0ea1d03956824640572c9 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Sun, 26 Nov 2023 19:59:56 +0800 Subject: [PATCH 14/32] show node id --- Shared/RoomView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Shared/RoomView.swift b/Shared/RoomView.swift index 6c155e4..d58dabc 100644 --- a/Shared/RoomView.swift +++ b/Shared/RoomView.swift @@ -194,7 +194,7 @@ struct RoomView: View { func content(geometry: GeometryProxy) -> some View { VStack { if showConnectionTime { - Text("Connected (\([room.serverRegion, "\(String(describing: room.connectStopwatch.total().rounded(to: 2)))s"].compactMap { $0 }.joined(separator: ", ")))") + Text("Connected (\([room.serverRegion, room.serverNodeId, "\(String(describing: room.connectStopwatch.total().rounded(to: 2)))s"].compactMap { $0 }.joined(separator: ", ")))") .multilineTextAlignment(.center) .foregroundColor(.white) .padding() From 94d1eeb193a30cff9f7bc86f888b222a72c0d21f Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Mon, 27 Nov 2023 20:35:51 +0900 Subject: [PATCH 15/32] PublishOptionsView --- LiveKitExample.xcodeproj/project.pbxproj | 6 ++ Shared/RoomView.swift | 93 +++++++++++++++--------- Shared/Views/PublishOptionsView.swift | 83 +++++++++++++++++++++ 3 files changed, 146 insertions(+), 36 deletions(-) create mode 100644 Shared/Views/PublishOptionsView.swift diff --git a/LiveKitExample.xcodeproj/project.pbxproj b/LiveKitExample.xcodeproj/project.pbxproj index 1eb6448..184c4a9 100644 --- a/LiveKitExample.xcodeproj/project.pbxproj +++ b/LiveKitExample.xcodeproj/project.pbxproj @@ -33,6 +33,8 @@ 6847616527B44A1A001611BE /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6847616327B44A1A001611BE /* Bundle.swift */; }; 6867533B27A65652003707B9 /* AppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6867533A27A65652003707B9 /* AppContext.swift */; }; 6867533C27A65652003707B9 /* AppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6867533A27A65652003707B9 /* AppContext.swift */; }; + 687230F82B14AE0A0098CCE6 /* PublishOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 687230F72B14AE0A0098CCE6 /* PublishOptionsView.swift */; }; + 687230F92B14AE0A0098CCE6 /* PublishOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 687230F72B14AE0A0098CCE6 /* PublishOptionsView.swift */; }; 68816CC127B4D6BC00E24622 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 68816CC027B4D6BC00E24622 /* KeychainAccess */; }; 68816CC327B4D94200E24622 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 68816CC227B4D94200E24622 /* KeychainAccess */; }; 68816CC527B4DCD500E24622 /* SecureStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68816CC427B4DCD500E24622 /* SecureStore.swift */; }; @@ -91,6 +93,7 @@ 6865EA2527513B4500FFAFC3 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 6865EA2D27513B6D00FFAFC3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 6867533A27A65652003707B9 /* AppContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppContext.swift; sourceTree = ""; }; + 687230F72B14AE0A0098CCE6 /* PublishOptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublishOptionsView.swift; sourceTree = ""; }; 68816CC427B4DCD500E24622 /* SecureStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureStore.swift; sourceTree = ""; }; 6884B77B2750507400732D47 /* ScreenShareSourcePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenShareSourcePickerView.swift; sourceTree = ""; }; 68B3853C271E780600711D5F /* LiveKitExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveKitExample.swift; sourceTree = ""; }; @@ -194,6 +197,7 @@ isa = PBXGroup; children = ( 6884B77B2750507400732D47 /* ScreenShareSourcePickerView.swift */, + 687230F72B14AE0A0098CCE6 /* PublishOptionsView.swift */, ); path = Views; sourceTree = ""; @@ -411,6 +415,7 @@ 68B3854C271E780700711D5F /* LiveKitExample.swift in Sources */, 681E3F3F271FC795007BB547 /* Custom.swift in Sources */, 6847616427B44A1A001611BE /* Bundle.swift in Sources */, + 687230F82B14AE0A0098CCE6 /* PublishOptionsView.swift in Sources */, 6816B1A8272D45DF005ADB85 /* ExampleRoomMessage.swift in Sources */, 6884B77C2750507400732D47 /* ScreenShareSourcePickerView.swift in Sources */, ); @@ -431,6 +436,7 @@ 68B3854D271E780700711D5F /* LiveKitExample.swift in Sources */, 681E3F40271FC795007BB547 /* Custom.swift in Sources */, 6847616527B44A1A001611BE /* Bundle.swift in Sources */, + 687230F92B14AE0A0098CCE6 /* PublishOptionsView.swift in Sources */, 6816B1A9272D45DF005ADB85 /* ExampleRoomMessage.swift in Sources */, 6884B77D2750507400732D47 /* ScreenShareSourcePickerView.swift in Sources */, ); diff --git a/Shared/RoomView.swift b/Shared/RoomView.swift index d58dabc..795d8ab 100644 --- a/Shared/RoomView.swift +++ b/Shared/RoomView.swift @@ -86,6 +86,10 @@ struct RoomView: View { @State var isScreenSharePublishingBusy = false @State private var screenPickerPresented = false + @State private var publishOptionsPickerPresented = false + + @State private var cameraPublishOptions = VideoPublishOptions() + #if os(macOS) @ObservedObject private var windowAccess = WindowAccess() #endif @@ -294,47 +298,64 @@ struct RoomView: View { let isMicrophoneEnabled = room.localParticipant.isMicrophoneEnabled() let isScreenShareEnabled = room.localParticipant.isScreenShareEnabled() - if isCameraEnabled, CameraCapturer.canSwitchPosition() { - Menu { - Button("Switch position") { - Task { - isCameraPublishingBusy = true - defer { Task { @MainActor in isCameraPublishingBusy = false } } - if let track = room.localParticipant.firstCameraVideoTrack as? LocalVideoTrack, - let cameraCapturer = track.capturer as? CameraCapturer - { - try await cameraCapturer.switchCameraPosition() + Group { + if isCameraEnabled, CameraCapturer.canSwitchPosition() { + Menu { + Button("Switch position") { + Task { + isCameraPublishingBusy = true + defer { Task { @MainActor in isCameraPublishingBusy = false } } + if let track = room.localParticipant.firstCameraVideoTrack as? LocalVideoTrack, + let cameraCapturer = track.capturer as? CameraCapturer + { + try await cameraCapturer.switchCameraPosition() + } } } - } - Button("Disable") { - Task { - isCameraPublishingBusy = true - defer { Task { @MainActor in isCameraPublishingBusy = false } } - try await room.localParticipant.setCamera(enabled: !isCameraEnabled) + Button("Disable") { + Task { + isCameraPublishingBusy = true + defer { Task { @MainActor in isCameraPublishingBusy = false } } + try await room.localParticipant.setCamera(enabled: !isCameraEnabled) + } } + } label: { + Image(systemSymbol: .videoFill) + .renderingMode(.original) } - } label: { - Image(systemSymbol: .videoFill) - .renderingMode(.original) + // disable while publishing/un-publishing + .disabled(isCameraPublishingBusy) + } else { + // Toggle camera enabled + Button(action: { + if isCameraEnabled { + Task { + isCameraPublishingBusy = true + defer { Task { @MainActor in isCameraPublishingBusy = false } } + try await room.localParticipant.setCamera(enabled: false) + } + } else { + publishOptionsPickerPresented = true + } + }, + label: { + Image(systemSymbol: .videoFill) + .renderingMode(isCameraEnabled ? .original : .template) + }) + // disable while publishing/un-publishing + .disabled(isCameraPublishingBusy) } - // disable while publishing/un-publishing - .disabled(isCameraPublishingBusy) - } else { - // Toggle camera enabled - Button(action: { - Task { - isCameraPublishingBusy = true - defer { Task { @MainActor in isCameraPublishingBusy = false } } - try await room.localParticipant.setCamera(enabled: !isCameraEnabled) - } - }, - label: { - Image(systemSymbol: .videoFill) - .renderingMode(isCameraEnabled ? .original : .template) - }) - // disable while publishing/un-publishing - .disabled(isCameraPublishingBusy) + }.popover(isPresented: $publishOptionsPickerPresented) { + PublishOptionsView(publishOptions: cameraPublishOptions) { pickerResult in + publishOptionsPickerPresented = false + isCameraPublishingBusy = true + cameraPublishOptions = pickerResult + Task { + defer { Task { @MainActor in isCameraPublishingBusy = false } } + try await room.localParticipant.setCamera(enabled: true, publishOptions: pickerResult) + } + } + .padding() } // Toggle microphone enabled diff --git a/Shared/Views/PublishOptionsView.swift b/Shared/Views/PublishOptionsView.swift new file mode 100644 index 0000000..5ecbfb1 --- /dev/null +++ b/Shared/Views/PublishOptionsView.swift @@ -0,0 +1,83 @@ +/* + * Copyright 2023 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 +import LiveKit +import SwiftUI + +struct PublishOptionsView: View { + typealias OnPublish = (_ publishOptions: VideoPublishOptions) -> Void + + @State private var preferredVideoCodec: VideoCodec? + @State private var preferredBackupVideoCodec: VideoCodec? + + private let providedPublishOptions: VideoPublishOptions + private let onPublish: OnPublish + + init(publishOptions: VideoPublishOptions, _ onPublish: @escaping OnPublish) { + providedPublishOptions = publishOptions + self.onPublish = onPublish + + preferredVideoCodec = publishOptions.preferredCodec + preferredBackupVideoCodec = publishOptions.preferredBackupCodec + } + + var body: some View { + VStack(alignment: .center, spacing: 10) { + Text("Publish options") + .fontWeight(.bold) + + Picker("Codec", selection: $preferredVideoCodec) { + Text("Auto") + ForEach(VideoCodec.all) { + Text($0.id.uppercased()) + } + }.onChange(of: preferredVideoCodec) { newValue in + if newValue == .av1 { + preferredBackupVideoCodec = .vp8 + } else { + preferredBackupVideoCodec = nil + } + } + + if preferredVideoCodec != nil { + Picker("Backup Codec", selection: $preferredBackupVideoCodec) { + Text("Off") + ForEach(VideoCodec.all.filter { $0 != .av1 && $0 != preferredVideoCodec }, id: \.self) { + Text($0.id.uppercased()) + } + } + } + + Button("Publish") { + let result = VideoPublishOptions( + name: providedPublishOptions.name, + encoding: providedPublishOptions.encoding, + screenShareEncoding: providedPublishOptions.screenShareEncoding, + simulcast: providedPublishOptions.simulcast, + simulcastLayers: providedPublishOptions.simulcastLayers, + screenShareSimulcastLayers: providedPublishOptions.screenShareSimulcastLayers, + preferredCodec: preferredVideoCodec, + preferredBackupCodec: preferredBackupVideoCodec + // backupEncoding: providedPublishOptions.backupEncoding + ) + + onPublish(result) + } + .keyboardShortcut(.defaultAction) + } + } +} From a8e65f56caca8c1a7d1146e714a3d1423f65902a Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Tue, 28 Nov 2023 08:22:04 +0900 Subject: [PATCH 16/32] fix publish view --- Shared/Views/PublishOptionsView.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Shared/Views/PublishOptionsView.swift b/Shared/Views/PublishOptionsView.swift index 5ecbfb1..9070d68 100644 --- a/Shared/Views/PublishOptionsView.swift +++ b/Shared/Views/PublishOptionsView.swift @@ -41,9 +41,9 @@ struct PublishOptionsView: View { .fontWeight(.bold) Picker("Codec", selection: $preferredVideoCodec) { - Text("Auto") + Text("Auto").tag(nil as VideoCodec?) ForEach(VideoCodec.all) { - Text($0.id.uppercased()) + Text($0.id.uppercased()).tag($0 as VideoCodec?) } }.onChange(of: preferredVideoCodec) { newValue in if newValue == .av1 { @@ -55,9 +55,9 @@ struct PublishOptionsView: View { if preferredVideoCodec != nil { Picker("Backup Codec", selection: $preferredBackupVideoCodec) { - Text("Off") - ForEach(VideoCodec.all.filter { $0 != .av1 && $0 != preferredVideoCodec }, id: \.self) { - Text($0.id.uppercased()) + Text("Off").tag(nil as VideoCodec?) + ForEach(VideoCodec.all.filter { $0 != preferredVideoCodec }) { + Text($0.id.uppercased()).tag($0 as VideoCodec?) } } } From 3cb087db6fe95de1d62929c2b59449dc68a49f53 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Tue, 28 Nov 2023 08:22:14 +0900 Subject: [PATCH 17/32] stats --- Shared/Controllers/RoomContext.swift | 3 +- Shared/ParticipantView.swift | 53 ++++++++++++++++++---------- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/Shared/Controllers/RoomContext.swift b/Shared/Controllers/RoomContext.swift index 60640f8..28ca9ff 100644 --- a/Shared/Controllers/RoomContext.swift +++ b/Shared/Controllers/RoomContext.swift @@ -148,7 +148,8 @@ final class RoomContext: ObservableObject { ), adaptiveStream: adaptiveStream, dynacast: dynacast, - e2eeOptions: e2eeOptions + e2eeOptions: e2eeOptions, + reportTrackStatistics: true ) let connectTask = Task { diff --git a/Shared/ParticipantView.swift b/Shared/ParticipantView.swift index 5a02aa3..7510b66 100644 --- a/Shared/ParticipantView.swift +++ b/Shared/ParticipantView.swift @@ -298,25 +298,40 @@ struct StatsView: View { } if let trackStats = viewModel.statistics { - // if trackStats.bpsSent != 0 { - // HStack(spacing: 3) { - // if let codecName = trackStats.codecName { - // Text(codecName.uppercased()).fontWeight(.bold) - // } - // Image(systemSymbol: .arrowUpCircle) - // Text(trackStats.formattedBpsSent()) - // } - // } - // - // if trackStats.bpsReceived != 0 { - // HStack(spacing: 3) { - // if let codecName = trackStats.codecName { - // Text(codecName.uppercased()).fontWeight(.bold) - // } - // Image(systemSymbol: .arrowDownCircle) - // Text(trackStats.formattedBpsReceived()) - // } - // } + ForEach(trackStats.outboundRtpStream.sortedByRidIndex()) { stream in + + HStack(spacing: 3) { + Image(systemSymbol: .arrowUp) + + if let codec = trackStats.codec.first(where: { $0.id == stream.codecId }) { + Text(codec.mimeType ?? "?") + } + + if let rid = stream.rid, !rid.isEmpty { + Text(rid.uppercased()) + } + + Text(stream.formattedBps()) + + if stream.qualityLimitationReason != QualityLimitationReason.none { + Image(systemSymbol: .exclamationmarkTriangleFill) + Text(stream.qualityLimitationReason!.rawValue.capitalized) + } + } + } + + ForEach(trackStats.inboundRtpStream) { stream in + + HStack(spacing: 3) { + Image(systemSymbol: .arrowDown) + + if let codec = trackStats.codec.first(where: { $0.id == stream.codecId }) { + Text(codec.mimeType ?? "?") + } + + Text(stream.formattedBps()) + } + } } } .font(.system(size: 10)) From c64fb620a8d19973e33896f6865dcbfd9f48b1a3 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Wed, 29 Nov 2023 12:45:41 +0900 Subject: [PATCH 18/32] fix: PublishOptionsView --- Shared/Views/PublishOptionsView.swift | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/Shared/Views/PublishOptionsView.swift b/Shared/Views/PublishOptionsView.swift index 9070d68..d94bb73 100644 --- a/Shared/Views/PublishOptionsView.swift +++ b/Shared/Views/PublishOptionsView.swift @@ -21,6 +21,7 @@ import SwiftUI struct PublishOptionsView: View { typealias OnPublish = (_ publishOptions: VideoPublishOptions) -> Void + @State private var simulcast: Bool = true @State private var preferredVideoCodec: VideoCodec? @State private var preferredBackupVideoCodec: VideoCodec? @@ -31,6 +32,7 @@ struct PublishOptionsView: View { providedPublishOptions = publishOptions self.onPublish = onPublish + simulcast = publishOptions.simulcast preferredVideoCodec = publishOptions.preferredCodec preferredBackupVideoCodec = publishOptions.preferredBackupCodec } @@ -46,33 +48,36 @@ struct PublishOptionsView: View { Text($0.id.uppercased()).tag($0 as VideoCodec?) } }.onChange(of: preferredVideoCodec) { newValue in - if newValue == .av1 { + if newValue?.isSVC ?? false { preferredBackupVideoCodec = .vp8 + simulcast = false } else { preferredBackupVideoCodec = nil + simulcast = true } } - if preferredVideoCodec != nil { - Picker("Backup Codec", selection: $preferredBackupVideoCodec) { - Text("Off").tag(nil as VideoCodec?) - ForEach(VideoCodec.all.filter { $0 != preferredVideoCodec }) { - Text($0.id.uppercased()).tag($0 as VideoCodec?) - } + Picker("Backup Codec", selection: $preferredBackupVideoCodec) { + Text("Off").tag(nil as VideoCodec?) + ForEach(VideoCodec.allBackup.filter { $0 != preferredVideoCodec }) { + Text($0.id.uppercased()).tag($0 as VideoCodec?) } - } + }.disabled(!(preferredVideoCodec?.isSVC ?? false)) + + Toggle(isOn: $simulcast, label: { + Text("Simulcast") + }) Button("Publish") { let result = VideoPublishOptions( name: providedPublishOptions.name, encoding: providedPublishOptions.encoding, screenShareEncoding: providedPublishOptions.screenShareEncoding, - simulcast: providedPublishOptions.simulcast, + simulcast: simulcast, simulcastLayers: providedPublishOptions.simulcastLayers, screenShareSimulcastLayers: providedPublishOptions.screenShareSimulcastLayers, preferredCodec: preferredVideoCodec, preferredBackupCodec: preferredBackupVideoCodec - // backupEncoding: providedPublishOptions.backupEncoding ) onPublish(result) From 39a7a8f3e4b4172d78f2836c9ba8675a0cb8db19 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Tue, 5 Dec 2023 08:36:22 +0900 Subject: [PATCH 19/32] default to simulcast on --- Shared/Controllers/RoomContext.swift | 6 +++--- Shared/Views/PublishOptionsView.swift | 2 -- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Shared/Controllers/RoomContext.swift b/Shared/Controllers/RoomContext.swift index 28ca9ff..b3dbb68 100644 --- a/Shared/Controllers/RoomContext.swift +++ b/Shared/Controllers/RoomContext.swift @@ -146,9 +146,9 @@ final class RoomContext: ObservableObject { defaultVideoPublishOptions: VideoPublishOptions( simulcast: publish ? false : simulcast ), - adaptiveStream: adaptiveStream, - dynacast: dynacast, - e2eeOptions: e2eeOptions, + adaptiveStream: true, + dynacast: true, + // e2eeOptions: e2eeOptions, reportTrackStatistics: true ) diff --git a/Shared/Views/PublishOptionsView.swift b/Shared/Views/PublishOptionsView.swift index d94bb73..b0025f1 100644 --- a/Shared/Views/PublishOptionsView.swift +++ b/Shared/Views/PublishOptionsView.swift @@ -50,10 +50,8 @@ struct PublishOptionsView: View { }.onChange(of: preferredVideoCodec) { newValue in if newValue?.isSVC ?? false { preferredBackupVideoCodec = .vp8 - simulcast = false } else { preferredBackupVideoCodec = nil - simulcast = true } } From 16c1db3dfdab24bd3844479157b4b13607ddd1c8 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Wed, 6 Dec 2023 15:55:19 +0900 Subject: [PATCH 20/32] Update ParticipantView.swift --- Shared/ParticipantView.swift | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/Shared/ParticipantView.swift b/Shared/ParticipantView.swift index 7510b66..056f3f2 100644 --- a/Shared/ParticipantView.swift +++ b/Shared/ParticipantView.swift @@ -297,7 +297,8 @@ struct StatsView: View { Text("Unknown").fontWeight(.bold) } - if let trackStats = viewModel.statistics { + // if let trackStats = viewModel.statistics { + ForEach(viewModel.allStatisticts, id: \.self) { trackStats in ForEach(trackStats.outboundRtpStream.sortedByRidIndex()) { stream in HStack(spacing: 3) { @@ -319,7 +320,6 @@ struct StatsView: View { } } } - ForEach(trackStats.inboundRtpStream) { stream in HStack(spacing: 3) { @@ -348,12 +348,23 @@ extension StatsView { private let track: Track @Published var dimensions: Dimensions? @Published var statistics: TrackStatistics? + @Published var simulcastStatistics: [VideoCodec: TrackStatistics] + + var allStatisticts: [TrackStatistics] { + var result: [TrackStatistics] = [] + if let statistics { + result.append(statistics) + } + result.append(contentsOf: simulcastStatistics.values) + return result + } init(track: Track) { self.track = track dimensions = track.dimensions statistics = track.statistics + simulcastStatistics = track.simulcastStatistics track.add(delegate: self) } @@ -364,9 +375,10 @@ extension StatsView { } } - func track(_: Track, didUpdateStatistics statistics: TrackStatistics) { + func track(_: Track, didUpdateStatistics statistics: TrackStatistics, simulcastStatistics: [VideoCodec: TrackStatistics]) { Task.detached { @MainActor in self.statistics = statistics + self.simulcastStatistics = simulcastStatistics } } } From d545928946d0deec61b8cb05cfdf8960b9061eb8 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Wed, 6 Dec 2023 16:53:18 +0900 Subject: [PATCH 21/32] Update RoomView.swift --- Shared/RoomView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Shared/RoomView.swift b/Shared/RoomView.swift index 795d8ab..adf5e8c 100644 --- a/Shared/RoomView.swift +++ b/Shared/RoomView.swift @@ -272,7 +272,7 @@ struct RoomView: View { .fontWeight(.bold) } - Text(room.localParticipant.identity ?? "") + Text(room.localParticipant.identity) #endif From 5b8173c31cac4a52fefddea707def76d62c4d6da Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 7 Dec 2023 00:30:47 +0900 Subject: [PATCH 22/32] fix audio publish crash --- LiveKitExample.xcodeproj/project.pbxproj | 8 ++++---- Shared/ParticipantView.swift | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/LiveKitExample.xcodeproj/project.pbxproj b/LiveKitExample.xcodeproj/project.pbxproj index 184c4a9..39df3fd 100644 --- a/LiveKitExample.xcodeproj/project.pbxproj +++ b/LiveKitExample.xcodeproj/project.pbxproj @@ -635,7 +635,7 @@ CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/iOS/iOS.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 30; + CURRENT_PROJECT_VERSION = 32; DEVELOPMENT_TEAM = 76TVFCUKK7; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = iOS/Info.plist; @@ -650,7 +650,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1.1; + MARKETING_VERSION = 1.2.1; PRODUCT_BUNDLE_IDENTIFIER = io.livekit.example.SwiftSDK.1; PRODUCT_NAME = LiveKitExample; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -669,7 +669,7 @@ CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/iOS/iOS.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 30; + CURRENT_PROJECT_VERSION = 32; DEVELOPMENT_TEAM = 76TVFCUKK7; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = iOS/Info.plist; @@ -684,7 +684,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1.1; + MARKETING_VERSION = 1.2.1; PRODUCT_BUNDLE_IDENTIFIER = io.livekit.example.SwiftSDK.1; PRODUCT_NAME = LiveKitExample; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Shared/ParticipantView.swift b/Shared/ParticipantView.swift index 056f3f2..95a547f 100644 --- a/Shared/ParticipantView.swift +++ b/Shared/ParticipantView.swift @@ -122,7 +122,7 @@ struct ParticipantView: View { // Bottom user info bar HStack { - Text(participant.identity ?? "") // (\(participant.publish ?? "-")) + Text("\(participant.identity)(\(participant.sid))") .lineLimit(1) .truncationMode(.tail) @@ -314,9 +314,9 @@ struct StatsView: View { Text(stream.formattedBps()) - if stream.qualityLimitationReason != QualityLimitationReason.none { + if let reason = stream.qualityLimitationReason, reason != QualityLimitationReason.none { Image(systemSymbol: .exclamationmarkTriangleFill) - Text(stream.qualityLimitationReason!.rawValue.capitalized) + Text(reason.rawValue.capitalized) } } } From 36a694d387c55569d6738a2a2f7ce5286338f351 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Wed, 20 Dec 2023 15:28:28 +0900 Subject: [PATCH 23/32] update isMuted state etc --- .../xcshareddata/swiftpm/Package.resolved | 10 +++++----- LiveKitExample.xcodeproj/project.pbxproj | 10 ++++------ Shared/ConnectView.swift | 4 +--- Shared/Controllers/AppContext.swift | 2 +- Shared/Controllers/RoomContext.swift | 18 ++++++++++-------- Shared/LiveKitExample.swift | 2 +- Shared/ParticipantView.swift | 10 +++++----- Shared/RoomView.swift | 4 ++-- 8 files changed, 29 insertions(+), 31 deletions(-) diff --git a/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved b/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved index a039db4..e1f0537 100644 --- a/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -33,17 +33,17 @@ "repositoryURL": "https://github.com/apple/swift-protobuf.git", "state": { "branch": null, - "revision": "07f7f26ded8df9645c072f220378879c4642e063", - "version": "1.25.1" + "revision": "65e8f29b2d63c4e38e736b25c27b83e012159be8", + "version": "1.25.2" } }, { "package": "WebRTC", - "repositoryURL": "https://github.com/livekit/webrtc-xcframework-static.git", + "repositoryURL": "https://github.com/livekit/webrtc-xcframework.git", "state": { "branch": null, - "revision": "05fb98380b8c2c43041050be6170ffa3dbc64174", - "version": "114.5735.9" + "revision": "da80ea5be0a2b92ca805ab7ee9ad191f6d938a5f", + "version": "114.5735.10" } } ] diff --git a/LiveKitExample.xcodeproj/project.pbxproj b/LiveKitExample.xcodeproj/project.pbxproj index 39df3fd..6e1850c 100644 --- a/LiveKitExample.xcodeproj/project.pbxproj +++ b/LiveKitExample.xcodeproj/project.pbxproj @@ -561,7 +561,6 @@ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; - OTHER_LDFLAGS = "-ObjC"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -619,7 +618,6 @@ GCC_WARN_UNUSED_VARIABLE = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; - OTHER_LDFLAGS = "-ObjC"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; @@ -635,7 +633,7 @@ CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/iOS/iOS.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 32; + CURRENT_PROJECT_VERSION = 33; DEVELOPMENT_TEAM = 76TVFCUKK7; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = iOS/Info.plist; @@ -650,7 +648,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.1; + MARKETING_VERSION = 1.2.2; PRODUCT_BUNDLE_IDENTIFIER = io.livekit.example.SwiftSDK.1; PRODUCT_NAME = LiveKitExample; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -669,7 +667,7 @@ CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/iOS/iOS.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 32; + CURRENT_PROJECT_VERSION = 33; DEVELOPMENT_TEAM = 76TVFCUKK7; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = iOS/Info.plist; @@ -684,7 +682,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.1; + MARKETING_VERSION = 1.2.2; PRODUCT_BUNDLE_IDENTIFIER = io.livekit.example.SwiftSDK.1; PRODUCT_NAME = LiveKitExample; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Shared/ConnectView.swift b/Shared/ConnectView.swift index 9047246..65a6b35 100644 --- a/Shared/ConnectView.swift +++ b/Shared/ConnectView.swift @@ -168,9 +168,7 @@ struct ConnectView: View { #endif .alert(isPresented: $roomCtx.shouldShowDisconnectReason) { Alert(title: Text("Disconnected"), - message: Text("Reason: " + (roomCtx.latestError != nil - ? String(describing: roomCtx.latestError!) - : "Unknown"))) + message: Text("Reason: " + String(describing: roomCtx.latestError))) } } } diff --git a/Shared/Controllers/AppContext.swift b/Shared/Controllers/AppContext.swift index c630cc8..6f8f8af 100644 --- a/Shared/Controllers/AppContext.swift +++ b/Shared/Controllers/AppContext.swift @@ -62,7 +62,7 @@ final class AppContext: ObservableObject { } @Published var preferSpeakerOutput: Bool = true { - didSet { AudioManager.shared.preferSpeakerOutput = preferSpeakerOutput } + didSet { AudioManager.shared.isSpeakerOutputPreferred = preferSpeakerOutput } } public init(store: ValueStore) { diff --git a/Shared/Controllers/RoomContext.swift b/Shared/Controllers/RoomContext.swift index b3dbb68..e83b92e 100644 --- a/Shared/Controllers/RoomContext.swift +++ b/Shared/Controllers/RoomContext.swift @@ -28,7 +28,7 @@ final class RoomContext: ObservableObject { // Used to show connection error dialog // private var didClose: Bool = false @Published var shouldShowDisconnectReason: Bool = false - public var latestError: DisconnectReason? + public var latestError: LiveKitError? public let room = Room() @@ -149,7 +149,7 @@ final class RoomContext: ObservableObject { adaptiveStream: true, dynacast: true, // e2eeOptions: e2eeOptions, - reportTrackStatistics: true + reportRemoteTrackStatistics: true ) let connectTask = Task { @@ -194,13 +194,13 @@ final class RoomContext: ObservableObject { weak var screenShareTrack: LocalTrackPublication? @available(macOS 12.3, *) - func setScreenShareMacOS(enabled: Bool, screenShareSource: MacOSScreenCaptureSource? = nil) async throws { - if enabled, let screenShareSource { + func setScreenShareMacOS(isEnabled: Bool, screenShareSource: MacOSScreenCaptureSource? = nil) async throws { + if isEnabled, let screenShareSource { let track = LocalVideoTrack.createMacOSScreenShareTrack(source: screenShareSource) screenShareTrack = try await room.localParticipant.publish(videoTrack: track) } - if !enabled, let screenShareTrack { + if !isEnabled, let screenShareTrack { try await room.localParticipant.unpublish(publication: screenShareTrack) } } @@ -212,11 +212,13 @@ extension RoomContext: RoomDelegate { print("Did update e2eeState = [\(e2eeState.toString())] for publication \(publication.sid)") } - func room(_: Room, didUpdate connectionState: ConnectionState, oldValue: ConnectionState) { + func room(_ room: Room, didUpdate connectionState: ConnectionState, oldValue: ConnectionState) { print("Did update connectionState \(oldValue) -> \(connectionState)") - if case let .disconnected(reason) = connectionState, reason != .user { - latestError = reason + if case .disconnected = connectionState, + let error = room.disconnectError, + error.type != .cancelled { + latestError = room.disconnectError Task { @MainActor in shouldShowDisconnectReason = true diff --git a/Shared/LiveKitExample.swift b/Shared/LiveKitExample.swift index 6d1732a..2776786 100644 --- a/Shared/LiveKitExample.swift +++ b/Shared/LiveKitExample.swift @@ -29,7 +29,7 @@ struct RoomSwitchView: View { @EnvironmentObject var room: Room var shouldShowRoomView: Bool { - room.connectionState.isConnected || room.connectionState.isReconnecting + room.connectionState == .connected || room.connectionState == .reconnecting } func computeTitle() -> String { diff --git a/Shared/ParticipantView.swift b/Shared/ParticipantView.swift index 95a547f..04abe5d 100644 --- a/Shared/ParticipantView.swift +++ b/Shared/ParticipantView.swift @@ -51,7 +51,7 @@ struct ParticipantView: View { // VideoView for the Participant if let publication = participant.mainVideoPublication, - !publication.muted, + !publication.isMuted, let track = publication.track as? VideoTrack, appCtx.videoViewVisible { @@ -84,14 +84,14 @@ struct ParticipantView: View { VStack(alignment: .leading, spacing: 5) { // Video stats if let publication = participant.mainVideoPublication, - !publication.muted, + !publication.isMuted, let track = publication.track as? VideoTrack { StatsView(track: track) } // Audio stats if let publication = participant.firstAudioPublication, - !publication.muted, + !publication.isMuted, let track = publication.track as? AudioTrack { StatsView(track: track) @@ -127,7 +127,7 @@ struct ParticipantView: View { .truncationMode(.tail) if let publication = participant.mainVideoPublication, - !publication.muted + !publication.isMuted { // is remote if let remotePub = publication as? RemoteTrackPublication { @@ -178,7 +178,7 @@ struct ParticipantView: View { } if let publication = participant.firstAudioPublication, - !publication.muted + !publication.isMuted { // is remote if let remotePub = publication as? RemoteTrackPublication { diff --git a/Shared/RoomView.swift b/Shared/RoomView.swift index adf5e8c..ae44a65 100644 --- a/Shared/RoomView.swift +++ b/Shared/RoomView.swift @@ -395,7 +395,7 @@ struct RoomView: View { Task { isScreenSharePublishingBusy = true defer { Task { @MainActor in isScreenSharePublishingBusy = false } } - try await roomCtx.setScreenShareMacOS(enabled: false) + try await roomCtx.setScreenShareMacOS(isEnabled: false) } } else { screenPickerPresented = true @@ -412,7 +412,7 @@ struct RoomView: View { Task { isScreenSharePublishingBusy = true defer { Task { @MainActor in isScreenSharePublishingBusy = false } } - try await roomCtx.setScreenShareMacOS(enabled: true, screenShareSource: source) + try await roomCtx.setScreenShareMacOS(isEnabled: true, screenShareSource: source) } screenPickerPresented = false }.padding() From 135eb0dd1e51e51f1a0222e2893e7b7f8509b90a Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Mon, 1 Jan 2024 23:50:08 +0900 Subject: [PATCH 24/32] trigger re-connect --- Shared/ConnectView.swift | 2 +- Shared/Controllers/AppContext.swift | 2 +- Shared/Controllers/RoomContext.swift | 5 +++-- Shared/LiveKitExample.swift | 2 +- Shared/ParticipantView.swift | 2 +- Shared/RoomView.swift | 13 ++++++++++++- 6 files changed, 19 insertions(+), 7 deletions(-) diff --git a/Shared/ConnectView.swift b/Shared/ConnectView.swift index 65a6b35..489b73b 100644 --- a/Shared/ConnectView.swift +++ b/Shared/ConnectView.swift @@ -1,5 +1,5 @@ /* - * Copyright 2023 LiveKit + * Copyright 2024 LiveKit * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Shared/Controllers/AppContext.swift b/Shared/Controllers/AppContext.swift index 6f8f8af..530852f 100644 --- a/Shared/Controllers/AppContext.swift +++ b/Shared/Controllers/AppContext.swift @@ -1,5 +1,5 @@ /* - * Copyright 2023 LiveKit + * Copyright 2024 LiveKit * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Shared/Controllers/RoomContext.swift b/Shared/Controllers/RoomContext.swift index e83b92e..2af9486 100644 --- a/Shared/Controllers/RoomContext.swift +++ b/Shared/Controllers/RoomContext.swift @@ -1,5 +1,5 @@ /* - * Copyright 2023 LiveKit + * Copyright 2024 LiveKit * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -217,7 +217,8 @@ extension RoomContext: RoomDelegate { if case .disconnected = connectionState, let error = room.disconnectError, - error.type != .cancelled { + error.type != .cancelled + { latestError = room.disconnectError Task { @MainActor in diff --git a/Shared/LiveKitExample.swift b/Shared/LiveKitExample.swift index 2776786..4f9b5b3 100644 --- a/Shared/LiveKitExample.swift +++ b/Shared/LiveKitExample.swift @@ -1,5 +1,5 @@ /* - * Copyright 2023 LiveKit + * Copyright 2024 LiveKit * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Shared/ParticipantView.swift b/Shared/ParticipantView.swift index 04abe5d..0ca3f96 100644 --- a/Shared/ParticipantView.swift +++ b/Shared/ParticipantView.swift @@ -1,5 +1,5 @@ /* - * Copyright 2023 LiveKit + * Copyright 2024 LiveKit * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Shared/RoomView.swift b/Shared/RoomView.swift index ae44a65..191995c 100644 --- a/Shared/RoomView.swift +++ b/Shared/RoomView.swift @@ -1,5 +1,5 @@ /* - * Copyright 2023 LiveKit + * Copyright 2024 LiveKit * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -293,6 +293,17 @@ struct RoomView: View { Spacer() + Group { + Button { + Task { + try await room.debug_triggerReconnect(reason: .transport) + } + } label: { + Image(systemSymbol: .repeat) + .renderingMode(.original) + } + } + Group { let isCameraEnabled = room.localParticipant.isCameraEnabled() let isMicrophoneEnabled = room.localParticipant.isMicrophoneEnabled() From 613c64c75b5c3fc0a1bbd718f565030c20e46d6d Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 18 Jan 2024 14:24:47 +0900 Subject: [PATCH 25/32] update debug scheme --- .../xcshareddata/swiftpm/Package.resolved | 18 ++++++++++++++++++ .../xcschemes/Example (macOS Debug).xcscheme | 17 ++++++++++++++++- Shared/Controllers/RoomContext.swift | 9 +++++---- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved b/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved index e1f0537..0e7cf88 100644 --- a/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -19,6 +19,24 @@ "version": "4.1.1" } }, + { + "package": "SwiftDocCPlugin", + "repositoryURL": "https://github.com/apple/swift-docc-plugin", + "state": { + "branch": null, + "revision": "26ac5758409154cc448d7ab82389c520fa8a8247", + "version": "1.3.0" + } + }, + { + "package": "SymbolKit", + "repositoryURL": "https://github.com/apple/swift-docc-symbolkit", + "state": { + "branch": null, + "revision": "b45d1f2ed151d057b54504d653e0da5552844e34", + "version": "1.0.0" + } + }, { "package": "swift-log", "repositoryURL": "https://github.com/apple/swift-log.git", diff --git a/LiveKitExample.xcodeproj/xcshareddata/xcschemes/Example (macOS Debug).xcscheme b/LiveKitExample.xcodeproj/xcshareddata/xcschemes/Example (macOS Debug).xcscheme index 6be4bd3..3f4a4c3 100644 --- a/LiveKitExample.xcodeproj/xcshareddata/xcschemes/Example (macOS Debug).xcscheme +++ b/LiveKitExample.xcodeproj/xcshareddata/xcschemes/Example (macOS Debug).xcscheme @@ -35,12 +35,15 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" enableASanStackUseAfterReturn = "YES" + disablePerformanceAntipatternChecker = "YES" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" - allowLocationSimulation = "YES"> + allowLocationSimulation = "YES" + queueDebuggingEnabled = "No" + memoryGraphOnResourceException = "Yes"> + + + + + + \(connectionState)") if case .disconnected = connectionState, @@ -233,7 +234,7 @@ extension RoomContext: RoomDelegate { } } - func room(_: Room, participantDidLeave participant: RemoteParticipant) { + func room(_: Room, participantDidDisconnect participant: RemoteParticipant) { Task { @MainActor in // self.participants.removeValue(forKey: participant.sid) if let focusParticipant, focusParticipant.sid == participant.sid { @@ -242,7 +243,7 @@ extension RoomContext: RoomDelegate { } } - func room(_: Room, participant _: RemoteParticipant?, didReceiveData data: Data, topic _: String) { + func room(_: Room, participant _: RemoteParticipant?, didReceiveData data: Data, forTopic _: String) { do { let roomMessage = try jsonDecoder.decode(ExampleRoomMessage.self, from: data) // Update UI from main queue From 9b3e1816adb31e60c4b8ed92a8cf6c8e534e5b6a Mon Sep 17 00:00:00 2001 From: cloudwebrtc Date: Tue, 23 Jan 2024 13:39:06 +0800 Subject: [PATCH 26/32] support external audio processor. --- LiveKitExample.xcodeproj/project.pbxproj | 6 ++++ Shared/Controllers/RoomContext.swift | 7 ++++- Shared/FakeAudioProcessor.swift | 36 ++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 Shared/FakeAudioProcessor.swift diff --git a/LiveKitExample.xcodeproj/project.pbxproj b/LiveKitExample.xcodeproj/project.pbxproj index 04f65ff..3567b4e 100644 --- a/LiveKitExample.xcodeproj/project.pbxproj +++ b/LiveKitExample.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 2C13AA9E2B5F886700E7BB18 /* FakeAudioProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C13AA9D2B5F886700E7BB18 /* FakeAudioProcessor.swift */; }; + 2C13AA9F2B5F886700E7BB18 /* FakeAudioProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C13AA9D2B5F886700E7BB18 /* FakeAudioProcessor.swift */; }; 680FE2F227A8EF7700B6F6DB /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = 680FE2F127A8EF7700B6F6DB /* SFSafeSymbols */; }; 680FE2F427A8EFF700B6F6DB /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = 680FE2F327A8EFF700B6F6DB /* SFSafeSymbols */; }; 6816B1A8272D45DF005ADB85 /* ExampleObservableRoom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6816B1A7272D45DF005ADB85 /* ExampleObservableRoom.swift */; }; @@ -72,6 +74,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 2C13AA9D2B5F886700E7BB18 /* FakeAudioProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FakeAudioProcessor.swift; sourceTree = ""; }; 6816B1A7272D45DF005ADB85 /* ExampleObservableRoom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleObservableRoom.swift; sourceTree = ""; }; 6816B1AF272D9198005ADB85 /* ParticipantView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParticipantView.swift; sourceTree = ""; }; 681E3F38271FC772007BB547 /* RoomContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomContext.swift; sourceTree = ""; }; @@ -212,6 +215,7 @@ 685271EA27443907006B4D6A /* Controllers */, 681E3F3E271FC795007BB547 /* Custom.swift */, 681E3F42271FC7AD007BB547 /* ConnectView.swift */, + 2C13AA9D2B5F886700E7BB18 /* FakeAudioProcessor.swift */, 681E3F41271FC7AC007BB547 /* RoomView.swift */, 68B3853C271E780600711D5F /* LiveKitExample.swift */, 6816B1A7272D45DF005ADB85 /* ExampleObservableRoom.swift */, @@ -399,6 +403,7 @@ 681E3F45271FC7AD007BB547 /* ConnectView.swift in Sources */, 6867533B27A65652003707B9 /* AppContext.swift in Sources */, 681E3F39271FC772007BB547 /* RoomContext.swift in Sources */, + 2C13AA9E2B5F886700E7BB18 /* FakeAudioProcessor.swift in Sources */, 683720D227A06404007DA986 /* ConnectionHistory.swift in Sources */, 681E3F43271FC7AD007BB547 /* RoomView.swift in Sources */, 6816B1B0272D9198005ADB85 /* ParticipantView.swift in Sources */, @@ -418,6 +423,7 @@ 681E3F46271FC7AD007BB547 /* ConnectView.swift in Sources */, 6867533C27A65652003707B9 /* AppContext.swift in Sources */, 681E3F3A271FC772007BB547 /* RoomContext.swift in Sources */, + 2C13AA9F2B5F886700E7BB18 /* FakeAudioProcessor.swift in Sources */, 683720D327A06404007DA986 /* ConnectionHistory.swift in Sources */, 681E3F44271FC7AD007BB547 /* RoomView.swift in Sources */, 6816B1B1272D9198005ADB85 /* ParticipantView.swift in Sources */, diff --git a/Shared/Controllers/RoomContext.swift b/Shared/Controllers/RoomContext.swift index 4c367bc..c3d0e36 100644 --- a/Shared/Controllers/RoomContext.swift +++ b/Shared/Controllers/RoomContext.swift @@ -66,6 +66,8 @@ final class RoomContext: ObservableObject { @Published var textFieldString: String = "" + var audioProcessor: AudioProcessor? = nil + public init(store: ValueStore) { self.store = store room.add(delegate: self) @@ -114,6 +116,8 @@ final class RoomContext: ObservableObject { keyProvider.setKey(key: e2eeKey) e2eeOptions = E2EEOptions(keyProvider: keyProvider) } + + audioProcessor = FakeAudioProcessor() let roomOptions = RoomOptions( defaultCameraCaptureOptions: CameraCaptureOptions( @@ -129,7 +133,8 @@ final class RoomContext: ObservableObject { adaptiveStream: adaptiveStream, dynacast: dynacast, reportStats: reportStats, - e2eeOptions: e2eeOptions + e2eeOptions: e2eeOptions, + audioProcessor: audioProcessor ) return try await room.connect(url, diff --git a/Shared/FakeAudioProcessor.swift b/Shared/FakeAudioProcessor.swift new file mode 100644 index 0000000..3293710 --- /dev/null +++ b/Shared/FakeAudioProcessor.swift @@ -0,0 +1,36 @@ +// +// FakeAudioProcessor.swift +// LiveKitExample +// +// Created by 段维伟 on 2024/1/23. +// + +import Foundation +import LiveKit + +class FakeAudioProcessor: AudioProcessor { + + public init() { + + } + func isEnabled(url: String, token: String) -> Bool { + print("check \(getName()) isEnabled: url: \(url) token: \(token)") + return true + } + + func getName() -> String { + "facke_audio_processor" + } + + func audioProcessingInitialize(sampleRate sampleRateHz: Int, channels: Int) { + print("\(getName()) audioProcessingInitialize: sampleRate: \(sampleRateHz) channels: \(channels)") + } + + func audioProcessingProcess(audioBuffer: LKAudioBuffer) { + print("\(getName()) audioProcessingProcess: \(String(describing: audioBuffer)) ") + } + + func audioProcessingRelease() { + print("\(getName()) audioProcessingRelease:") + } +} From 0b03be82cc3e757148ae80073fff418e31ba63d0 Mon Sep 17 00:00:00 2001 From: cloudwebrtc Date: Thu, 25 Jan 2024 15:12:43 +0800 Subject: [PATCH 27/32] update. --- Shared/FakeAudioProcessor.swift | 21 ++- .../Views/ScreenShareSourcePickerView.swift | 168 ++++++++++-------- 2 files changed, 105 insertions(+), 84 deletions(-) diff --git a/Shared/FakeAudioProcessor.swift b/Shared/FakeAudioProcessor.swift index 3293710..6c49b6c 100644 --- a/Shared/FakeAudioProcessor.swift +++ b/Shared/FakeAudioProcessor.swift @@ -1,9 +1,18 @@ -// -// FakeAudioProcessor.swift -// LiveKitExample -// -// Created by 段维伟 on 2024/1/23. -// +/* + * Copyright 2023 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 import LiveKit diff --git a/Shared/Views/ScreenShareSourcePickerView.swift b/Shared/Views/ScreenShareSourcePickerView.swift index b72018d..f4f1014 100644 --- a/Shared/Views/ScreenShareSourcePickerView.swift +++ b/Shared/Views/ScreenShareSourcePickerView.swift @@ -1,25 +1,39 @@ -import Foundation +/* + * Copyright 2023 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 CoreGraphics -import SwiftUI +import Foundation import LiveKit +import SwiftUI #if os(macOS) -class ScreenShareSourcePickerCtrl: ObservableObject { - - @Published var tracks = [LocalVideoTrack]() - @Published var mode: ScreenShareSourcePickerView.Mode = .display { - didSet { - guard oldValue != mode else { return } - Task { - await restartTracks() + @available(macOS 12.3, *) + class ScreenShareSourcePickerCtrl: ObservableObject { + @Published var tracks = [LocalVideoTrack]() + @Published var mode: ScreenShareSourcePickerView.Mode = .display { + didSet { + guard oldValue != mode else { return } + Task { + try await restartTracks() + } } } - } - - private func restartTracks() async { - Task { + private func restartTracks() async throws { // stop in parallel await withThrowingTaskGroup(of: Void.self) { group in for track in tracks { @@ -29,7 +43,7 @@ class ScreenShareSourcePickerCtrl: ObservableObject { } } - let sources = try await MacOSScreenCapturer.sources(for: (mode == .display ? .display : .window)) + let sources = try await MacOSScreenCapturer.sources(for: mode == .display ? .display : .window) let options = ScreenShareCaptureOptions(dimensions: .h360_43, fps: 5) let _newTracks = sources.map { LocalVideoTrack.createMacOSScreenShareTrack(source: $0, options: options) } @@ -46,93 +60,91 @@ class ScreenShareSourcePickerCtrl: ObservableObject { } } } - } - init() { - Task { - await restartTracks() + init() { + Task { + try await restartTracks() + } } - } - - deinit { - print("\(type(of: self)) deinit") + deinit { + print("\(type(of: self)) deinit") - // copy - let _tracks = tracks + // copy + let _tracks = tracks - Task { - // stop in parallel - await withThrowingTaskGroup(of: Void.self) { group in - for track in _tracks { - group.addTask { - try await track.stop() + Task { + // stop in parallel + await withThrowingTaskGroup(of: Void.self) { group in + for track in _tracks { + group.addTask { + try await track.stop() + } } } } } } -} -typealias OnPickScreenShareSource = (MacOSScreenCaptureSource) -> Void + typealias OnPickScreenShareSource = (MacOSScreenCaptureSource) -> Void -struct ScreenShareSourcePickerView: View { - - public enum Mode { - case display - case window - } - - @ObservedObject var ctrl = ScreenShareSourcePickerCtrl() + @available(macOS 12.3, *) + struct ScreenShareSourcePickerView: View { + public enum Mode { + case display + case window + } - let onPickScreenShareSource: OnPickScreenShareSource? + @ObservedObject var ctrl = ScreenShareSourcePickerCtrl() - private var columns = [ - GridItem(.fixed(250)), - GridItem(.fixed(250)) - ] + let onPickScreenShareSource: OnPickScreenShareSource? - init(onPickScreenShareSource: OnPickScreenShareSource? = nil) { - self.onPickScreenShareSource = onPickScreenShareSource - } + private var columns = [ + GridItem(.fixed(250)), + GridItem(.fixed(250)), + ] - var body: some View { + init(onPickScreenShareSource: OnPickScreenShareSource? = nil) { + self.onPickScreenShareSource = onPickScreenShareSource + } - VStack { - Picker("", selection: $ctrl.mode) { - Text("Entire Screen").tag(ScreenShareSourcePickerView.Mode.display) - Text("Application Window").tag(ScreenShareSourcePickerView.Mode.window) - } - .pickerStyle(SegmentedPickerStyle()) - - ScrollView(.vertical, showsIndicators: true) { - LazyVGrid(columns: columns, - alignment: .center, - spacing: 10) { - - ForEach(ctrl.tracks) { track in - ZStack { - SwiftUIVideoView(track, layoutMode: .fit) - .aspectRatio(1, contentMode: .fit) - .onTapGesture { - guard let capturer = track.capturer as? MacOSScreenCapturer, - let source = capturer.captureSource else { return } - onPickScreenShareSource?(source) + var body: some View { + VStack { + Picker("", selection: $ctrl.mode) { + Text("Entire Screen").tag(ScreenShareSourcePickerView.Mode.display) + Text("Application Window").tag(ScreenShareSourcePickerView.Mode.window) + } + .pickerStyle(SegmentedPickerStyle()) + + ScrollView(.vertical, showsIndicators: true) { + LazyVGrid(columns: columns, + alignment: .center, + spacing: 10) + { + ForEach(ctrl.tracks) { track in + ZStack { + SwiftUIVideoView(track, layoutMode: .fit) + .aspectRatio(1, contentMode: .fit) + .onTapGesture { + guard let capturer = track.capturer as? MacOSScreenCapturer, + let source = capturer.captureSource else { return } + onPickScreenShareSource?(source) + } + + if let capturer = track.capturer as? MacOSScreenCapturer, + let source = capturer.captureSource as? MacOSWindow, + let appName = source.owningApplication?.applicationName + { + Text(appName) + .shadow(color: .black, radius: 1) } - - if let capturer = track.capturer as? MacOSScreenCapturer, - let source = capturer.captureSource as? MacOSWindow, - let appName = source.owningApplication?.applicationName { - Text(appName) - .shadow(color: .black, radius: 1) } } } } + .frame(minHeight: 350) } - .frame(minHeight: 350) } } -} #endif From 84dfd92f1ac0d7c020ca809e9b6b28a3777de8a4 Mon Sep 17 00:00:00 2001 From: cloudwebrtc Date: Sat, 27 Jan 2024 00:02:59 +0800 Subject: [PATCH 28/32] add AudioProcessorOptionsView. --- LiveKitExample.xcodeproj/project.pbxproj | 6 +++ Shared/ConnectView.swift | 2 +- Shared/RoomView.swift | 14 ++++++ Shared/Views/AudioProcessorOptionsView.swift | 48 ++++++++++++++++++++ 4 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 Shared/Views/AudioProcessorOptionsView.swift diff --git a/LiveKitExample.xcodeproj/project.pbxproj b/LiveKitExample.xcodeproj/project.pbxproj index 5fe618a..6d53879 100644 --- a/LiveKitExample.xcodeproj/project.pbxproj +++ b/LiveKitExample.xcodeproj/project.pbxproj @@ -9,6 +9,8 @@ /* Begin PBXBuildFile section */ 2C13AA9E2B5F886700E7BB18 /* FakeAudioProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C13AA9D2B5F886700E7BB18 /* FakeAudioProcessor.swift */; }; 2C13AA9F2B5F886700E7BB18 /* FakeAudioProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C13AA9D2B5F886700E7BB18 /* FakeAudioProcessor.swift */; }; + 2C8910302B640DA60058BECE /* AudioProcessorOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C89102F2B640DA60058BECE /* AudioProcessorOptionsView.swift */; }; + 2C8910312B640DA60058BECE /* AudioProcessorOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C89102F2B640DA60058BECE /* AudioProcessorOptionsView.swift */; }; 680FE2F227A8EF7700B6F6DB /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = 680FE2F127A8EF7700B6F6DB /* SFSafeSymbols */; }; 680FE2F427A8EFF700B6F6DB /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = 680FE2F327A8EFF700B6F6DB /* SFSafeSymbols */; }; 6816968E2AF96240008ED486 /* Participant+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6816968D2AF96240008ED486 /* Participant+Helpers.swift */; }; @@ -79,6 +81,7 @@ /* Begin PBXFileReference section */ 2C13AA9D2B5F886700E7BB18 /* FakeAudioProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FakeAudioProcessor.swift; sourceTree = ""; }; + 2C89102F2B640DA60058BECE /* AudioProcessorOptionsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioProcessorOptionsView.swift; sourceTree = ""; }; 6816968D2AF96240008ED486 /* Participant+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Participant+Helpers.swift"; sourceTree = ""; }; 6816B1A7272D45DF005ADB85 /* ExampleRoomMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleRoomMessage.swift; sourceTree = ""; }; 6816B1AF272D9198005ADB85 /* ParticipantView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParticipantView.swift; sourceTree = ""; }; @@ -199,6 +202,7 @@ 6884B77A2750505B00732D47 /* Views */ = { isa = PBXGroup; children = ( + 2C89102F2B640DA60058BECE /* AudioProcessorOptionsView.swift */, 6884B77B2750507400732D47 /* ScreenShareSourcePickerView.swift */, 687230F72B14AE0A0098CCE6 /* PublishOptionsView.swift */, ); @@ -411,6 +415,7 @@ 681E3F45271FC7AD007BB547 /* ConnectView.swift in Sources */, 6867533B27A65652003707B9 /* AppContext.swift in Sources */, 681E3F39271FC772007BB547 /* RoomContext.swift in Sources */, + 2C8910302B640DA60058BECE /* AudioProcessorOptionsView.swift in Sources */, 2C13AA9E2B5F886700E7BB18 /* FakeAudioProcessor.swift in Sources */, 683720D227A06404007DA986 /* ConnectionHistory.swift in Sources */, 681E3F43271FC7AD007BB547 /* RoomView.swift in Sources */, @@ -433,6 +438,7 @@ 681E3F46271FC7AD007BB547 /* ConnectView.swift in Sources */, 6867533C27A65652003707B9 /* AppContext.swift in Sources */, 681E3F3A271FC772007BB547 /* RoomContext.swift in Sources */, + 2C8910312B640DA60058BECE /* AudioProcessorOptionsView.swift in Sources */, 2C13AA9F2B5F886700E7BB18 /* FakeAudioProcessor.swift in Sources */, 683720D327A06404007DA986 /* ConnectionHistory.swift in Sources */, 681E3F44271FC7AD007BB547 /* RoomView.swift in Sources */, diff --git a/Shared/ConnectView.swift b/Shared/ConnectView.swift index 489b73b..02d0f79 100644 --- a/Shared/ConnectView.swift +++ b/Shared/ConnectView.swift @@ -34,7 +34,7 @@ struct ConnectView: View { .aspectRatio(contentMode: .fit) .frame(height: 30) .padding(.bottom, 10) - Text("SDK Version \(LiveKit.version)") + //Text("SDK Version \(LiveKit.version)") .opacity(0.5) Text("Example App Version \(Bundle.main.appVersionLong) (\(Bundle.main.appBuild))") .opacity(0.5) diff --git a/Shared/RoomView.swift b/Shared/RoomView.swift index 191995c..54616e3 100644 --- a/Shared/RoomView.swift +++ b/Shared/RoomView.swift @@ -95,6 +95,8 @@ struct RoomView: View { #endif @State private var showConnectionTime = true + + @State private var audioProcessorPickerPresented = false func messageView(_ message: ExampleRoomMessage) -> some View { let isMe = message.senderSid == room.localParticipant.sid @@ -442,6 +444,18 @@ struct RoomView: View { Image(systemSymbol: .messageFill) .renderingMode(roomCtx.showMessagesView ? .original : .template) }) + Button(action: { + audioProcessorPickerPresented = true + }, + label: { + Image(systemSymbol: .waveform) + .renderingMode(roomCtx.room.audioProcessorIsEnabled ? .original : .template) + }) + .popover(isPresented: $audioProcessorPickerPresented) { + AudioProcessorOptionsView(roomCtx: roomCtx) { pickerResult in + publishOptionsPickerPresented = false + }.padding() + }.disabled(!roomCtx.room.audioProcessorIsEnabled) } // Spacer() diff --git a/Shared/Views/AudioProcessorOptionsView.swift b/Shared/Views/AudioProcessorOptionsView.swift new file mode 100644 index 0000000..6fd7ab0 --- /dev/null +++ b/Shared/Views/AudioProcessorOptionsView.swift @@ -0,0 +1,48 @@ +/* + * Copyright 2023 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 +import LiveKit +import SwiftUI + +struct AudioProcessorOptionsView: View { + typealias OnNSEnabled = (_ enabled: Bool) -> Void + + @State private var enabled: Bool + private let roomCtx: RoomContext + private let onEnabled: OnNSEnabled + + init(roomCtx: RoomContext, _ onEnabled: @escaping OnNSEnabled) { + self.roomCtx = roomCtx + self.onEnabled = onEnabled + self.enabled = !LiveKit.AudioManager.shared.bypassForCapturePostProcessing + } + + var body: some View { + VStack(alignment: .center, spacing: 10) { + Text("Audio Processor") + .fontWeight(.bold) + Text("name: \(roomCtx.room.audioProcessor?.getName() ?? "")") + + Toggle(isOn: $enabled, label: { + Text("Toggle") + }).onChange(of: enabled) { newValue in + LiveKit.AudioManager.shared.bypassForCapturePostProcessing = !newValue + } + .keyboardShortcut(.defaultAction) + } + } +} From 04e9dd984297030e8627b48c841f997607d6e327 Mon Sep 17 00:00:00 2001 From: cloudwebrtc Date: Mon, 29 Jan 2024 08:57:53 +0800 Subject: [PATCH 29/32] add support for AudioProcessorOptions. --- Shared/Controllers/RoomContext.swift | 10 +++++++--- Shared/FakeAudioProcessor.swift | 1 + Shared/RoomView.swift | 8 ++++---- Shared/Views/AudioProcessorOptionsView.swift | 2 +- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Shared/Controllers/RoomContext.swift b/Shared/Controllers/RoomContext.swift index 033fb2b..11bfb47 100644 --- a/Shared/Controllers/RoomContext.swift +++ b/Shared/Controllers/RoomContext.swift @@ -80,6 +80,7 @@ final class RoomContext: ObservableObject { @Published var messages: [ExampleRoomMessage] = [] @Published var textFieldString: String = "" + var audioProcessor: AudioProcessor? = nil @@ -136,8 +137,11 @@ final class RoomContext: ObservableObject { keyProvider.setKey(key: e2eeKey) e2eeOptions = E2EEOptions(keyProvider: keyProvider) } - - audioProcessor = FakeAudioProcessor() + + let audioProcessorOptions = AudioProcessorOptions( + capturePost: FakeAudioProcessor(), + bypassCapturePost: true + ) let roomOptions = RoomOptions( defaultCameraCaptureOptions: CameraCaptureOptions( @@ -153,7 +157,7 @@ final class RoomContext: ObservableObject { adaptiveStream: true, dynacast: true, // e2eeOptions: e2eeOptions, - audioProcessor: audioProcessor, + audioProcessorOptions: audioProcessorOptions, reportRemoteTrackStatistics: true ) diff --git a/Shared/FakeAudioProcessor.swift b/Shared/FakeAudioProcessor.swift index 6c49b6c..58d246c 100644 --- a/Shared/FakeAudioProcessor.swift +++ b/Shared/FakeAudioProcessor.swift @@ -22,6 +22,7 @@ class FakeAudioProcessor: AudioProcessor { public init() { } + func isEnabled(url: String, token: String) -> Bool { print("check \(getName()) isEnabled: url: \(url) token: \(token)") return true diff --git a/Shared/RoomView.swift b/Shared/RoomView.swift index 54616e3..de1fef7 100644 --- a/Shared/RoomView.swift +++ b/Shared/RoomView.swift @@ -449,13 +449,13 @@ struct RoomView: View { }, label: { Image(systemSymbol: .waveform) - .renderingMode(roomCtx.room.audioProcessorIsEnabled ? .original : .template) + .renderingMode(roomCtx.room.audioProcessorOptions != nil ? .original : .template) }) .popover(isPresented: $audioProcessorPickerPresented) { - AudioProcessorOptionsView(roomCtx: roomCtx) { pickerResult in - publishOptionsPickerPresented = false + AudioProcessorOptionsView(roomCtx: roomCtx) { _ in + audioProcessorPickerPresented = false }.padding() - }.disabled(!roomCtx.room.audioProcessorIsEnabled) + }.disabled(roomCtx.room.audioProcessorOptions == nil) } // Spacer() diff --git a/Shared/Views/AudioProcessorOptionsView.swift b/Shared/Views/AudioProcessorOptionsView.swift index 6fd7ab0..439efc1 100644 --- a/Shared/Views/AudioProcessorOptionsView.swift +++ b/Shared/Views/AudioProcessorOptionsView.swift @@ -35,7 +35,7 @@ struct AudioProcessorOptionsView: View { VStack(alignment: .center, spacing: 10) { Text("Audio Processor") .fontWeight(.bold) - Text("name: \(roomCtx.room.audioProcessor?.getName() ?? "")") + Text("name: \(roomCtx.room.audioProcessorOptions?.getCapturePostProcessor()?.getName() ?? "")") Toggle(isOn: $enabled, label: { Text("Toggle") From 52ff5611021a368e44e87ea92426a96ce8758e27 Mon Sep 17 00:00:00 2001 From: cloudwebrtc Date: Tue, 30 Jan 2024 12:53:50 +0800 Subject: [PATCH 30/32] update. --- Shared/Controllers/RoomContext.swift | 3 +-- Shared/Views/AudioProcessorOptionsView.swift | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Shared/Controllers/RoomContext.swift b/Shared/Controllers/RoomContext.swift index 11bfb47..dccad84 100644 --- a/Shared/Controllers/RoomContext.swift +++ b/Shared/Controllers/RoomContext.swift @@ -139,8 +139,7 @@ final class RoomContext: ObservableObject { } let audioProcessorOptions = AudioProcessorOptions( - capturePost: FakeAudioProcessor(), - bypassCapturePost: true + capturePostProcessor: FakeAudioProcessor() ) let roomOptions = RoomOptions( diff --git a/Shared/Views/AudioProcessorOptionsView.swift b/Shared/Views/AudioProcessorOptionsView.swift index 439efc1..4f9b683 100644 --- a/Shared/Views/AudioProcessorOptionsView.swift +++ b/Shared/Views/AudioProcessorOptionsView.swift @@ -35,7 +35,7 @@ struct AudioProcessorOptionsView: View { VStack(alignment: .center, spacing: 10) { Text("Audio Processor") .fontWeight(.bold) - Text("name: \(roomCtx.room.audioProcessorOptions?.getCapturePostProcessor()?.getName() ?? "")") + Text("name: \(roomCtx.room.audioProcessorOptions?.capturePostProcessor?.getName() ?? "")") Toggle(isOn: $enabled, label: { Text("Toggle") From 867e223efddc41f2a5976f60649493252b92df21 Mon Sep 17 00:00:00 2001 From: CloudWebRTC Date: Tue, 30 Jan 2024 13:07:34 +0800 Subject: [PATCH 31/32] fix typo. --- Shared/FakeAudioProcessor.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Shared/FakeAudioProcessor.swift b/Shared/FakeAudioProcessor.swift index 58d246c..1414029 100644 --- a/Shared/FakeAudioProcessor.swift +++ b/Shared/FakeAudioProcessor.swift @@ -29,7 +29,7 @@ class FakeAudioProcessor: AudioProcessor { } func getName() -> String { - "facke_audio_processor" + "fake_audio_processor" } func audioProcessingInitialize(sampleRate sampleRateHz: Int, channels: Int) { From ed17c8c94dcf33b7f0e718d8ed3e031c1493dc30 Mon Sep 17 00:00:00 2001 From: cloudwebrtc Date: Tue, 30 Jan 2024 13:09:18 +0800 Subject: [PATCH 32/32] update. --- Shared/Views/AudioProcessorOptionsView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Shared/Views/AudioProcessorOptionsView.swift b/Shared/Views/AudioProcessorOptionsView.swift index 4f9b683..47adf21 100644 --- a/Shared/Views/AudioProcessorOptionsView.swift +++ b/Shared/Views/AudioProcessorOptionsView.swift @@ -41,6 +41,7 @@ struct AudioProcessorOptionsView: View { Text("Toggle") }).onChange(of: enabled) { newValue in LiveKit.AudioManager.shared.bypassForCapturePostProcessing = !newValue + onEnabled(newValue); } .keyboardShortcut(.defaultAction) }