├── .swift-version ├── Shared ├── Assets.xcassets │ ├── Contents.json │ ├── iOS.appiconset │ │ ├── Twitter Icon-20.png │ │ ├── Twitter Icon-29.png │ │ ├── Twitter Icon-40.png │ │ ├── Twitter Icon-76.png │ │ ├── Twitter Icon-1024.png │ │ ├── Twitter Icon-20@2x.png │ │ ├── Twitter Icon-20@3x.png │ │ ├── Twitter Icon-29@2x.png │ │ ├── Twitter Icon-29@3x.png │ │ ├── Twitter Icon-40@2x.png │ │ ├── Twitter Icon-40@3x.png │ │ ├── Twitter Icon-60@2x.png │ │ ├── Twitter Icon-60@3x.png │ │ ├── Twitter Icon-76@2x.png │ │ ├── Twitter Icon-83.5@2x.png │ │ └── Contents.json │ ├── logo.imageset │ │ ├── Livekit Wordmark.png │ │ ├── Livekit Wordmark@2x.png │ │ ├── Livekit Wordmark@3x.png │ │ └── Contents.json │ ├── MacOS.appiconset │ │ ├── Twitter Icon-128.png │ │ ├── Twitter Icon-16.png │ │ ├── Twitter Icon-256.png │ │ ├── Twitter Icon-32.png │ │ ├── Twitter Icon-512.png │ │ ├── Twitter Icon-64.png │ │ ├── Twitter Icon-1024.png │ │ └── Contents.json │ ├── LKBlue.colorset │ │ └── Contents.json │ ├── LKRed.colorset │ │ └── Contents.json │ ├── lkDarkRed.colorset │ │ └── Contents.json │ ├── lkGray1.colorset │ │ └── Contents.json │ ├── lkGray2.colorset │ │ └── Contents.json │ ├── lkGray3.colorset │ │ └── Contents.json │ ├── LKDarkBlue.colorset │ │ └── Contents.json │ └── AccentColor.colorset │ │ └── Contents.json ├── Support │ ├── Participant+Helpers.swift │ ├── ExampleRoomMessage.swift │ ├── Bundle.swift │ ├── Binding+OptionSet.swift │ ├── ConnectionHistory.swift │ └── SecureStore.swift ├── Custom.swift ├── Controllers │ ├── AppContext.swift │ └── RoomContext.swift ├── Views │ ├── PublishOptionsView.swift │ └── ScreenShareSourcePickerView.swift ├── LiveKitExample.swift ├── ConnectView.swift ├── ParticipantView.swift └── RoomView.swift ├── .periphery.yml ├── LiveKitExample.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ └── xcschemes │ ├── LiveKitExample (iOS).xcscheme │ ├── LiveKitExample (macOS).xcscheme │ └── BroadcastExt.xcscheme ├── LiveKitExample-main.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved ├── xcshareddata │ └── xcschemes │ │ ├── LiveKitExample (macOS).xcscheme │ │ └── BroadcastExt.xcscheme └── project.pbxproj ├── LiveKitExample-dev.xcworkspace ├── xcshareddata │ ├── WorkspaceSettings.xcsettings │ ├── IDEWorkspaceChecks.plist │ └── swiftpm │ │ └── Package.resolved └── contents.xcworkspacedata ├── .swiftformat ├── iOS ├── BroadcastExt │ ├── BroadcastExt.entitlements │ ├── SampleHandler.swift │ ├── Info.plist │ └── LoggingOSLog.swift ├── iOS.entitlements └── Info.plist ├── macOS ├── macOS.entitlements └── Info.plist ├── .gitignore └── README.md /.swift-version: -------------------------------------------------------------------------------- 1 | 5.7 # Xcode 14 2 | -------------------------------------------------------------------------------- /Shared/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Shared/Assets.xcassets/iOS.appiconset/Twitter Icon-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcherry/swift-example/main/Shared/Assets.xcassets/iOS.appiconset/Twitter Icon-20.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/iOS.appiconset/Twitter Icon-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcherry/swift-example/main/Shared/Assets.xcassets/iOS.appiconset/Twitter Icon-29.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/iOS.appiconset/Twitter Icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcherry/swift-example/main/Shared/Assets.xcassets/iOS.appiconset/Twitter Icon-40.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/iOS.appiconset/Twitter Icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcherry/swift-example/main/Shared/Assets.xcassets/iOS.appiconset/Twitter Icon-76.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/logo.imageset/Livekit Wordmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcherry/swift-example/main/Shared/Assets.xcassets/logo.imageset/Livekit Wordmark.png -------------------------------------------------------------------------------- /.periphery.yml: -------------------------------------------------------------------------------- 1 | retain_objc_accessible: true 2 | schemes: 3 | - Example (macOS Debug) 4 | targets: 5 | - LiveKitExample (macOS) 6 | workspace: LiveKitExample-dev.xcworkspace 7 | -------------------------------------------------------------------------------- /Shared/Assets.xcassets/MacOS.appiconset/Twitter Icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcherry/swift-example/main/Shared/Assets.xcassets/MacOS.appiconset/Twitter Icon-128.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/MacOS.appiconset/Twitter Icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcherry/swift-example/main/Shared/Assets.xcassets/MacOS.appiconset/Twitter Icon-16.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/MacOS.appiconset/Twitter Icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcherry/swift-example/main/Shared/Assets.xcassets/MacOS.appiconset/Twitter Icon-256.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/MacOS.appiconset/Twitter Icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcherry/swift-example/main/Shared/Assets.xcassets/MacOS.appiconset/Twitter Icon-32.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/MacOS.appiconset/Twitter Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcherry/swift-example/main/Shared/Assets.xcassets/MacOS.appiconset/Twitter Icon-512.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/MacOS.appiconset/Twitter Icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcherry/swift-example/main/Shared/Assets.xcassets/MacOS.appiconset/Twitter Icon-64.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/iOS.appiconset/Twitter Icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcherry/swift-example/main/Shared/Assets.xcassets/iOS.appiconset/Twitter Icon-1024.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/iOS.appiconset/Twitter Icon-20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcherry/swift-example/main/Shared/Assets.xcassets/iOS.appiconset/Twitter Icon-20@2x.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/iOS.appiconset/Twitter Icon-20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcherry/swift-example/main/Shared/Assets.xcassets/iOS.appiconset/Twitter Icon-20@3x.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/iOS.appiconset/Twitter Icon-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcherry/swift-example/main/Shared/Assets.xcassets/iOS.appiconset/Twitter Icon-29@2x.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/iOS.appiconset/Twitter Icon-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcherry/swift-example/main/Shared/Assets.xcassets/iOS.appiconset/Twitter Icon-29@3x.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/iOS.appiconset/Twitter Icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcherry/swift-example/main/Shared/Assets.xcassets/iOS.appiconset/Twitter Icon-40@2x.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/iOS.appiconset/Twitter Icon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcherry/swift-example/main/Shared/Assets.xcassets/iOS.appiconset/Twitter Icon-40@3x.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/iOS.appiconset/Twitter Icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcherry/swift-example/main/Shared/Assets.xcassets/iOS.appiconset/Twitter Icon-60@2x.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/iOS.appiconset/Twitter Icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcherry/swift-example/main/Shared/Assets.xcassets/iOS.appiconset/Twitter Icon-60@3x.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/iOS.appiconset/Twitter Icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcherry/swift-example/main/Shared/Assets.xcassets/iOS.appiconset/Twitter Icon-76@2x.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/logo.imageset/Livekit Wordmark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcherry/swift-example/main/Shared/Assets.xcassets/logo.imageset/Livekit Wordmark@2x.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/logo.imageset/Livekit Wordmark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcherry/swift-example/main/Shared/Assets.xcassets/logo.imageset/Livekit Wordmark@3x.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/MacOS.appiconset/Twitter Icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcherry/swift-example/main/Shared/Assets.xcassets/MacOS.appiconset/Twitter Icon-1024.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/iOS.appiconset/Twitter Icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcherry/swift-example/main/Shared/Assets.xcassets/iOS.appiconset/Twitter Icon-83.5@2x.png -------------------------------------------------------------------------------- /LiveKitExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LiveKitExample-main.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LiveKitExample-dev.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LiveKitExample-dev.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LiveKitExample-dev.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /LiveKitExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LiveKitExample-main.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Shared/Assets.xcassets/LKBlue.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFF", 9 | "green" : "0x8B", 10 | "red" : "0x5A" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Shared/Assets.xcassets/LKRed.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x5D", 9 | "green" : "0x6D", 10 | "red" : "0xFF" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Shared/Assets.xcassets/lkDarkRed.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x40", 9 | "green" : "0x4B", 10 | "red" : "0xB0" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Shared/Assets.xcassets/lkGray1.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x19", 9 | "green" : "0x19", 10 | "red" : "0x19" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Shared/Assets.xcassets/lkGray2.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x32", 9 | "green" : "0x32", 10 | "red" : "0x32" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Shared/Assets.xcassets/lkGray3.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x4B", 9 | "green" : "0x4B", 10 | "red" : "0x4B" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Shared/Assets.xcassets/LKDarkBlue.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x3C", 9 | "green" : "0x15", 10 | "red" : "0x00" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Shared/Assets.xcassets/logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Livekit Wordmark.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Livekit Wordmark@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "Livekit Wordmark@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | --exclude Sources/LiveKit/Protos 2 | --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 */" 3 | -------------------------------------------------------------------------------- /iOS/BroadcastExt/BroadcastExt.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | 9 | group.io.livekit.example.SwiftSDK.1 10 | 11 | com.apple.security.device.audio-input 12 | 13 | com.apple.security.device.camera 14 | 15 | com.apple.security.network.client 16 | 17 | com.apple.security.network.server 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /iOS/iOS.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | 9 | group.io.livekit.example.SwiftSDK.1 10 | 11 | com.apple.security.device.audio-input 12 | 13 | com.apple.security.device.camera 14 | 15 | com.apple.security.network.client 16 | 17 | com.apple.security.network.server 18 | 19 | keychain-access-groups 20 | 21 | $(AppIdentifierPrefix)keychain-group.io.livekit.example.SwiftSDK.1 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Shared/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "1.000", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "1.000", 27 | "green" : "1.000", 28 | "red" : "1.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /macOS/macOS.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | 9 | group.io.livekit.example.SwiftSDK.1 10 | 11 | com.apple.security.device.audio-input 12 | 13 | com.apple.security.device.camera 14 | 15 | com.apple.security.files.user-selected.read-only 16 | 17 | com.apple.security.network.client 18 | 19 | com.apple.security.network.server 20 | 21 | keychain-access-groups 22 | 23 | $(AppIdentifierPrefix)keychain-group.io.livekit.example.SwiftSDK.1 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Shared/Support/Participant+Helpers.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 LiveKit 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import LiveKit 18 | 19 | public extension Participant { 20 | var mainVideoPublication: TrackPublication? { 21 | firstScreenSharePublication ?? firstCameraPublication 22 | } 23 | 24 | var mainVideoTrack: VideoTrack? { 25 | firstScreenShareVideoTrack ?? firstCameraVideoTrack 26 | } 27 | 28 | var subVideoTrack: VideoTrack? { 29 | firstScreenShareVideoTrack != nil ? firstCameraVideoTrack : nil 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /iOS/BroadcastExt/SampleHandler.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 LiveKit 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import Foundation 18 | import LiveKit 19 | import Logging 20 | import OSLog 21 | 22 | private let broadcastLogger = OSLog(subsystem: "io.livekit.example.SwiftSDK", category: "Broadcast") 23 | class SampleHandler: LKSampleHandler { 24 | override public init() { 25 | // Turn on logging for the Broadcast Extension 26 | LoggingSystem.bootstrap { label in 27 | var logHandler = LoggingOSLog(label: label, log: broadcastLogger) 28 | logHandler.logLevel = .debug 29 | return logHandler 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Shared/Support/ExampleRoomMessage.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 LiveKit 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import LiveKit 18 | 19 | struct ExampleRoomMessage: Identifiable, Equatable, Hashable, Codable { 20 | // Identifiable protocol needs param named id 21 | var id: String { 22 | messageId 23 | } 24 | 25 | // message id 26 | let messageId: String 27 | 28 | let senderSid: Participant.Sid? 29 | let senderIdentity: Participant.Identity? 30 | let text: String 31 | 32 | static func == (lhs: Self, rhs: Self) -> Bool { 33 | lhs.messageId == rhs.messageId 34 | } 35 | 36 | func hash(into hasher: inout Hasher) { 37 | hasher.combine(messageId) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Shared/Support/Bundle.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 LiveKit 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import Foundation 18 | 19 | public extension Bundle { 20 | var appName: String { getInfo("CFBundleName") } 21 | var displayName: String { getInfo("CFBundleDisplayName") } 22 | var language: String { getInfo("CFBundleDevelopmentRegion") } 23 | var identifier: String { getInfo("CFBundleIdentifier") } 24 | 25 | var appBuild: String { getInfo("CFBundleVersion") } 26 | var appVersionLong: String { getInfo("CFBundleShortVersionString") } 27 | var appVersionShort: String { getInfo("CFBundleShortVersion") } 28 | 29 | private func getInfo(_ str: String) -> String { infoDictionary?[str] as? String ?? "⚠️" } 30 | } 31 | -------------------------------------------------------------------------------- /iOS/BroadcastExt/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | LiveKit Broadcast Example 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSExtension 24 | 25 | NSExtensionPointIdentifier 26 | com.apple.broadcast-services-upload 27 | NSExtensionPrincipalClass 28 | $(PRODUCT_MODULE_NAME).SampleHandler 29 | RPBroadcastProcessMode 30 | RPBroadcastProcessModeSampleBuffer 31 | 32 | RTCAppGroupIdentifier 33 | group.io.livekit.example.SwiftSDK.1 34 | 35 | 36 | -------------------------------------------------------------------------------- /Shared/Support/Binding+OptionSet.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 LiveKit 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import SwiftUI 18 | 19 | extension Binding where Value: OptionSet, Value == Value.Element { 20 | func bindedValue(_ options: Value) -> Bool { 21 | wrappedValue.contains(options) 22 | } 23 | 24 | func bind( 25 | _ options: Value, 26 | animate: Bool = false 27 | ) -> Binding { 28 | .init { () -> Bool in 29 | self.wrappedValue.contains(options) 30 | } set: { newValue in 31 | let body = { 32 | if newValue { 33 | self.wrappedValue.insert(options) 34 | } else { 35 | self.wrappedValue.remove(options) 36 | } 37 | } 38 | guard animate else { 39 | body() 40 | return 41 | } 42 | withAnimation { 43 | body() 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Shared/Assets.xcassets/MacOS.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Twitter Icon-16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "Twitter Icon-32.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "Twitter Icon-32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "Twitter Icon-64.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "Twitter Icon-128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "Twitter Icon-256.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "Twitter Icon-256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "Twitter Icon-512.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "Twitter Icon-512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "Twitter Icon-1024.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /macOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | LiveKit Example 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIconFile 12 | 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | $(PRODUCT_NAME) 19 | CFBundlePackageType 20 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 21 | CFBundleShortVersionString 22 | $(MARKETING_VERSION) 23 | CFBundleURLTypes 24 | 25 | 26 | CFBundleURLSchemes 27 | 28 | livekit 29 | 30 | 31 | 32 | CFBundleVersion 33 | $(CURRENT_PROJECT_VERSION) 34 | LSApplicationCategoryType 35 | public.app-category.developer-tools 36 | LSMinimumSystemVersion 37 | $(MACOSX_DEPLOYMENT_TARGET) 38 | NSAppTransportSecurity 39 | 40 | NSAllowsArbitraryLoads 41 | 42 | 43 | NSCameraUsageDescription 44 | Please allow for publishing your camera 45 | NSMicrophoneUsageDescription 46 | Please allow for publishing your microphone 47 | 48 | 49 | -------------------------------------------------------------------------------- /LiveKitExample-main.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "bf59b79f5d4ea0a54ef7e929b035c5821f5094ee990a7a369ad0cb3713f6c62d", 3 | "pins" : [ 4 | { 5 | "identity" : "client-sdk-swift", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/livekit/client-sdk-swift", 8 | "state" : { 9 | "branch" : "main", 10 | "revision" : "fceb4566aac45b8e0107c43c740a9e8149850d3b" 11 | } 12 | }, 13 | { 14 | "identity" : "keychainaccess", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/kishikawakatsumi/KeychainAccess.git", 17 | "state" : { 18 | "revision" : "84e546727d66f1adc5439debad16270d0fdd04e7", 19 | "version" : "4.2.2" 20 | } 21 | }, 22 | { 23 | "identity" : "sfsafesymbols", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/SFSafeSymbols/SFSafeSymbols", 26 | "state" : { 27 | "revision" : "7cca2d60925876b5953a2cf7341cd80fbeac983c", 28 | "version" : "4.1.1" 29 | } 30 | }, 31 | { 32 | "identity" : "swift-log", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/apple/swift-log.git", 35 | "state" : { 36 | "revision" : "e97a6fcb1ab07462881ac165fdbb37f067e205d5", 37 | "version" : "1.5.4" 38 | } 39 | }, 40 | { 41 | "identity" : "swift-protobuf", 42 | "kind" : "remoteSourceControl", 43 | "location" : "https://github.com/apple/swift-protobuf.git", 44 | "state" : { 45 | "revision" : "65e8f29b2d63c4e38e736b25c27b83e012159be8", 46 | "version" : "1.25.2" 47 | } 48 | }, 49 | { 50 | "identity" : "webrtc-xcframework", 51 | "kind" : "remoteSourceControl", 52 | "location" : "https://github.com/livekit/webrtc-xcframework.git", 53 | "state" : { 54 | "revision" : "56e5119f00daa2e42e10670d29ae2edbff6be961", 55 | "version" : "114.5735.13" 56 | } 57 | } 58 | ], 59 | "version" : 3 60 | } 61 | -------------------------------------------------------------------------------- /LiveKitExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "bf59b79f5d4ea0a54ef7e929b035c5821f5094ee990a7a369ad0cb3713f6c62d", 3 | "pins" : [ 4 | { 5 | "identity" : "client-sdk-swift", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/livekit/client-sdk-swift", 8 | "state" : { 9 | "revision" : "c7358137775ed6f6ac583d13c79f39f6244005d8", 10 | "version" : "2.0.9" 11 | } 12 | }, 13 | { 14 | "identity" : "keychainaccess", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/kishikawakatsumi/KeychainAccess.git", 17 | "state" : { 18 | "revision" : "84e546727d66f1adc5439debad16270d0fdd04e7", 19 | "version" : "4.2.2" 20 | } 21 | }, 22 | { 23 | "identity" : "sfsafesymbols", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/SFSafeSymbols/SFSafeSymbols", 26 | "state" : { 27 | "revision" : "7cca2d60925876b5953a2cf7341cd80fbeac983c", 28 | "version" : "4.1.1" 29 | } 30 | }, 31 | { 32 | "identity" : "swift-log", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/apple/swift-log.git", 35 | "state" : { 36 | "revision" : "e97a6fcb1ab07462881ac165fdbb37f067e205d5", 37 | "version" : "1.5.4" 38 | } 39 | }, 40 | { 41 | "identity" : "swift-protobuf", 42 | "kind" : "remoteSourceControl", 43 | "location" : "https://github.com/apple/swift-protobuf.git", 44 | "state" : { 45 | "revision" : "9f0c76544701845ad98716f3f6a774a892152bcb", 46 | "version" : "1.26.0" 47 | } 48 | }, 49 | { 50 | "identity" : "webrtc-xcframework", 51 | "kind" : "remoteSourceControl", 52 | "location" : "https://github.com/livekit/webrtc-xcframework.git", 53 | "state" : { 54 | "revision" : "f7222b7a8012fb96e87ae95f1075e27a927de7c5", 55 | "version" : "114.5735.18" 56 | } 57 | } 58 | ], 59 | "version" : 3 60 | } 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | .DS_Store 92 | -------------------------------------------------------------------------------- /iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | LiveKit Example 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleURLTypes 22 | 23 | 24 | CFBundleURLSchemes 25 | 26 | livekit 27 | 28 | 29 | 30 | CFBundleVersion 31 | $(CURRENT_PROJECT_VERSION) 32 | ITSAppUsesNonExemptEncryption 33 | 34 | LSRequiresIPhoneOS 35 | 36 | NSAppTransportSecurity 37 | 38 | NSAllowsArbitraryLoads 39 | 40 | 41 | NSCameraUsageDescription 42 | uses your camera for video chat 43 | NSFaceIDUsageDescription 44 | Keychain is used to store all preferences. 45 | NSMicrophoneUsageDescription 46 | uses your microphone for video chat 47 | RTCAppGroupIdentifier 48 | group.io.livekit.example.SwiftSDK.1 49 | RTCScreenSharingExtension 50 | io.livekit.example.SwiftSDK.1.BroadcastExt 51 | UIApplicationSceneManifest 52 | 53 | UIApplicationSupportsMultipleScenes 54 | 55 | 56 | UIApplicationSupportsIndirectInputEvents 57 | 58 | UIBackgroundModes 59 | 60 | audio 61 | voip 62 | 63 | UILaunchScreen 64 | 65 | UIRequiredDeviceCapabilities 66 | 67 | armv7 68 | 69 | UISupportedInterfaceOrientations 70 | 71 | UIInterfaceOrientationLandscapeLeft 72 | UIInterfaceOrientationLandscapeRight 73 | UIInterfaceOrientationPortrait 74 | UIInterfaceOrientationPortraitUpsideDown 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "jwt-kit", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/vapor/jwt-kit.git", 7 | "state" : { 8 | "revision" : "c2595b9ad7f512d7f334830b4df1fed6e917946a", 9 | "version" : "4.13.4" 10 | } 11 | }, 12 | { 13 | "identity" : "keychainaccess", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/kishikawakatsumi/KeychainAccess.git", 16 | "state" : { 17 | "revision" : "84e546727d66f1adc5439debad16270d0fdd04e7", 18 | "version" : "4.2.2" 19 | } 20 | }, 21 | { 22 | "identity" : "sfsafesymbols", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/SFSafeSymbols/SFSafeSymbols", 25 | "state" : { 26 | "revision" : "7cca2d60925876b5953a2cf7341cd80fbeac983c", 27 | "version" : "4.1.1" 28 | } 29 | }, 30 | { 31 | "identity" : "swift-crypto", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/apple/swift-crypto.git", 34 | "state" : { 35 | "revision" : "bc1c29221f6dfeb0ebbfbc98eb95cd3d4967868e", 36 | "version" : "3.4.0" 37 | } 38 | }, 39 | { 40 | "identity" : "swift-docc-plugin", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/apple/swift-docc-plugin.git", 43 | "state" : { 44 | "revision" : "26ac5758409154cc448d7ab82389c520fa8a8247", 45 | "version" : "1.3.0" 46 | } 47 | }, 48 | { 49 | "identity" : "swift-docc-symbolkit", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/apple/swift-docc-symbolkit", 52 | "state" : { 53 | "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", 54 | "version" : "1.0.0" 55 | } 56 | }, 57 | { 58 | "identity" : "swift-log", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/apple/swift-log.git", 61 | "state" : { 62 | "revision" : "e97a6fcb1ab07462881ac165fdbb37f067e205d5", 63 | "version" : "1.5.4" 64 | } 65 | }, 66 | { 67 | "identity" : "swift-protobuf", 68 | "kind" : "remoteSourceControl", 69 | "location" : "https://github.com/apple/swift-protobuf.git", 70 | "state" : { 71 | "revision" : "9f0c76544701845ad98716f3f6a774a892152bcb", 72 | "version" : "1.26.0" 73 | } 74 | }, 75 | { 76 | "identity" : "webrtc-xcframework", 77 | "kind" : "remoteSourceControl", 78 | "location" : "https://github.com/livekit/webrtc-xcframework.git", 79 | "state" : { 80 | "revision" : "f7222b7a8012fb96e87ae95f1075e27a927de7c5", 81 | "version" : "114.5735.18" 82 | } 83 | } 84 | ], 85 | "version" : 2 86 | } 87 | -------------------------------------------------------------------------------- /Shared/Support/ConnectionHistory.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 LiveKit 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import LiveKit 18 | import SwiftUI 19 | 20 | struct ConnectionHistory: Codable { 21 | let updated: Date 22 | let url: String 23 | let token: String 24 | let e2ee: Bool 25 | let e2eeKey: String 26 | let roomSid: Room.Sid? 27 | let roomName: String? 28 | let participantSid: Participant.Sid? 29 | let participantIdentity: Participant.Identity? 30 | let participantName: String? 31 | } 32 | 33 | extension ConnectionHistory: CustomStringConvertible { 34 | var description: String { 35 | var segments: [String] = [] 36 | if let roomName { 37 | segments.append(String(describing: roomName)) 38 | } 39 | if let participantIdentity { 40 | segments.append(String(describing: participantIdentity)) 41 | } 42 | segments.append(url) 43 | return segments.joined(separator: " ") 44 | } 45 | } 46 | 47 | extension ConnectionHistory: Identifiable { 48 | var id: Int { 49 | hashValue 50 | } 51 | } 52 | 53 | extension ConnectionHistory: Hashable, Equatable { 54 | func hash(into hasher: inout Hasher) { 55 | hasher.combine(url) 56 | hasher.combine(token) 57 | } 58 | 59 | static func == (lhs: ConnectionHistory, rhs: ConnectionHistory) -> Bool { 60 | lhs.url == rhs.url && lhs.token == rhs.token 61 | } 62 | } 63 | 64 | extension Sequence { 65 | var sortedByUpdated: [ConnectionHistory] { 66 | Array(self).sorted { $0.updated > $1.updated } 67 | } 68 | } 69 | 70 | extension Set { 71 | mutating func update(room: Room, e2ee: Bool, e2eeKey: String) { 72 | guard let url = room.url, 73 | let token = room.token else { return } 74 | 75 | let element = ConnectionHistory( 76 | updated: Date(), 77 | url: url, 78 | token: token, 79 | e2ee: e2ee, 80 | e2eeKey: e2eeKey, 81 | roomSid: room.sid, 82 | roomName: room.name, 83 | participantSid: room.localParticipant.sid, 84 | participantIdentity: room.localParticipant.identity, 85 | participantName: room.localParticipant.name 86 | ) 87 | 88 | update(with: element) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Shared/Assets.xcassets/iOS.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Twitter Icon-20@2x.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "Twitter Icon-20@3x.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "Twitter Icon-29@2x.png", 17 | "idiom" : "iphone", 18 | "scale" : "2x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "Twitter Icon-29@3x.png", 23 | "idiom" : "iphone", 24 | "scale" : "3x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "Twitter Icon-40@2x.png", 29 | "idiom" : "iphone", 30 | "scale" : "2x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "filename" : "Twitter Icon-40@3x.png", 35 | "idiom" : "iphone", 36 | "scale" : "3x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "Twitter Icon-60@2x.png", 41 | "idiom" : "iphone", 42 | "scale" : "2x", 43 | "size" : "60x60" 44 | }, 45 | { 46 | "filename" : "Twitter Icon-60@3x.png", 47 | "idiom" : "iphone", 48 | "scale" : "3x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "Twitter Icon-20.png", 53 | "idiom" : "ipad", 54 | "scale" : "1x", 55 | "size" : "20x20" 56 | }, 57 | { 58 | "filename" : "Twitter Icon-20@2x.png", 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "20x20" 62 | }, 63 | { 64 | "filename" : "Twitter Icon-29.png", 65 | "idiom" : "ipad", 66 | "scale" : "1x", 67 | "size" : "29x29" 68 | }, 69 | { 70 | "filename" : "Twitter Icon-29@2x.png", 71 | "idiom" : "ipad", 72 | "scale" : "2x", 73 | "size" : "29x29" 74 | }, 75 | { 76 | "filename" : "Twitter Icon-40.png", 77 | "idiom" : "ipad", 78 | "scale" : "1x", 79 | "size" : "40x40" 80 | }, 81 | { 82 | "filename" : "Twitter Icon-40@2x.png", 83 | "idiom" : "ipad", 84 | "scale" : "2x", 85 | "size" : "40x40" 86 | }, 87 | { 88 | "filename" : "Twitter Icon-76.png", 89 | "idiom" : "ipad", 90 | "scale" : "1x", 91 | "size" : "76x76" 92 | }, 93 | { 94 | "filename" : "Twitter Icon-76@2x.png", 95 | "idiom" : "ipad", 96 | "scale" : "2x", 97 | "size" : "76x76" 98 | }, 99 | { 100 | "filename" : "Twitter Icon-83.5@2x.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "83.5x83.5" 104 | }, 105 | { 106 | "filename" : "Twitter Icon-1024.png", 107 | "idiom" : "ios-marketing", 108 | "scale" : "1x", 109 | "size" : "1024x1024" 110 | } 111 | ], 112 | "info" : { 113 | "author" : "xcode", 114 | "version" : 1 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Shared/Support/SecureStore.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 LiveKit 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import Combine 18 | import KeychainAccess 19 | import LiveKit 20 | import SwiftUI 21 | 22 | struct Preferences: Codable, Equatable { 23 | var url = "" 24 | var token = "" 25 | var e2eeKey = "" 26 | var isE2eeEnabled = false 27 | 28 | // Connect options 29 | var autoSubscribe = true 30 | 31 | // Room options 32 | var simulcast = true 33 | var adaptiveStream = true 34 | var dynacast = true 35 | var reportStats = true 36 | 37 | // Settings 38 | var videoViewVisible = true 39 | var showInformationOverlay = false 40 | var preferSampleBufferRendering = false 41 | var videoViewMode: VideoView.LayoutMode = .fit 42 | var videoViewMirrored = false 43 | 44 | var connectionHistory = Set() 45 | } 46 | 47 | let encoder = JSONEncoder() 48 | let decoder = JSONDecoder() 49 | 50 | class ValueStore: ObservableObject { 51 | private let store: Keychain 52 | private let key: String 53 | private let message = "" 54 | private weak var timer: Timer? 55 | 56 | public var value: T { 57 | didSet { 58 | guard oldValue != value else { return } 59 | lazySync() 60 | } 61 | } 62 | 63 | private var storeWithOptions: Keychain { 64 | store 65 | .accessibility(.whenUnlocked) 66 | .synchronizable(true) 67 | } 68 | 69 | public init(store: Keychain, key: String, default: T) { 70 | self.store = store 71 | self.key = key 72 | value = `default` 73 | 74 | if let data = try? storeWithOptions.getData(key), 75 | let result = try? decoder.decode(T.self, from: data) 76 | { 77 | value = result 78 | } 79 | } 80 | 81 | deinit { 82 | timer?.invalidate() 83 | } 84 | 85 | public func lazySync() { 86 | timer?.invalidate() 87 | timer = Timer.scheduledTimer(withTimeInterval: 1, 88 | repeats: false, 89 | block: { _ in self.sync() }) 90 | } 91 | 92 | public func sync() { 93 | do { 94 | let data = try encoder.encode(value) 95 | try storeWithOptions.set(data, key: key) 96 | } catch { 97 | print("Failed to write in Keychain, error: \(error)") 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /LiveKitExample-main.xcodeproj/xcshareddata/xcschemes/LiveKitExample (macOS).xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 44 | 50 | 51 | 52 | 53 | 59 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /LiveKitExample.xcodeproj/xcshareddata/xcschemes/LiveKitExample (iOS).xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /LiveKitExample.xcodeproj/xcshareddata/xcschemes/LiveKitExample (macOS).xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 44 | 46 | 52 | 53 | 54 | 55 | 61 | 63 | 69 | 70 | 71 | 72 | 74 | 75 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LiveKit SDK Example App for iOS & macOS 2 | 3 | This app demonstrates the basic usage of [LiveKit Swift SDK (iOS/macOS)](https://github.com/livekit/client-sdk-swift). See [LiveKit Docs](https://docs.livekit.io/) for more information. 4 | 5 | ### Compiled version 6 | 7 | Precompiled macOS version is available from the [Releases page](https://github.com/livekit/client-example-swift/releases), so you can quickly try out features of the [LiveKit Swift SDK](https://github.com/livekit/client-sdk-swift) or the [LiveKit Server](https://github.com/livekit/livekit-server). 8 | 9 | Precompiled iOS version can be downloaded on [Apple TestFlight](https://testflight.apple.com/join/21F6ARiQ). Click on the link from an iOS device and follow the instructions. 10 | 11 | ### Screenshots 12 | **macOS** 13 | ![macOS](https://user-images.githubusercontent.com/548776/150068761-ce8f7d59-72e8-412a-9675-66a2eec9f04f.png) 14 | 15 | # How to run the example 16 | 17 | ### Get the code 18 | 19 | 1. Clone this [LiveKit Swift Example](https://github.com/livekit/client-example-swift) repo. 20 | 2. Open `LiveKitExample.xcodeproj` (not the `-dev.xcworkspace`). 21 | 3. Wait for packages to sync. 22 | 23 | ### Change bundle id & code signing information 24 | 1. Select the `LiveKitExample` project from the left Navigator. 25 | 2. For each **Target**, select **Signing & Capabilities** tab and update your **Team** and **Bundle Identifier** to your preference. 26 | 27 | ### 🚀 Run 28 | 1. Select `LiveKitExample (iOS)` or `LiveKitExample (macOS)` from the **Scheme selector** at the top of Xcode. 29 | 2. **Run** the project from the menu **Product** → **Run** or by ⌘R. 30 | 31 | If you encounter code signing issues, make sure you change the **Team** and **bundle id** from the previous step. 32 | 33 | ### ⚡️ Connect 34 | 35 | 1. Prepare & Start [LiveKit Server](https://github.com/livekit/livekit-server). See the [Getting Started page](https://docs.livekit.io/guides/getting-started) for more information. 36 | 2. Generate an access token. 37 | 3. Enter the **Server URL** and **Access token** to the example app and tap **Connect**. 38 | 39 | Server URL would typically look like `ws://localhost:7880` depending on your configuration. It should start with `ws://` for *non-secure* and `wss://` for *secure* connections. 40 | 41 | ### ✅ Permissions 42 | 43 | iOS/macOS will ask you to grant permission when enabling **Camera**, **Microphone** and/or **Screen Share**. Simply allow this to continue publishing the track. 44 | 45 | #### macOS Screen Share 46 | 47 | Open **Settings** → **Security & Privacy** → **Screen Recording** and make sure **LiveKitExample** has a ✔️ mark. You will need to restart the app. 48 | 49 | # Troubleshooting 50 | 51 | ### Package errors 52 | 53 | If you get package syncing errors, try *resetting your package caches* by right clicking **Package Dependencies** and choosing **Reset Package Caches** from the **Navigator**. 54 | 55 | # Getting help / Contributing 56 | 57 | Please join us on [Slack](https://join.slack.com/t/livekit-users/shared_invite/zt-rrdy5abr-5pZ1wW8pXEkiQxBzFiXPUg) to get help from our [devs](https://github.com/orgs/livekit/teams/devs/members) / community members. We welcome your contributions(PRs) and details can be discussed there. 58 | 59 | # Development 60 | 61 | For development, open `LiveKitExample-dev.xcworkspace` instead. This workspace will compile with the local `../client-sdk-swift`. 62 | -------------------------------------------------------------------------------- /Shared/Custom.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 LiveKit 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import SwiftUI 18 | 19 | extension Color { 20 | static let lkRed = Color("lkRed") 21 | static let lkDarkRed = Color("lkDarkRed") 22 | static let lkGray1 = Color("lkGray1") 23 | static let lkGray2 = Color("lkGray2") 24 | static let lkGray3 = Color("lkGray3") 25 | } 26 | 27 | struct LazyView: View { 28 | let build: () -> Content 29 | init(_ build: @autoclosure @escaping () -> Content) { 30 | self.build = build 31 | } 32 | 33 | var body: Content { 34 | build() 35 | } 36 | } 37 | 38 | // Default button style for this example 39 | struct LKButton: View { 40 | let title: String 41 | let action: () -> Void 42 | 43 | var body: some View { 44 | Button(action: action, 45 | label: { 46 | Text(title.uppercased()) 47 | .fontWeight(.bold) 48 | .padding(.horizontal, 12) 49 | .padding(.vertical, 10) 50 | }) 51 | .background(Color.lkRed) 52 | .cornerRadius(8) 53 | } 54 | } 55 | 56 | #if os(iOS) 57 | extension LKTextField.`Type` { 58 | func toiOSType() -> UIKeyboardType { 59 | switch self { 60 | case .default: return .default 61 | case .URL: return .URL 62 | case .ascii: return .asciiCapable 63 | } 64 | } 65 | } 66 | #endif 67 | 68 | #if os(macOS) 69 | // Avoid showing focus border around textfield for macOS 70 | extension NSTextField { 71 | override open var focusRingType: NSFocusRingType { 72 | get { .none } 73 | set {} 74 | } 75 | } 76 | #endif 77 | 78 | struct LKTextField: View { 79 | enum `Type` { 80 | case `default` 81 | case URL 82 | case ascii 83 | } 84 | 85 | let title: String 86 | @Binding var text: String 87 | var type: Type = .default 88 | 89 | var body: some View { 90 | VStack(alignment: .leading, spacing: 10.0) { 91 | Text(title) 92 | .fontWeight(.bold) 93 | 94 | TextField("", text: $text) 95 | .textFieldStyle(PlainTextFieldStyle()) 96 | .disableAutocorrection(true) 97 | // TODO: add iOS unique view modifiers 98 | // #if os(iOS) 99 | // .autocapitalization(.none) 100 | // .keyboardType(type.toiOSType()) 101 | // #endif 102 | .padding() 103 | .overlay(RoundedRectangle(cornerRadius: 10.0) 104 | .strokeBorder(Color.white.opacity(0.3), 105 | style: StrokeStyle(lineWidth: 1.0))) 106 | 107 | }.frame(maxWidth: .infinity) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Shared/Controllers/AppContext.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 LiveKit 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import Combine 18 | import LiveKit 19 | import SwiftUI 20 | 21 | // This class contains the logic to control behavior of the whole app. 22 | final class AppContext: ObservableObject { 23 | private let store: ValueStore 24 | 25 | @Published var videoViewVisible: Bool = true { 26 | didSet { store.value.videoViewVisible = videoViewVisible } 27 | } 28 | 29 | @Published var showInformationOverlay: Bool = false { 30 | didSet { store.value.showInformationOverlay = showInformationOverlay } 31 | } 32 | 33 | @Published var preferSampleBufferRendering: Bool = false { 34 | didSet { store.value.preferSampleBufferRendering = preferSampleBufferRendering } 35 | } 36 | 37 | @Published var videoViewMode: VideoView.LayoutMode = .fit { 38 | didSet { store.value.videoViewMode = videoViewMode } 39 | } 40 | 41 | @Published var videoViewMirrored: Bool = false { 42 | didSet { store.value.videoViewMirrored = videoViewMirrored } 43 | } 44 | 45 | @Published var videoViewPinchToZoomOptions: VideoView.PinchToZoomOptions = [] 46 | 47 | @Published var connectionHistory: Set = [] { 48 | didSet { store.value.connectionHistory = connectionHistory } 49 | } 50 | 51 | @Published var outputDevice: AudioDevice = AudioManager.shared.defaultOutputDevice { 52 | didSet { 53 | print("didSet outputDevice: \(String(describing: outputDevice))") 54 | AudioManager.shared.outputDevice = outputDevice 55 | } 56 | } 57 | 58 | @Published var inputDevice: AudioDevice = AudioManager.shared.defaultInputDevice { 59 | didSet { 60 | print("didSet inputDevice: \(String(describing: inputDevice))") 61 | AudioManager.shared.inputDevice = inputDevice 62 | } 63 | } 64 | 65 | @Published var preferSpeakerOutput: Bool = true { 66 | didSet { AudioManager.shared.isSpeakerOutputPreferred = preferSpeakerOutput } 67 | } 68 | 69 | public init(store: ValueStore) { 70 | self.store = store 71 | 72 | videoViewVisible = store.value.videoViewVisible 73 | showInformationOverlay = store.value.showInformationOverlay 74 | preferSampleBufferRendering = store.value.preferSampleBufferRendering 75 | videoViewMode = store.value.videoViewMode 76 | videoViewMirrored = store.value.videoViewMirrored 77 | connectionHistory = store.value.connectionHistory 78 | 79 | AudioManager.shared.onDeviceUpdate = { [weak self] audioManager in 80 | guard let self else { return } 81 | print("devices did update") 82 | // force UI update for outputDevice / inputDevice 83 | Task.detached { @MainActor [weak self] in 84 | guard let self else { return } 85 | self.outputDevice = audioManager.outputDevice 86 | self.inputDevice = audioManager.inputDevice 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /iOS/BroadcastExt/LoggingOSLog.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 LiveKit 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import Foundation 18 | import Logging 19 | import os 20 | 21 | public struct LoggingOSLog: LogHandler { 22 | public var logLevel: Logging.Logger.Level = .debug 23 | private let oslogger: OSLog 24 | 25 | public init(label: String) { 26 | oslogger = OSLog(subsystem: label, category: "") 27 | } 28 | 29 | public init(label _: String, log: OSLog) { 30 | oslogger = log 31 | } 32 | 33 | public func log(level _: Logging.Logger.Level, message: Logging.Logger.Message, metadata: Logging.Logger.Metadata?, file _: String, function _: String, line _: UInt) { 34 | var combinedPrettyMetadata = prettyMetadata 35 | if let metadataOverride = metadata, !metadataOverride.isEmpty { 36 | combinedPrettyMetadata = prettify( 37 | self.metadata.merging(metadataOverride) { 38 | $1 39 | } 40 | ) 41 | } 42 | 43 | var formedMessage = message.description 44 | if combinedPrettyMetadata != nil { 45 | formedMessage += " -- " + combinedPrettyMetadata! 46 | } 47 | os_log("%{public}@", log: oslogger, type: OSLogType.from(loggerLevel: .info), formedMessage as NSString) 48 | } 49 | 50 | private var prettyMetadata: String? 51 | public var metadata = Logger.Metadata() { 52 | didSet { 53 | prettyMetadata = prettify(metadata) 54 | } 55 | } 56 | 57 | /// Add, remove, or change the logging metadata. 58 | /// - parameters: 59 | /// - metadataKey: the key for the metadata item. 60 | public subscript(metadataKey metadataKey: String) -> Logging.Logger.Metadata.Value? { 61 | get { 62 | metadata[metadataKey] 63 | } 64 | set { 65 | metadata[metadataKey] = newValue 66 | } 67 | } 68 | 69 | private func prettify(_ metadata: Logging.Logger.Metadata) -> String? { 70 | if metadata.isEmpty { 71 | return nil 72 | } 73 | return metadata.map { 74 | "\($0)=\($1)" 75 | }.joined(separator: " ") 76 | } 77 | } 78 | 79 | extension OSLogType { 80 | static func from(loggerLevel: Logging.Logger.Level) -> Self { 81 | switch loggerLevel { 82 | case .trace: 83 | /// `OSLog` doesn't have `trace`, so use `debug` 84 | return .debug 85 | case .debug: 86 | return .debug 87 | case .info: 88 | return .info 89 | case .notice: 90 | // https://developer.apple.com/documentation/os/logging/generating_log_messages_from_your_code 91 | // According to the documentation, `default` is `notice`. 92 | return .default 93 | case .warning: 94 | /// `OSLog` doesn't have `warning`, so use `info` 95 | return .info 96 | case .error: 97 | return .error 98 | case .critical: 99 | return .fault 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /LiveKitExample.xcodeproj/xcshareddata/xcschemes/BroadcastExt.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 6 | 9 | 10 | 16 | 22 | 23 | 24 | 30 | 36 | 37 | 38 | 39 | 40 | 45 | 46 | 47 | 48 | 60 | 62 | 68 | 69 | 70 | 71 | 79 | 81 | 87 | 88 | 89 | 90 | 92 | 93 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /LiveKitExample-main.xcodeproj/xcshareddata/xcschemes/BroadcastExt.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 6 | 9 | 10 | 16 | 22 | 23 | 24 | 30 | 36 | 37 | 38 | 39 | 40 | 45 | 46 | 47 | 48 | 60 | 62 | 68 | 69 | 70 | 71 | 79 | 81 | 87 | 88 | 89 | 90 | 92 | 93 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /Shared/Views/PublishOptionsView.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 LiveKit 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import AVFoundation 18 | import LiveKit 19 | import SwiftUI 20 | 21 | extension AVCaptureDevice: Identifiable { 22 | public var id: String { uniqueID } 23 | } 24 | 25 | struct PublishOptionsView: View { 26 | typealias OnPublish = (_ captureOptions: CameraCaptureOptions, _ publishOptions: VideoPublishOptions) -> Void 27 | 28 | @State private var devices: [AVCaptureDevice] = [] 29 | @State private var device: AVCaptureDevice? 30 | @State private var simulcast: Bool 31 | @State private var preferredVideoCodec: VideoCodec? 32 | @State private var preferredBackupVideoCodec: VideoCodec? 33 | @State private var maxFPS: Int = 30 34 | 35 | private let providedPublishOptions: VideoPublishOptions 36 | private let onPublish: OnPublish 37 | 38 | init(publishOptions: VideoPublishOptions, _ onPublish: @escaping OnPublish) { 39 | providedPublishOptions = publishOptions 40 | self.onPublish = onPublish 41 | 42 | simulcast = publishOptions.simulcast 43 | preferredVideoCodec = publishOptions.preferredCodec 44 | preferredBackupVideoCodec = publishOptions.preferredBackupCodec 45 | } 46 | 47 | var body: some View { 48 | VStack(alignment: .center, spacing: 10) { 49 | Text("Publish options") 50 | .fontWeight(.bold) 51 | Form { 52 | Picker("Device", selection: $device) { 53 | Text("Auto").tag(nil as AVCaptureDevice?) 54 | ForEach(devices) { 55 | Text($0.localizedName).tag($0 as AVCaptureDevice?) 56 | } 57 | } 58 | 59 | Picker("Codec", selection: $preferredVideoCodec) { 60 | Text("Auto").tag(nil as VideoCodec?) 61 | ForEach(VideoCodec.all) { 62 | Text($0.id.uppercased()).tag($0 as VideoCodec?) 63 | } 64 | }.onChange(of: preferredVideoCodec) { newValue in 65 | if newValue?.isSVC ?? false { 66 | preferredBackupVideoCodec = .vp8 67 | } else { 68 | preferredBackupVideoCodec = nil 69 | } 70 | } 71 | 72 | Picker("Backup Codec", selection: $preferredBackupVideoCodec) { 73 | Text("Off").tag(nil as VideoCodec?) 74 | ForEach(VideoCodec.allBackup.filter { $0 != preferredVideoCodec }) { 75 | Text($0.id.uppercased()).tag($0 as VideoCodec?) 76 | } 77 | }.disabled(!(preferredVideoCodec?.isSVC ?? false)) 78 | 79 | Picker("Max FPS", selection: $maxFPS) { 80 | ForEach(1 ... 30, id: \.self) { 81 | Text("\($0)").tag($0) 82 | } 83 | } 84 | 85 | Toggle(isOn: $simulcast, label: { 86 | Text("Simulcast") 87 | }) 88 | } 89 | 90 | Button("Publish") { 91 | let captureOptions = CameraCaptureOptions( 92 | device: device, 93 | dimensions: .h1080_169 94 | ) 95 | 96 | let publishOptions = VideoPublishOptions( 97 | name: providedPublishOptions.name, 98 | encoding: VideoEncoding(maxBitrate: VideoParameters.presetH1080_169.encoding.maxBitrate, maxFps: maxFPS), 99 | screenShareEncoding: providedPublishOptions.screenShareEncoding, 100 | simulcast: simulcast, 101 | simulcastLayers: providedPublishOptions.simulcastLayers, 102 | screenShareSimulcastLayers: providedPublishOptions.screenShareSimulcastLayers, 103 | preferredCodec: preferredVideoCodec, 104 | preferredBackupCodec: preferredBackupVideoCodec 105 | ) 106 | 107 | onPublish(captureOptions, publishOptions) 108 | } 109 | .keyboardShortcut(.defaultAction) 110 | 111 | Spacer() 112 | } 113 | .onAppear(perform: { 114 | Task { @MainActor in 115 | devices = try await CameraCapturer.captureDevices().singleDeviceforEachPosition() 116 | } 117 | }) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Shared/Views/ScreenShareSourcePickerView.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 LiveKit 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import CoreGraphics 18 | import Foundation 19 | import LiveKit 20 | import SwiftUI 21 | 22 | #if os(macOS) 23 | 24 | @available(macOS 12.3, *) 25 | class ScreenShareSourcePickerCtrl: ObservableObject { 26 | @Published var tracks = [LocalVideoTrack]() 27 | @Published var mode: ScreenShareSourcePickerView.Mode = .display { 28 | didSet { 29 | guard oldValue != mode else { return } 30 | Task.detached { [weak self] in 31 | guard let self else { return } 32 | try await self.restartTracks() 33 | } 34 | } 35 | } 36 | 37 | private func restartTracks() async throws { 38 | // stop in parallel 39 | await withThrowingTaskGroup(of: Void.self) { group in 40 | for track in tracks { 41 | group.addTask { 42 | try await track.stop() 43 | } 44 | } 45 | } 46 | 47 | let sources = try await MacOSScreenCapturer.sources(for: mode == .display ? .display : .window) 48 | let options = ScreenShareCaptureOptions(dimensions: .h360_43, fps: 5) 49 | let _newTracks = sources.map { LocalVideoTrack.createMacOSScreenShareTrack(source: $0, options: options) } 50 | 51 | Task { @MainActor in 52 | self.tracks = _newTracks 53 | } 54 | 55 | // start in parallel 56 | await withThrowingTaskGroup(of: Void.self) { group in 57 | for track in _newTracks { 58 | group.addTask { 59 | try await track.start() 60 | } 61 | } 62 | } 63 | } 64 | 65 | init() { 66 | Task { 67 | try await restartTracks() 68 | } 69 | } 70 | 71 | deinit { 72 | print("\(type(of: self)) deinit") 73 | 74 | // copy 75 | let _tracks = tracks 76 | 77 | Task { 78 | // stop in parallel 79 | await withThrowingTaskGroup(of: Void.self) { group in 80 | for track in _tracks { 81 | group.addTask { 82 | try await track.stop() 83 | } 84 | } 85 | } 86 | } 87 | } 88 | } 89 | 90 | typealias OnPickScreenShareSource = (MacOSScreenCaptureSource) -> Void 91 | 92 | @available(macOS 12.3, *) 93 | struct ScreenShareSourcePickerView: View { 94 | public enum Mode { 95 | case display 96 | case window 97 | } 98 | 99 | @ObservedObject var ctrl = ScreenShareSourcePickerCtrl() 100 | 101 | let onPickScreenShareSource: OnPickScreenShareSource? 102 | 103 | private var columns = [ 104 | GridItem(.fixed(250)), 105 | GridItem(.fixed(250)), 106 | ] 107 | 108 | init(onPickScreenShareSource: OnPickScreenShareSource? = nil) { 109 | self.onPickScreenShareSource = onPickScreenShareSource 110 | } 111 | 112 | var body: some View { 113 | VStack { 114 | Picker("", selection: $ctrl.mode) { 115 | Text("Entire Screen").tag(ScreenShareSourcePickerView.Mode.display) 116 | Text("Application Window").tag(ScreenShareSourcePickerView.Mode.window) 117 | } 118 | .pickerStyle(SegmentedPickerStyle()) 119 | 120 | ScrollView(.vertical, showsIndicators: true) { 121 | LazyVGrid(columns: columns, 122 | alignment: .center, 123 | spacing: 10) 124 | { 125 | ForEach(ctrl.tracks) { track in 126 | ZStack { 127 | SwiftUIVideoView(track, layoutMode: .fit) 128 | .aspectRatio(1, contentMode: .fit) 129 | .onTapGesture { 130 | guard let capturer = track.capturer as? MacOSScreenCapturer, 131 | let source = capturer.captureSource else { return } 132 | onPickScreenShareSource?(source) 133 | } 134 | 135 | if let capturer = track.capturer as? MacOSScreenCapturer, 136 | let source = capturer.captureSource as? MacOSWindow, 137 | let appName = source.owningApplication?.applicationName 138 | { 139 | Text(appName) 140 | .shadow(color: .black, radius: 1) 141 | } 142 | } 143 | } 144 | } 145 | } 146 | .frame(minHeight: 350) 147 | } 148 | } 149 | } 150 | 151 | #endif 152 | -------------------------------------------------------------------------------- /Shared/LiveKitExample.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 LiveKit 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import KeychainAccess 18 | import LiveKit 19 | import Logging 20 | import SwiftUI 21 | 22 | let sync = ValueStore(store: Keychain(service: "io.livekit.example.SwiftSDK.1"), 23 | key: "preferences", 24 | default: Preferences()) 25 | 26 | struct RoomSwitchView: View { 27 | @EnvironmentObject var appCtx: AppContext 28 | @EnvironmentObject var roomCtx: RoomContext 29 | @EnvironmentObject var room: Room 30 | 31 | var shouldShowRoomView: Bool { 32 | room.connectionState == .connected || room.connectionState == .reconnecting 33 | } 34 | 35 | func computeTitle() -> String { 36 | if shouldShowRoomView { 37 | var elements: [String] = [] 38 | if let roomName = room.name { 39 | elements.append(roomName) 40 | } 41 | if let localParticipantName = room.localParticipant.name { 42 | elements.append(localParticipantName) 43 | } 44 | if let localParticipantIdentity = room.localParticipant.identity { 45 | elements.append(String(describing: localParticipantIdentity)) 46 | } 47 | return elements.joined(separator: " ") 48 | } 49 | 50 | return "LiveKit" 51 | } 52 | 53 | var body: some View { 54 | ZStack { 55 | Color.black 56 | .ignoresSafeArea() 57 | 58 | if shouldShowRoomView { 59 | RoomView() 60 | } else { 61 | ConnectView() 62 | } 63 | } 64 | .navigationTitle(computeTitle()) 65 | } 66 | } 67 | 68 | // Attaches RoomContext and Room to the environment 69 | struct RoomContextView: View { 70 | @EnvironmentObject var appCtx: AppContext 71 | @StateObject var roomCtx = RoomContext(store: sync) 72 | 73 | var body: some View { 74 | RoomSwitchView() 75 | .environmentObject(roomCtx) 76 | .environmentObject(roomCtx.room) 77 | .environment(\.colorScheme, .dark) 78 | .foregroundColor(Color.white) 79 | .onDisappear { 80 | print("\(String(describing: type(of: self))) onDisappear") 81 | Task { 82 | await roomCtx.disconnect() 83 | } 84 | } 85 | .onOpenURL(perform: { url in 86 | 87 | guard let urlComponent = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return } 88 | guard let host = url.host else { return } 89 | 90 | let secureValue = urlComponent.queryItems?.first(where: { $0.name == "secure" })?.value?.lowercased() 91 | let secure = ["true", "1"].contains { $0 == secureValue } 92 | 93 | let tokenValue = urlComponent.queryItems?.first(where: { $0.name == "token" })?.value ?? "" 94 | 95 | let e2eeValue = urlComponent.queryItems?.first(where: { $0.name == "e2ee" })?.value?.lowercased() 96 | let e2ee = ["true", "1"].contains { $0 == secureValue } 97 | let e2eeKey = urlComponent.queryItems?.first(where: { $0.name == "e2eeKey" })?.value ?? "" 98 | 99 | var builder = URLComponents() 100 | builder.scheme = secure ? "wss" : "ws" 101 | builder.host = host 102 | builder.port = url.port 103 | 104 | guard let builtUrl = builder.url?.absoluteString else { return } 105 | 106 | print("built URL: \(builtUrl), token: \(tokenValue)") 107 | 108 | Task { @MainActor in 109 | roomCtx.url = builtUrl 110 | roomCtx.token = tokenValue 111 | roomCtx.isE2eeEnabled = e2ee 112 | roomCtx.e2eeKey = e2eeKey 113 | if !roomCtx.token.isEmpty { 114 | let room = try await roomCtx.connect() 115 | appCtx.connectionHistory.update(room: room, e2ee: e2ee, e2eeKey: e2eeKey) 116 | } 117 | } 118 | }) 119 | } 120 | } 121 | 122 | extension Decimal { 123 | mutating func round(_ scale: Int, _ roundingMode: NSDecimalNumber.RoundingMode) { 124 | var localCopy = self 125 | NSDecimalRound(&self, &localCopy, scale, roundingMode) 126 | } 127 | 128 | func rounded(_ scale: Int, _ roundingMode: NSDecimalNumber.RoundingMode) -> Decimal { 129 | var result = Decimal() 130 | var localCopy = self 131 | NSDecimalRound(&result, &localCopy, scale, roundingMode) 132 | return result 133 | } 134 | 135 | func remainder(of divisor: Decimal) -> Decimal { 136 | let s = self as NSDecimalNumber 137 | let d = divisor as NSDecimalNumber 138 | let b = NSDecimalNumberHandler(roundingMode: .down, 139 | scale: 0, 140 | raiseOnExactness: false, 141 | raiseOnOverflow: false, 142 | raiseOnUnderflow: false, 143 | raiseOnDivideByZero: false) 144 | let quotient = s.dividing(by: d, withBehavior: b) 145 | 146 | let subtractAmount = quotient.multiplying(by: d) 147 | return s.subtracting(subtractAmount) as Decimal 148 | } 149 | } 150 | 151 | @main 152 | struct LiveKitExample: App { 153 | @StateObject var appCtx = AppContext(store: sync) 154 | 155 | func nearestSafeScale(for target: Int, scale: Double) -> Decimal { 156 | let p = Decimal(sign: .plus, exponent: -3, significand: 1) 157 | let t = Decimal(target) 158 | var s = Decimal(scale).rounded(3, .down) 159 | 160 | while (t * s / 2).remainder(of: 2) != 0 { 161 | s = s + p 162 | } 163 | 164 | return s 165 | } 166 | 167 | init() { 168 | LoggingSystem.bootstrap { 169 | var logHandler = StreamLogHandler.standardOutput(label: $0) 170 | logHandler.logLevel = .debug 171 | return logHandler 172 | } 173 | } 174 | 175 | var body: some Scene { 176 | WindowGroup { 177 | RoomContextView() 178 | .environmentObject(appCtx) 179 | } 180 | .handlesExternalEvents(matching: Set(arrayLiteral: "*")) 181 | #if os(macOS) 182 | .windowStyle(.hiddenTitleBar) 183 | .windowToolbarStyle(.unifiedCompact) 184 | #endif 185 | } 186 | } 187 | 188 | #if os(macOS) 189 | 190 | extension View { 191 | func withHostingWindow(_ callback: @escaping (NSWindow) -> Void) -> some View { 192 | background(HostingWindowFinder(callback: callback)) 193 | } 194 | } 195 | 196 | struct HostingWindowFinder: NSViewRepresentable { 197 | var callback: (NSWindow) -> Void 198 | 199 | func makeNSView(context _: Context) -> NSView { 200 | let view = NSView() 201 | DispatchQueue.main.async { [weak view] in 202 | if let window = view?.window { 203 | callback(window) 204 | } 205 | } 206 | return view 207 | } 208 | 209 | func updateNSView(_: NSView, context _: Context) {} 210 | } 211 | #endif 212 | -------------------------------------------------------------------------------- /Shared/ConnectView.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 LiveKit 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import Foundation 18 | import LiveKit 19 | import SFSafeSymbols 20 | import SwiftUI 21 | 22 | struct ConnectView: View { 23 | @EnvironmentObject var appCtx: AppContext 24 | @EnvironmentObject var roomCtx: RoomContext 25 | @EnvironmentObject var room: Room 26 | 27 | var body: some View { 28 | GeometryReader { geometry in 29 | ScrollView { 30 | VStack(alignment: .center, spacing: 40.0) { 31 | VStack(spacing: 10) { 32 | Image("logo") 33 | .resizable() 34 | .aspectRatio(contentMode: .fit) 35 | .frame(height: 30) 36 | .padding(.bottom, 10) 37 | Text("SDK Version \(LiveKitSDK.version)") 38 | .opacity(0.5) 39 | Text("Example App Version \(Bundle.main.appVersionLong) (\(Bundle.main.appBuild))") 40 | .opacity(0.5) 41 | } 42 | 43 | VStack(spacing: 15) { 44 | LKTextField(title: "Server URL", text: $roomCtx.url, type: .URL) 45 | LKTextField(title: "Token", text: $roomCtx.token, type: .ascii) 46 | LKTextField(title: "E2EE Key", text: $roomCtx.e2eeKey, type: .ascii) 47 | 48 | HStack { 49 | Menu { 50 | Toggle(isOn: $roomCtx.autoSubscribe) { 51 | Text("Auto-Subscribe") 52 | } 53 | Toggle(isOn: $roomCtx.isE2eeEnabled) { 54 | Text("Enable E2EE") 55 | } 56 | } label: { 57 | Image(systemSymbol: .boltFill) 58 | .renderingMode(.original) 59 | Text("Connect Options") 60 | } 61 | #if os(macOS) 62 | .menuStyle(BorderlessButtonMenuStyle(showsMenuIndicator: true)) 63 | #elseif os(iOS) 64 | .menuStyle(BorderlessButtonMenuStyle()) 65 | #endif 66 | .fixedSize() 67 | 68 | Menu { 69 | Toggle(isOn: $roomCtx.simulcast) { 70 | Text("Simulcast") 71 | } 72 | Toggle(isOn: $roomCtx.adaptiveStream) { 73 | Text("AdaptiveStream") 74 | } 75 | Toggle(isOn: $roomCtx.dynacast) { 76 | Text("Dynacast") 77 | } 78 | Toggle(isOn: $roomCtx.reportStats) { 79 | Text("Report stats") 80 | } 81 | } label: { 82 | Image(systemSymbol: .gear) 83 | .renderingMode(.original) 84 | Text("Room Options") 85 | } 86 | #if os(macOS) 87 | .menuStyle(BorderlessButtonMenuStyle(showsMenuIndicator: true)) 88 | #elseif os(iOS) 89 | .menuStyle(BorderlessButtonMenuStyle()) 90 | #endif 91 | .fixedSize() 92 | } 93 | }.frame(maxWidth: 350) 94 | 95 | if case .connecting = room.connectionState { 96 | HStack(alignment: .center) { 97 | ProgressView() 98 | 99 | LKButton(title: "Cancel") { 100 | roomCtx.cancelConnect() 101 | } 102 | } 103 | } else { 104 | HStack(alignment: .center) { 105 | Spacer() 106 | 107 | LKButton(title: "Connect") { 108 | Task.detached { @MainActor in 109 | let room = try await roomCtx.connect() 110 | appCtx.connectionHistory.update(room: room, e2ee: roomCtx.isE2eeEnabled, e2eeKey: roomCtx.e2eeKey) 111 | } 112 | } 113 | 114 | if !appCtx.connectionHistory.isEmpty { 115 | Menu { 116 | ForEach(appCtx.connectionHistory.sortedByUpdated) { entry in 117 | Button { 118 | Task.detached { @MainActor in 119 | let room = try await roomCtx.connect(entry: entry) 120 | appCtx.connectionHistory.update(room: room, e2ee: roomCtx.isE2eeEnabled, e2eeKey: roomCtx.e2eeKey) 121 | } 122 | } label: { 123 | Image(systemSymbol: .boltFill) 124 | .renderingMode(.original) 125 | Text(String(describing: entry)) 126 | } 127 | } 128 | 129 | Divider() 130 | 131 | Button { 132 | appCtx.connectionHistory.removeAll() 133 | } label: { 134 | Image(systemSymbol: .xmarkCircleFill) 135 | .renderingMode(.original) 136 | Text("Clear history") 137 | } 138 | 139 | } label: { 140 | Image(systemSymbol: .clockFill) 141 | .renderingMode(.original) 142 | Text("Recent") 143 | } 144 | #if os(macOS) 145 | .menuStyle(BorderlessButtonMenuStyle(showsMenuIndicator: true)) 146 | #elseif os(iOS) 147 | .menuStyle(BorderlessButtonMenuStyle()) 148 | #endif 149 | .fixedSize() 150 | } 151 | 152 | Spacer() 153 | } 154 | } 155 | } 156 | .padding() 157 | .frame(width: geometry.size.width) // Make the scroll view full-width 158 | .frame(minHeight: geometry.size.height) // Set the content’s min height to the parent 159 | } 160 | } 161 | #if os(macOS) 162 | .frame(minWidth: 500, minHeight: 500) 163 | #endif 164 | .alert(isPresented: $roomCtx.shouldShowDisconnectReason) { 165 | Alert(title: Text("Disconnected"), 166 | message: Text("Reason: " + String(describing: roomCtx.latestError))) 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /Shared/Controllers/RoomContext.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 LiveKit 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import LiveKit 18 | import SwiftUI 19 | 20 | // This class contains the logic to control behavior of the whole app. 21 | final class RoomContext: ObservableObject { 22 | let jsonEncoder = JSONEncoder() 23 | let jsonDecoder = JSONDecoder() 24 | 25 | private let store: ValueStore 26 | 27 | // Used to show connection error dialog 28 | // private var didClose: Bool = false 29 | @Published var shouldShowDisconnectReason: Bool = false 30 | public var latestError: LiveKitError? 31 | 32 | public let room = Room() 33 | 34 | @Published var url: String = "" { 35 | didSet { store.value.url = url } 36 | } 37 | 38 | @Published var token: String = "" { 39 | didSet { store.value.token = token } 40 | } 41 | 42 | @Published var e2eeKey: String = "" { 43 | didSet { store.value.e2eeKey = e2eeKey } 44 | } 45 | 46 | @Published var isE2eeEnabled: Bool = false { 47 | didSet { 48 | store.value.isE2eeEnabled = isE2eeEnabled 49 | // room.set(isE2eeEnabled: isE2eeEnabled) 50 | } 51 | } 52 | 53 | // RoomOptions 54 | @Published var simulcast: Bool = true { 55 | didSet { store.value.simulcast = simulcast } 56 | } 57 | 58 | @Published var adaptiveStream: Bool = false { 59 | didSet { store.value.adaptiveStream = adaptiveStream } 60 | } 61 | 62 | @Published var dynacast: Bool = false { 63 | didSet { store.value.dynacast = dynacast } 64 | } 65 | 66 | @Published var reportStats: Bool = false { 67 | didSet { store.value.reportStats = reportStats } 68 | } 69 | 70 | // ConnectOptions 71 | @Published var autoSubscribe: Bool = true { 72 | didSet { store.value.autoSubscribe = autoSubscribe } 73 | } 74 | 75 | @Published var focusParticipant: Participant? 76 | 77 | @Published var showMessagesView: Bool = false 78 | @Published var messages: [ExampleRoomMessage] = [] 79 | 80 | @Published var textFieldString: String = "" 81 | 82 | var _connectTask: Task? 83 | 84 | public init(store: ValueStore) { 85 | self.store = store 86 | room.add(delegate: self) 87 | 88 | url = store.value.url 89 | token = store.value.token 90 | isE2eeEnabled = store.value.isE2eeEnabled 91 | e2eeKey = store.value.e2eeKey 92 | simulcast = store.value.simulcast 93 | adaptiveStream = store.value.adaptiveStream 94 | dynacast = store.value.dynacast 95 | reportStats = store.value.reportStats 96 | autoSubscribe = store.value.autoSubscribe 97 | 98 | #if os(iOS) 99 | UIApplication.shared.isIdleTimerDisabled = true 100 | #endif 101 | } 102 | 103 | deinit { 104 | #if os(iOS) 105 | UIApplication.shared.isIdleTimerDisabled = false 106 | #endif 107 | print("RoomContext.deinit") 108 | } 109 | 110 | func cancelConnect() { 111 | _connectTask?.cancel() 112 | } 113 | 114 | @MainActor 115 | func connect(entry: ConnectionHistory? = nil) async throws -> Room { 116 | if let entry { 117 | url = entry.url 118 | token = entry.token 119 | isE2eeEnabled = entry.e2ee 120 | e2eeKey = entry.e2eeKey 121 | } 122 | 123 | let connectOptions = ConnectOptions( 124 | autoSubscribe: autoSubscribe 125 | ) 126 | 127 | var e2eeOptions: E2EEOptions? = nil 128 | if isE2eeEnabled { 129 | let keyProvider = BaseKeyProvider(isSharedKey: true) 130 | keyProvider.setKey(key: e2eeKey) 131 | e2eeOptions = E2EEOptions(keyProvider: keyProvider) 132 | } 133 | 134 | let roomOptions = RoomOptions( 135 | defaultCameraCaptureOptions: CameraCaptureOptions( 136 | dimensions: .h1080_169 137 | ), 138 | defaultScreenShareCaptureOptions: ScreenShareCaptureOptions( 139 | dimensions: .h1080_169, 140 | useBroadcastExtension: true 141 | ), 142 | defaultVideoPublishOptions: VideoPublishOptions( 143 | simulcast: simulcast 144 | ), 145 | adaptiveStream: true, 146 | dynacast: true, 147 | // isE2eeEnabled: isE2eeEnabled, 148 | e2eeOptions: e2eeOptions, 149 | reportRemoteTrackStatistics: true 150 | ) 151 | 152 | let connectTask = Task.detached { [weak self] in 153 | guard let self else { return } 154 | try await self.room.connect(url: self.url, 155 | token: self.token, 156 | connectOptions: connectOptions, 157 | roomOptions: roomOptions) 158 | } 159 | 160 | _connectTask = connectTask 161 | try await connectTask.value 162 | 163 | return room 164 | } 165 | 166 | func disconnect() async { 167 | await room.disconnect() 168 | } 169 | 170 | func sendMessage() { 171 | // Make sure the message is not empty 172 | guard !textFieldString.isEmpty else { return } 173 | 174 | let roomMessage = ExampleRoomMessage(messageId: UUID().uuidString, 175 | senderSid: room.localParticipant.sid, 176 | senderIdentity: room.localParticipant.identity, 177 | text: textFieldString) 178 | textFieldString = "" 179 | messages.append(roomMessage) 180 | 181 | Task.detached { [weak self] in 182 | guard let self else { return } 183 | do { 184 | let json = try self.jsonEncoder.encode(roomMessage) 185 | try await self.room.localParticipant.publish(data: json) 186 | } catch { 187 | print("Failed to encode data \(error)") 188 | } 189 | } 190 | } 191 | 192 | #if os(macOS) 193 | weak var screenShareTrack: LocalTrackPublication? 194 | 195 | @available(macOS 12.3, *) 196 | func setScreenShareMacOS(isEnabled: Bool, screenShareSource: MacOSScreenCaptureSource? = nil) async throws { 197 | if isEnabled, let screenShareSource { 198 | let track = LocalVideoTrack.createMacOSScreenShareTrack(source: screenShareSource) 199 | let options = VideoPublishOptions(preferredCodec: VideoCodec.h264) 200 | screenShareTrack = try await room.localParticipant.publish(videoTrack: track, options: options) 201 | } 202 | 203 | if !isEnabled, let screenShareTrack { 204 | try await room.localParticipant.unpublish(publication: screenShareTrack) 205 | } 206 | } 207 | #endif 208 | } 209 | 210 | extension RoomContext: RoomDelegate { 211 | func room(_: Room, track publication: TrackPublication, didUpdateE2EEState e2eeState: E2EEState) { 212 | print("Did update e2eeState = [\(String(describing: e2eeState))] for publication \(publication.sid)") 213 | } 214 | 215 | func room(_ room: Room, didUpdateConnectionState connectionState: ConnectionState, from oldValue: ConnectionState) { 216 | print("Did update connectionState \(oldValue) -> \(connectionState)") 217 | 218 | if case .disconnected = connectionState, 219 | let error = room.disconnectError, 220 | error.type != .cancelled 221 | { 222 | latestError = room.disconnectError 223 | 224 | Task.detached { @MainActor [weak self] in 225 | guard let self else { return } 226 | self.shouldShowDisconnectReason = true 227 | // Reset state 228 | self.focusParticipant = nil 229 | self.showMessagesView = false 230 | self.textFieldString = "" 231 | self.messages.removeAll() 232 | // self.objectWillChange.send() 233 | } 234 | } 235 | } 236 | 237 | func room(_: Room, participantDidDisconnect participant: RemoteParticipant) { 238 | Task.detached { @MainActor [weak self] in 239 | guard let self else { return } 240 | if let focusParticipant = self.focusParticipant, focusParticipant.identity == participant.identity { 241 | self.focusParticipant = nil 242 | } 243 | } 244 | } 245 | 246 | func room(_: Room, participant _: RemoteParticipant?, didReceiveData data: Data, forTopic _: String) { 247 | do { 248 | let roomMessage = try jsonDecoder.decode(ExampleRoomMessage.self, from: data) 249 | // Update UI from main queue 250 | Task.detached { @MainActor [weak self] in 251 | guard let self else { return } 252 | 253 | withAnimation { 254 | // Add messages to the @Published messages property 255 | // which will trigger the UI to update 256 | self.messages.append(roomMessage) 257 | // Show the messages view when new messages arrive 258 | self.showMessagesView = true 259 | } 260 | } 261 | 262 | } catch { 263 | print("Failed to decode data \(error)") 264 | } 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /Shared/ParticipantView.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 LiveKit 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import LiveKit 18 | import SFSafeSymbols 19 | import SwiftUI 20 | 21 | struct ParticipantView: View { 22 | @ObservedObject var participant: Participant 23 | @EnvironmentObject var appCtx: AppContext 24 | 25 | var videoViewMode: VideoView.LayoutMode = .fill 26 | var onTap: ((_ participant: Participant) -> Void)? 27 | 28 | @State private var isRendering: Bool = false 29 | 30 | func bgView(systemSymbol: SFSymbol, geometry: GeometryProxy) -> some View { 31 | Image(systemSymbol: systemSymbol) 32 | .resizable() 33 | .aspectRatio(contentMode: .fit) 34 | .foregroundColor(Color.lkGray2) 35 | .frame(width: min(geometry.size.width, geometry.size.height) * 0.3) 36 | .frame( 37 | maxWidth: .infinity, 38 | maxHeight: .infinity 39 | ) 40 | } 41 | 42 | var body: some View { 43 | GeometryReader { geometry in 44 | 45 | ZStack(alignment: .bottom) { 46 | // Background color 47 | Color.lkGray1 48 | .ignoresSafeArea() 49 | 50 | // VideoView for the Participant 51 | if let publication = participant.mainVideoPublication, 52 | !publication.isMuted, 53 | let track = publication.track as? VideoTrack, 54 | appCtx.videoViewVisible 55 | { 56 | ZStack(alignment: .topLeading) { 57 | SwiftUIVideoView(track, 58 | layoutMode: videoViewMode, 59 | mirrorMode: appCtx.videoViewMirrored ? .mirror : .auto, 60 | renderMode: appCtx.preferSampleBufferRendering ? .sampleBuffer : .auto, 61 | pinchToZoomOptions: appCtx.videoViewPinchToZoomOptions, 62 | isDebugMode: appCtx.showInformationOverlay, 63 | isRendering: $isRendering) 64 | 65 | if !isRendering { 66 | ProgressView().progressViewStyle(CircularProgressViewStyle()) 67 | .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) 68 | } 69 | } 70 | } else if let publication = participant.mainVideoPublication as? RemoteTrackPublication, 71 | case .notAllowed = publication.subscriptionState 72 | { 73 | // Show no permission icon 74 | bgView(systemSymbol: .exclamationmarkCircle, geometry: geometry) 75 | } else { 76 | // Show no camera icon 77 | bgView(systemSymbol: .videoSlashFill, geometry: geometry) 78 | } 79 | 80 | if appCtx.showInformationOverlay { 81 | VStack(alignment: .leading, spacing: 5) { 82 | // Video stats 83 | if let publication = participant.mainVideoPublication, 84 | !publication.isMuted, 85 | let track = publication.track as? VideoTrack 86 | { 87 | StatsView(track: track) 88 | } 89 | // Audio stats 90 | if let publication = participant.firstAudioPublication, 91 | !publication.isMuted, 92 | let track = publication.track as? AudioTrack 93 | { 94 | StatsView(track: track) 95 | } 96 | } 97 | .padding(8) 98 | .frame( 99 | minWidth: 0, 100 | maxWidth: .infinity, 101 | minHeight: 0, 102 | maxHeight: .infinity, 103 | alignment: .topLeading 104 | ) 105 | } 106 | 107 | VStack(alignment: .trailing, spacing: 0) { 108 | // Show the sub-video view 109 | if let subVideoTrack = participant.subVideoTrack { 110 | SwiftUIVideoView(subVideoTrack, 111 | layoutMode: .fill, 112 | mirrorMode: appCtx.videoViewMirrored ? .mirror : .auto) 113 | .background(Color.black) 114 | .aspectRatio(contentMode: .fit) 115 | .frame(width: min(geometry.size.width, geometry.size.height) * 0.3) 116 | .cornerRadius(8) 117 | .padding() 118 | } 119 | 120 | // Bottom user info bar 121 | HStack { 122 | if let identity = participant.identity { 123 | Text(String(describing: identity)) 124 | .lineLimit(1) 125 | .truncationMode(.tail) 126 | } 127 | 128 | if let publication = participant.mainVideoPublication, 129 | !publication.isMuted 130 | { 131 | // is remote 132 | if let remotePub = publication as? RemoteTrackPublication { 133 | Menu { 134 | if case .subscribed = remotePub.subscriptionState { 135 | Button { 136 | Task { 137 | try await remotePub.set(subscribed: false) 138 | } 139 | } label: { 140 | Text("Unsubscribe") 141 | } 142 | } else if case .unsubscribed = remotePub.subscriptionState { 143 | Button { 144 | Task { 145 | try await remotePub.set(subscribed: true) 146 | } 147 | } label: { 148 | Text("Subscribe") 149 | } 150 | } 151 | } label: { 152 | if case .subscribed = remotePub.subscriptionState { 153 | Image(systemSymbol: .videoFill) 154 | .foregroundColor(Color.green) 155 | } else if case .notAllowed = remotePub.subscriptionState { 156 | Image(systemSymbol: .exclamationmarkCircle) 157 | .foregroundColor(Color.red) 158 | } else { 159 | Image(systemSymbol: .videoSlashFill) 160 | } 161 | } 162 | #if os(macOS) 163 | .menuStyle(BorderlessButtonMenuStyle(showsMenuIndicator: true)) 164 | #elseif os(iOS) 165 | .menuStyle(BorderlessButtonMenuStyle()) 166 | #endif 167 | .fixedSize() 168 | } else { 169 | // local 170 | Image(systemSymbol: .videoFill) 171 | .foregroundColor(Color.green) 172 | } 173 | 174 | } else { 175 | Image(systemSymbol: .videoSlashFill) 176 | .foregroundColor(Color.white) 177 | } 178 | 179 | if let publication = participant.firstAudioPublication, 180 | !publication.isMuted 181 | { 182 | // is remote 183 | if let remotePub = publication as? RemoteTrackPublication { 184 | Menu { 185 | if case .subscribed = remotePub.subscriptionState { 186 | Button { 187 | Task { 188 | try await remotePub.set(subscribed: false) 189 | } 190 | } label: { 191 | Text("Unsubscribe") 192 | } 193 | } else if case .unsubscribed = remotePub.subscriptionState { 194 | Button { 195 | Task { 196 | try await remotePub.set(subscribed: true) 197 | } 198 | } label: { 199 | Text("Subscribe") 200 | } 201 | } 202 | } label: { 203 | if case .subscribed = remotePub.subscriptionState { 204 | Image(systemSymbol: .micFill) 205 | .foregroundColor(Color.orange) 206 | } else if case .notAllowed = remotePub.subscriptionState { 207 | Image(systemSymbol: .exclamationmarkCircle) 208 | .foregroundColor(Color.red) 209 | } else { 210 | Image(systemSymbol: .micSlashFill) 211 | } 212 | } 213 | #if os(macOS) 214 | .menuStyle(BorderlessButtonMenuStyle(showsMenuIndicator: true)) 215 | #elseif os(iOS) 216 | .menuStyle(BorderlessButtonMenuStyle()) 217 | #endif 218 | .fixedSize() 219 | } else { 220 | // local 221 | Image(systemSymbol: .micFill) 222 | .foregroundColor(Color.orange) 223 | } 224 | 225 | } else { 226 | Image(systemSymbol: .micSlashFill) 227 | .foregroundColor(Color.white) 228 | } 229 | 230 | if participant.connectionQuality == .excellent { 231 | Image(systemSymbol: .wifi) 232 | .foregroundColor(.green) 233 | } else if participant.connectionQuality == .good { 234 | Image(systemSymbol: .wifi) 235 | .foregroundColor(Color.orange) 236 | } else if participant.connectionQuality == .poor { 237 | Image(systemSymbol: .wifiExclamationmark) 238 | .foregroundColor(Color.red) 239 | } 240 | 241 | if participant.firstTrackEncryptionType == .none { 242 | Image(systemSymbol: .lockOpenFill) 243 | .foregroundColor(.red) 244 | } else { 245 | Image(systemSymbol: .lockFill) 246 | .foregroundColor(.green) 247 | } 248 | 249 | }.padding(5) 250 | .frame(minWidth: 0, maxWidth: .infinity) 251 | .background(Color.black.opacity(0.5)) 252 | } 253 | } 254 | .cornerRadius(8) 255 | // Glow the border when the participant is speaking 256 | .overlay( 257 | participant.isSpeaking ? 258 | RoundedRectangle(cornerRadius: 5) 259 | .stroke(Color.blue, lineWidth: 5.0) 260 | : nil 261 | ) 262 | }.gesture(TapGesture() 263 | .onEnded { _ in 264 | // Pass the tap event 265 | onTap?(participant) 266 | }) 267 | } 268 | } 269 | 270 | struct StatsView: View { 271 | private let track: Track 272 | @ObservedObject private var observer: TrackDelegateObserver 273 | 274 | init(track: Track) { 275 | self.track = track 276 | observer = TrackDelegateObserver(track: track) 277 | } 278 | 279 | var body: some View { 280 | HStack(alignment: .top, spacing: 5) { 281 | VStack(alignment: .leading, spacing: 5) { 282 | if track is VideoTrack { 283 | HStack(spacing: 3) { 284 | Image(systemSymbol: .videoFill) 285 | Text("Video").fontWeight(.bold) 286 | if let dimensions = observer.dimensions { 287 | Text("\(dimensions.width)×\(dimensions.height)") 288 | } 289 | } 290 | } else if track is AudioTrack { 291 | HStack(spacing: 3) { 292 | Image(systemSymbol: .micFill) 293 | Text("Audio").fontWeight(.bold) 294 | } 295 | } else { 296 | Text("Unknown").fontWeight(.bold) 297 | } 298 | 299 | // if let trackStats = viewModel.statistics { 300 | ForEach(observer.allStatisticts, id: \.self) { trackStats in 301 | ForEach(trackStats.outboundRtpStream.sortedByRidIndex()) { stream in 302 | 303 | HStack(spacing: 3) { 304 | Image(systemSymbol: .arrowUp) 305 | 306 | if let codec = trackStats.codec.first(where: { $0.id == stream.codecId }) { 307 | Text(codec.mimeType ?? "?") 308 | } 309 | 310 | if let rid = stream.rid, !rid.isEmpty { 311 | Text(rid.uppercased()) 312 | } 313 | 314 | Text(stream.formattedBps()) 315 | 316 | if let reason = stream.qualityLimitationReason, reason != QualityLimitationReason.none { 317 | Image(systemSymbol: .exclamationmarkTriangleFill) 318 | Text(reason.rawValue.capitalized) 319 | } 320 | } 321 | } 322 | ForEach(trackStats.inboundRtpStream) { stream in 323 | 324 | HStack(spacing: 3) { 325 | Image(systemSymbol: .arrowDown) 326 | 327 | if let codec = trackStats.codec.first(where: { $0.id == stream.codecId }) { 328 | Text(codec.mimeType ?? "?") 329 | } 330 | 331 | Text(stream.formattedBps()) 332 | } 333 | } 334 | } 335 | } 336 | .font(.system(size: 10)) 337 | .foregroundColor(Color.white) 338 | .padding(5) 339 | .background(Color.black.opacity(0.5)) 340 | .cornerRadius(8) 341 | } 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /Shared/RoomView.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 LiveKit 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import LiveKit 18 | import SFSafeSymbols 19 | import SwiftUI 20 | 21 | #if !os(macOS) 22 | let adaptiveMin = 170.0 23 | let toolbarPlacement: ToolbarItemPlacement = .bottomBar 24 | #else 25 | let adaptiveMin = 300.0 26 | let toolbarPlacement: ToolbarItemPlacement = .primaryAction 27 | #endif 28 | 29 | extension CIImage { 30 | // helper to create a `CIImage` for both platforms 31 | convenience init(named name: String) { 32 | #if !os(macOS) 33 | self.init(cgImage: UIImage(named: name)!.cgImage!) 34 | #else 35 | self.init(data: NSImage(named: name)!.tiffRepresentation!)! 36 | #endif 37 | } 38 | } 39 | 40 | #if os(macOS) 41 | // keeps weak reference to NSWindow 42 | class WindowAccess: ObservableObject { 43 | private weak var window: NSWindow? 44 | 45 | deinit { 46 | // reset changed properties 47 | DispatchQueue.main.async { [weak window] in 48 | window?.level = .normal 49 | } 50 | } 51 | 52 | @Published public var pinned: Bool = false { 53 | didSet { 54 | guard oldValue != pinned else { return } 55 | level = pinned ? .floating : .normal 56 | } 57 | } 58 | 59 | private var level: NSWindow.Level { 60 | get { window?.level ?? .normal } 61 | set { 62 | Task { @MainActor in 63 | window?.level = newValue 64 | objectWillChange.send() 65 | } 66 | } 67 | } 68 | 69 | public func set(window: NSWindow?) { 70 | self.window = window 71 | Task { @MainActor in 72 | objectWillChange.send() 73 | } 74 | } 75 | } 76 | #endif 77 | 78 | struct RoomView: View { 79 | @EnvironmentObject var appCtx: AppContext 80 | @EnvironmentObject var roomCtx: RoomContext 81 | @EnvironmentObject var room: Room 82 | 83 | @State var isCameraPublishingBusy = false 84 | @State var isMicrophonePublishingBusy = false 85 | @State var isScreenSharePublishingBusy = false 86 | 87 | @State private var screenPickerPresented = false 88 | @State private var publishOptionsPickerPresented = false 89 | 90 | @State private var cameraPublishOptions = VideoPublishOptions() 91 | 92 | #if os(macOS) 93 | @ObservedObject private var windowAccess = WindowAccess() 94 | #endif 95 | 96 | @State private var showConnectionTime = true 97 | @State private var canSwitchCameraPosition = false 98 | 99 | func messageView(_ message: ExampleRoomMessage) -> some View { 100 | let isMe = message.senderSid == room.localParticipant.sid 101 | 102 | return HStack { 103 | if isMe { 104 | Spacer() 105 | } 106 | 107 | // VStack(alignment: isMe ? .trailing : .leading) { 108 | // Text(message.identity) 109 | Text(message.text) 110 | .padding(8) 111 | .background(isMe ? Color.lkRed : Color.lkGray3) 112 | .foregroundColor(Color.white) 113 | .cornerRadius(18) 114 | // } 115 | if !isMe { 116 | Spacer() 117 | } 118 | }.padding(.vertical, 5) 119 | .padding(.horizontal, 10) 120 | } 121 | 122 | func scrollToBottom(_ scrollView: ScrollViewProxy) { 123 | guard let last = roomCtx.messages.last else { return } 124 | withAnimation { 125 | scrollView.scrollTo(last.id) 126 | } 127 | } 128 | 129 | func messagesView(geometry: GeometryProxy) -> some View { 130 | VStack(spacing: 0) { 131 | ScrollViewReader { scrollView in 132 | ScrollView(.vertical, showsIndicators: true) { 133 | LazyVStack(alignment: .center, spacing: 0) { 134 | ForEach(roomCtx.messages) { 135 | messageView($0) 136 | } 137 | } 138 | .padding(.vertical, 12) 139 | .padding(.horizontal, 7) 140 | } 141 | .onAppear(perform: { 142 | // Scroll to bottom when first showing the messages list 143 | scrollToBottom(scrollView) 144 | }) 145 | .onChange(of: roomCtx.messages, perform: { _ in 146 | // Scroll to bottom when there is a new message 147 | scrollToBottom(scrollView) 148 | }) 149 | .frame( 150 | minWidth: 0, 151 | maxWidth: .infinity, 152 | minHeight: 0, 153 | maxHeight: .infinity, 154 | alignment: .topLeading 155 | ) 156 | } 157 | HStack(spacing: 0) { 158 | TextField("Enter message", text: $roomCtx.textFieldString) 159 | .textFieldStyle(PlainTextFieldStyle()) 160 | .disableAutocorrection(true) 161 | // TODO: add iOS unique view modifiers 162 | // #if os(iOS) 163 | // .autocapitalization(.none) 164 | // .keyboardType(type.toiOSType()) 165 | // #endif 166 | 167 | // .overlay(RoundedRectangle(cornerRadius: 10.0) 168 | // .strokeBorder(Color.white.opacity(0.3), 169 | // style: StrokeStyle(lineWidth: 1.0))) 170 | 171 | Button { 172 | roomCtx.sendMessage() 173 | } label: { 174 | Image(systemSymbol: .paperplaneFill) 175 | .foregroundColor(roomCtx.textFieldString.isEmpty ? nil : Color.lkRed) 176 | } 177 | .buttonStyle(.borderless) 178 | } 179 | .padding() 180 | .background(Color.lkGray2) 181 | } 182 | .background(Color.lkGray1) 183 | .cornerRadius(8) 184 | .frame( 185 | minWidth: 0, 186 | maxWidth: geometry.isTall ? .infinity : 320 187 | ) 188 | } 189 | 190 | func sortedParticipants() -> [Participant] { 191 | room.allParticipants.values.sorted { p1, p2 in 192 | if p1 is LocalParticipant { return true } 193 | if p2 is LocalParticipant { return false } 194 | return (p1.joinedAt ?? Date()) < (p2.joinedAt ?? Date()) 195 | } 196 | } 197 | 198 | func content(geometry: GeometryProxy) -> some View { 199 | VStack { 200 | if showConnectionTime { 201 | Text("Connected (\([room.serverRegion, room.serverNodeId, "\(String(describing: room.connectStopwatch.total().rounded(to: 2)))s"].compactMap { $0 }.joined(separator: ", ")))") 202 | .multilineTextAlignment(.center) 203 | .foregroundColor(.white) 204 | .padding() 205 | } 206 | 207 | if case .connecting = room.connectionState { 208 | Text("Re-connecting...") 209 | .multilineTextAlignment(.center) 210 | .foregroundColor(.white) 211 | .padding() 212 | } 213 | 214 | HorVStack(axis: geometry.isTall ? .vertical : .horizontal, spacing: 5) { 215 | Group { 216 | if let focusParticipant = roomCtx.focusParticipant { 217 | ZStack(alignment: .bottomTrailing) { 218 | ParticipantView(participant: focusParticipant, 219 | videoViewMode: appCtx.videoViewMode) 220 | { _ in 221 | roomCtx.focusParticipant = nil 222 | } 223 | .overlay(RoundedRectangle(cornerRadius: 5) 224 | .stroke(Color.lkRed.opacity(0.7), lineWidth: 5.0)) 225 | Text("SELECTED") 226 | .font(.system(size: 10)) 227 | .fontWeight(.bold) 228 | .foregroundColor(Color.white) 229 | .padding(.horizontal, 5) 230 | .padding(.vertical, 2) 231 | .background(Color.lkRed.opacity(0.7)) 232 | .cornerRadius(8) 233 | .padding(.vertical, 35) 234 | .padding(.horizontal, 10) 235 | } 236 | 237 | } else { 238 | // Array([room.allParticipants.values, room.allParticipants.values].joined()) 239 | ParticipantLayout(sortedParticipants(), spacing: 5) { participant in 240 | ParticipantView(participant: participant, 241 | videoViewMode: appCtx.videoViewMode) 242 | { participant in 243 | roomCtx.focusParticipant = participant 244 | } 245 | } 246 | } 247 | } 248 | .frame( 249 | minWidth: 0, 250 | maxWidth: .infinity, 251 | minHeight: 0, 252 | maxHeight: .infinity 253 | ) 254 | // Show messages view if enabled 255 | if roomCtx.showMessagesView { 256 | messagesView(geometry: geometry) 257 | } 258 | } 259 | } 260 | .padding(5) 261 | } 262 | 263 | var body: some View { 264 | GeometryReader { geometry in 265 | content(geometry: geometry) 266 | } 267 | .toolbar { 268 | ToolbarItemGroup(placement: toolbarPlacement) { 269 | // Insufficient space on iOS bar 270 | #if os(macOS) 271 | Group { 272 | if let name = room.name { 273 | Text(name) 274 | .fontWeight(.bold) 275 | } 276 | 277 | if let identity = room.localParticipant.identity { 278 | Text(String(describing: identity)) 279 | } 280 | 281 | Spacer() 282 | 283 | Picker("Mode", selection: $appCtx.videoViewMode) { 284 | Text("Fit").tag(VideoView.LayoutMode.fit) 285 | Text("Fill").tag(VideoView.LayoutMode.fill) 286 | } 287 | .pickerStyle(SegmentedPickerStyle()) 288 | } 289 | #else 290 | Spacer() 291 | #endif 292 | 293 | Group { 294 | let isCameraEnabled = room.localParticipant.isCameraEnabled() 295 | let isMicrophoneEnabled = room.localParticipant.isMicrophoneEnabled() 296 | let isScreenShareEnabled = room.localParticipant.isScreenShareEnabled() 297 | 298 | Group { 299 | if isCameraEnabled, canSwitchCameraPosition { 300 | Menu { 301 | Button("Switch position") { 302 | Task { 303 | isCameraPublishingBusy = true 304 | defer { Task { @MainActor in isCameraPublishingBusy = false } } 305 | if let track = room.localParticipant.firstCameraVideoTrack as? LocalVideoTrack, 306 | let cameraCapturer = track.capturer as? CameraCapturer 307 | { 308 | try await cameraCapturer.switchCameraPosition() 309 | } 310 | } 311 | } 312 | 313 | Button("Disable") { 314 | Task { 315 | isCameraPublishingBusy = true 316 | defer { Task { @MainActor in isCameraPublishingBusy = false } } 317 | try await room.localParticipant.setCamera(enabled: !isCameraEnabled) 318 | } 319 | } 320 | } label: { 321 | Image(systemSymbol: .videoFill) 322 | .renderingMode(.original) 323 | } 324 | // disable while publishing/un-publishing 325 | .disabled(isCameraPublishingBusy) 326 | } else { 327 | // Toggle camera enabled 328 | Button(action: { 329 | if isCameraEnabled { 330 | Task { 331 | isCameraPublishingBusy = true 332 | defer { Task { @MainActor in isCameraPublishingBusy = false } } 333 | try await room.localParticipant.setCamera(enabled: false) 334 | } 335 | } else { 336 | publishOptionsPickerPresented = true 337 | } 338 | }, 339 | label: { 340 | Image(systemSymbol: .videoFill) 341 | .renderingMode(isCameraEnabled ? .original : .template) 342 | }) 343 | // disable while publishing/un-publishing 344 | .disabled(isCameraPublishingBusy) 345 | } 346 | }.popover(isPresented: $publishOptionsPickerPresented) { 347 | PublishOptionsView(publishOptions: cameraPublishOptions) { captureOptions, publishOptions in 348 | publishOptionsPickerPresented = false 349 | isCameraPublishingBusy = true 350 | cameraPublishOptions = publishOptions 351 | Task { 352 | defer { Task { @MainActor in isCameraPublishingBusy = false } } 353 | try await room.localParticipant.setCamera(enabled: true, 354 | captureOptions: captureOptions, 355 | publishOptions: publishOptions) 356 | } 357 | } 358 | .padding() 359 | } 360 | 361 | // Toggle microphone enabled 362 | Button(action: { 363 | Task { 364 | isMicrophonePublishingBusy = true 365 | defer { Task { @MainActor in isMicrophonePublishingBusy = false } } 366 | try await room.localParticipant.setMicrophone(enabled: !isMicrophoneEnabled) 367 | } 368 | }, 369 | label: { 370 | Image(systemSymbol: .micFill) 371 | .renderingMode(isMicrophoneEnabled ? .original : .template) 372 | }) 373 | // disable while publishing/un-publishing 374 | .disabled(isMicrophonePublishingBusy) 375 | 376 | #if os(iOS) 377 | Button(action: { 378 | Task { 379 | isScreenSharePublishingBusy = true 380 | defer { Task { @MainActor in isScreenSharePublishingBusy = false } } 381 | try await room.localParticipant.setScreenShare(enabled: !isScreenShareEnabled) 382 | } 383 | }, 384 | label: { 385 | Image(systemSymbol: .rectangleFillOnRectangleFill) 386 | .renderingMode(isScreenShareEnabled ? .original : .template) 387 | }) 388 | // disable while publishing/un-publishing 389 | .disabled(isScreenSharePublishingBusy) 390 | #elseif os(macOS) 391 | Button(action: { 392 | if #available(macOS 12.3, *) { 393 | if isScreenShareEnabled { 394 | // Turn off screen share 395 | Task { 396 | isScreenSharePublishingBusy = true 397 | defer { Task { @MainActor in isScreenSharePublishingBusy = false } } 398 | try await roomCtx.setScreenShareMacOS(isEnabled: false) 399 | } 400 | } else { 401 | screenPickerPresented = true 402 | } 403 | } 404 | }, 405 | label: { 406 | Image(systemSymbol: .rectangleFillOnRectangleFill) 407 | .renderingMode(isScreenShareEnabled ? .original : .template) 408 | .foregroundColor(isScreenShareEnabled ? Color.green : Color.white) 409 | }).popover(isPresented: $screenPickerPresented) { 410 | if #available(macOS 12.3, *) { 411 | ScreenShareSourcePickerView { source in 412 | Task { 413 | isScreenSharePublishingBusy = true 414 | defer { Task { @MainActor in isScreenSharePublishingBusy = false } } 415 | try await roomCtx.setScreenShareMacOS(isEnabled: true, screenShareSource: source) 416 | } 417 | screenPickerPresented = false 418 | }.padding() 419 | } 420 | } 421 | .disabled(isScreenSharePublishingBusy) 422 | #endif 423 | 424 | // Toggle messages view (chat example) 425 | Button(action: { 426 | withAnimation { 427 | roomCtx.showMessagesView.toggle() 428 | } 429 | }, 430 | label: { 431 | Image(systemSymbol: .messageFill) 432 | .renderingMode(roomCtx.showMessagesView ? .original : .template) 433 | }) 434 | } 435 | 436 | // Spacer() 437 | 438 | #if os(iOS) 439 | SwiftUIAudioRoutePickerButton() 440 | #endif 441 | 442 | Menu { 443 | #if os(macOS) 444 | Button { 445 | if let url = URL(string: "livekit://") { 446 | NSWorkspace.shared.open(url) 447 | } 448 | } label: { 449 | Text("New window") 450 | } 451 | 452 | Divider() 453 | 454 | #endif 455 | 456 | Toggle("Show info overlay", isOn: $appCtx.showInformationOverlay) 457 | 458 | Group { 459 | Toggle("VideoView visible", isOn: $appCtx.videoViewVisible) 460 | Toggle("VideoView flip", isOn: $appCtx.videoViewMirrored) 461 | Toggle("VideoView renderMode: .sampleBuffer", isOn: $appCtx.preferSampleBufferRendering) 462 | #if os(iOS) 463 | Menu("Pinch to zoom") { 464 | Toggle("Zoom In", isOn: $appCtx.videoViewPinchToZoomOptions.bind(.zoomIn)) 465 | Toggle("Zoom Out", isOn: $appCtx.videoViewPinchToZoomOptions.bind(.zoomOut)) 466 | Toggle("Auto Reset", isOn: $appCtx.videoViewPinchToZoomOptions.bind(.resetOnRelease)) 467 | } 468 | #endif 469 | Divider() 470 | } 471 | 472 | #if os(macOS) 473 | 474 | Group { 475 | Picker("Output device", selection: $appCtx.outputDevice) { 476 | ForEach(AudioManager.shared.outputDevices) { device in 477 | Text(device.isDefault ? "Default" : "\(device.name)").tag(device) 478 | } 479 | } 480 | Picker("Input device", selection: $appCtx.inputDevice) { 481 | ForEach(AudioManager.shared.inputDevices) { device in 482 | Text(device.isDefault ? "Default" : "\(device.name)").tag(device) 483 | } 484 | } 485 | } 486 | #endif 487 | 488 | Group { 489 | Divider() 490 | 491 | Button { 492 | Task { 493 | await room.localParticipant.unpublishAll() 494 | } 495 | } label: { 496 | Text("Unpublish all") 497 | } 498 | 499 | Divider() 500 | 501 | Menu { 502 | Button { 503 | Task { 504 | try await room.debug_simulate(scenario: .quickReconnect) 505 | } 506 | } label: { 507 | Text("Quick reconnect") 508 | } 509 | 510 | Button { 511 | Task { 512 | try await room.debug_simulate(scenario: .fullReconnect) 513 | } 514 | } label: { 515 | Text("Full reconnect") 516 | } 517 | 518 | Button { 519 | Task { 520 | try await room.debug_simulate(scenario: .nodeFailure) 521 | } 522 | } label: { 523 | Text("Node failure") 524 | } 525 | 526 | Button { 527 | Task { 528 | try await room.debug_simulate(scenario: .serverLeave) 529 | } 530 | } label: { 531 | Text("Server leave") 532 | } 533 | 534 | Button { 535 | Task { 536 | try await room.debug_simulate(scenario: .migration) 537 | } 538 | } label: { 539 | Text("Migration") 540 | } 541 | 542 | Button { 543 | Task { 544 | try await room.debug_simulate(scenario: .speakerUpdate(seconds: 3)) 545 | } 546 | } label: { 547 | Text("Speaker update") 548 | } 549 | Button { 550 | Task { 551 | try await room.debug_simulate(scenario: .forceTCP) 552 | } 553 | } label: { 554 | Text("Force TCP") 555 | } 556 | Button { 557 | Task { 558 | try await room.debug_simulate(scenario: .forceTLS) 559 | } 560 | } label: { 561 | Text("Force TLS") 562 | } 563 | } label: { 564 | Text("Simulate scenario") 565 | } 566 | } 567 | 568 | Group { 569 | Menu { 570 | Button { 571 | Task { 572 | try await room.localParticipant.setTrackSubscriptionPermissions(allParticipantsAllowed: true) 573 | } 574 | } label: { 575 | Text("Allow all") 576 | } 577 | 578 | Button { 579 | Task { 580 | try await room.localParticipant.setTrackSubscriptionPermissions(allParticipantsAllowed: false) 581 | } 582 | } label: { 583 | Text("Disallow all") 584 | } 585 | } label: { 586 | Text("Track permissions") 587 | } 588 | 589 | Toggle("Prefer speaker output", isOn: $appCtx.preferSpeakerOutput) 590 | 591 | Toggle("E2EE enabled", isOn: $roomCtx.isE2eeEnabled) 592 | } 593 | 594 | } label: { 595 | Image(systemSymbol: .gear) 596 | .renderingMode(.original) 597 | } 598 | 599 | // Disconnect 600 | Button(action: { 601 | Task { 602 | await roomCtx.disconnect() 603 | } 604 | }, 605 | label: { 606 | Image(systemSymbol: .xmarkCircleFill) 607 | .renderingMode(.original) 608 | }) 609 | } 610 | } 611 | // #if os(macOS) 612 | // .withHostingWindow { self.windowAccess.set(window: $0) } 613 | // #endif 614 | .onAppear { 615 | Task { @MainActor in 616 | canSwitchCameraPosition = try await CameraCapturer.canSwitchPosition() 617 | } 618 | Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { _ in 619 | Task { @MainActor in 620 | withAnimation { 621 | showConnectionTime = false 622 | } 623 | } 624 | } 625 | } 626 | } 627 | } 628 | 629 | struct ParticipantLayout: View { 630 | let views: [AnyView] 631 | let spacing: CGFloat 632 | 633 | init( 634 | _ data: Data, 635 | id: KeyPath = \.self, 636 | spacing: CGFloat, 637 | @ViewBuilder content: @escaping (Data.Element) -> Content 638 | ) { 639 | self.spacing = spacing 640 | views = data.map { AnyView(content($0[keyPath: id])) } 641 | } 642 | 643 | func computeColumn(with geometry: GeometryProxy) -> (x: Int, y: Int) { 644 | let sqr = Double(views.count).squareRoot() 645 | let r: [Int] = [Int(sqr.rounded()), Int(sqr.rounded(.up))] 646 | let c = geometry.isTall ? r : r.reversed() 647 | return (x: c[0], y: c[1]) 648 | } 649 | 650 | func grid(axis: Axis, geometry: GeometryProxy) -> some View { 651 | ScrollView([axis == .vertical ? .vertical : .horizontal]) { 652 | HorVGrid(axis: axis, columns: [GridItem(.flexible())], spacing: spacing) { 653 | ForEach(0 ..< views.count, id: \.self) { i in 654 | views[i] 655 | .aspectRatio(1, contentMode: .fill) 656 | } 657 | } 658 | .padding(axis == .horizontal ? [.leading, .trailing] : [.top, .bottom], 659 | max(0, ((axis == .horizontal ? geometry.size.width : geometry.size.height) 660 | - ((axis == .horizontal ? geometry.size.height : geometry.size.width) * CGFloat(views.count)) - (spacing * CGFloat(views.count - 1))) / 2)) 661 | } 662 | } 663 | 664 | var body: some View { 665 | GeometryReader { geometry in 666 | if views.isEmpty { 667 | EmptyView() 668 | } else if geometry.size.width <= 300 { 669 | grid(axis: .vertical, geometry: geometry) 670 | } else if geometry.size.height <= 300 { 671 | grid(axis: .horizontal, geometry: geometry) 672 | } else { 673 | let verticalWhenTall: Axis = geometry.isTall ? .vertical : .horizontal 674 | let horizontalWhenTall: Axis = geometry.isTall ? .horizontal : .vertical 675 | 676 | switch views.count { 677 | // simply return first view 678 | case 1: views[0] 679 | case 3: HorVStack(axis: verticalWhenTall, spacing: spacing) { 680 | views[0] 681 | HorVStack(axis: horizontalWhenTall, spacing: spacing) { 682 | views[1] 683 | views[2] 684 | } 685 | } 686 | case 5: HorVStack(axis: verticalWhenTall, spacing: spacing) { 687 | views[0] 688 | if geometry.isTall { 689 | HStack(spacing: spacing) { 690 | views[1] 691 | views[2] 692 | } 693 | HStack(spacing: spacing) { 694 | views[3] 695 | views[4] 696 | } 697 | } else { 698 | VStack(spacing: spacing) { 699 | views[1] 700 | views[3] 701 | } 702 | VStack(spacing: spacing) { 703 | views[2] 704 | views[4] 705 | } 706 | } 707 | } 708 | // case 6: 709 | // if geometry.isTall { 710 | // VStack { 711 | // HStack { 712 | // views[0] 713 | // views[1] 714 | // } 715 | // HStack { 716 | // views[2] 717 | // views[3] 718 | // } 719 | // HStack { 720 | // views[4] 721 | // views[5] 722 | // } 723 | // } 724 | // } else { 725 | // VStack { 726 | // HStack { 727 | // views[0] 728 | // views[1] 729 | // views[2] 730 | // } 731 | // HStack { 732 | // views[3] 733 | // views[4] 734 | // views[5] 735 | // } 736 | // } 737 | // } 738 | default: 739 | let c = computeColumn(with: geometry) 740 | VStack(spacing: spacing) { 741 | ForEach(0 ... (c.y - 1), id: \.self) { y in 742 | HStack(spacing: spacing) { 743 | ForEach(0 ... (c.x - 1), id: \.self) { x in 744 | let index = (y * c.x) + x 745 | if index < views.count { 746 | views[index] 747 | } 748 | } 749 | } 750 | } 751 | } 752 | } 753 | } 754 | } 755 | } 756 | } 757 | 758 | struct HorVStack: View { 759 | let axis: Axis 760 | let horizontalAlignment: HorizontalAlignment 761 | let verticalAlignment: VerticalAlignment 762 | let spacing: CGFloat? 763 | let content: () -> Content 764 | 765 | init(axis: Axis = .horizontal, 766 | horizontalAlignment: HorizontalAlignment = .center, 767 | verticalAlignment: VerticalAlignment = .center, 768 | spacing: CGFloat? = nil, 769 | @ViewBuilder content: @escaping () -> Content) 770 | { 771 | self.axis = axis 772 | self.horizontalAlignment = horizontalAlignment 773 | self.verticalAlignment = verticalAlignment 774 | self.spacing = spacing 775 | self.content = content 776 | } 777 | 778 | var body: some View { 779 | Group { 780 | if axis == .vertical { 781 | VStack(alignment: horizontalAlignment, spacing: spacing, content: content) 782 | } else { 783 | HStack(alignment: verticalAlignment, spacing: spacing, content: content) 784 | } 785 | } 786 | } 787 | } 788 | 789 | struct HorVGrid: View { 790 | let axis: Axis 791 | let spacing: CGFloat? 792 | let content: () -> Content 793 | let columns: [GridItem] 794 | 795 | init(axis: Axis = .horizontal, 796 | columns: [GridItem], 797 | spacing: CGFloat? = nil, 798 | @ViewBuilder content: @escaping () -> Content) 799 | { 800 | self.axis = axis 801 | self.spacing = spacing 802 | self.columns = columns 803 | self.content = content 804 | } 805 | 806 | var body: some View { 807 | Group { 808 | if axis == .vertical { 809 | LazyVGrid(columns: columns, spacing: spacing, content: content) 810 | } else { 811 | LazyHGrid(rows: columns, spacing: spacing, content: content) 812 | } 813 | } 814 | } 815 | } 816 | 817 | extension GeometryProxy { 818 | public var isTall: Bool { 819 | size.height > size.width 820 | } 821 | 822 | var isWide: Bool { 823 | size.width > size.height 824 | } 825 | } 826 | -------------------------------------------------------------------------------- /LiveKitExample-main.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 680FE2F227A8EF7700B6F6DB /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = 680FE2F127A8EF7700B6F6DB /* SFSafeSymbols */; }; 11 | 680FE2F427A8EFF700B6F6DB /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = 680FE2F327A8EFF700B6F6DB /* SFSafeSymbols */; }; 12 | 6816968E2AF96240008ED486 /* Participant+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6816968D2AF96240008ED486 /* Participant+Helpers.swift */; }; 13 | 6816968F2AF96240008ED486 /* Participant+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6816968D2AF96240008ED486 /* Participant+Helpers.swift */; }; 14 | 6816B1A8272D45DF005ADB85 /* ExampleRoomMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6816B1A7272D45DF005ADB85 /* ExampleRoomMessage.swift */; }; 15 | 6816B1A9272D45DF005ADB85 /* ExampleRoomMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6816B1A7272D45DF005ADB85 /* ExampleRoomMessage.swift */; }; 16 | 6816B1B0272D9198005ADB85 /* ParticipantView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6816B1AF272D9198005ADB85 /* ParticipantView.swift */; }; 17 | 6816B1B1272D9198005ADB85 /* ParticipantView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6816B1AF272D9198005ADB85 /* ParticipantView.swift */; }; 18 | 681A0AB727D888D80097E3F4 /* LiveKit in Frameworks */ = {isa = PBXBuildFile; productRef = 681A0AB627D888D80097E3F4 /* LiveKit */; }; 19 | 681A0AB827D88B190097E3F4 /* ReplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 683F05F4273F96B20080C7AC /* ReplayKit.framework */; platformFilter = maccatalyst; }; 20 | 681E3F39271FC772007BB547 /* RoomContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681E3F38271FC772007BB547 /* RoomContext.swift */; }; 21 | 681E3F3A271FC772007BB547 /* RoomContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681E3F38271FC772007BB547 /* RoomContext.swift */; }; 22 | 681E3F3F271FC795007BB547 /* Custom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681E3F3E271FC795007BB547 /* Custom.swift */; }; 23 | 681E3F40271FC795007BB547 /* Custom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681E3F3E271FC795007BB547 /* Custom.swift */; }; 24 | 681E3F43271FC7AD007BB547 /* RoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681E3F41271FC7AC007BB547 /* RoomView.swift */; }; 25 | 681E3F44271FC7AD007BB547 /* RoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681E3F41271FC7AC007BB547 /* RoomView.swift */; }; 26 | 681E3F45271FC7AD007BB547 /* ConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681E3F42271FC7AD007BB547 /* ConnectView.swift */; }; 27 | 681E3F46271FC7AD007BB547 /* ConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681E3F42271FC7AD007BB547 /* ConnectView.swift */; }; 28 | 683332C227D9DC3800378C41 /* (null) in Frameworks */ = {isa = PBXBuildFile; }; 29 | 683720D227A06404007DA986 /* ConnectionHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 683720D127A06404007DA986 /* ConnectionHistory.swift */; }; 30 | 683720D327A06404007DA986 /* ConnectionHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 683720D127A06404007DA986 /* ConnectionHistory.swift */; }; 31 | 6845DD5627DEBDD90009B5CD /* LiveKit in Frameworks */ = {isa = PBXBuildFile; productRef = 6845DD5527DEBDD90009B5CD /* LiveKit */; }; 32 | 6847616427B44A1A001611BE /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6847616327B44A1A001611BE /* Bundle.swift */; }; 33 | 6847616527B44A1A001611BE /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6847616327B44A1A001611BE /* Bundle.swift */; }; 34 | 6867533B27A65652003707B9 /* AppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6867533A27A65652003707B9 /* AppContext.swift */; }; 35 | 6867533C27A65652003707B9 /* AppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6867533A27A65652003707B9 /* AppContext.swift */; }; 36 | 687230F82B14AE0A0098CCE6 /* PublishOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 687230F72B14AE0A0098CCE6 /* PublishOptionsView.swift */; }; 37 | 687230F92B14AE0A0098CCE6 /* PublishOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 687230F72B14AE0A0098CCE6 /* PublishOptionsView.swift */; }; 38 | 68816CC127B4D6BC00E24622 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 68816CC027B4D6BC00E24622 /* KeychainAccess */; }; 39 | 68816CC327B4D94200E24622 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 68816CC227B4D94200E24622 /* KeychainAccess */; }; 40 | 68816CC527B4DCD500E24622 /* SecureStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68816CC427B4DCD500E24622 /* SecureStore.swift */; }; 41 | 68816CC627B4DCD500E24622 /* SecureStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68816CC427B4DCD500E24622 /* SecureStore.swift */; }; 42 | 6884B77C2750507400732D47 /* ScreenShareSourcePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6884B77B2750507400732D47 /* ScreenShareSourcePickerView.swift */; }; 43 | 6884B77D2750507400732D47 /* ScreenShareSourcePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6884B77B2750507400732D47 /* ScreenShareSourcePickerView.swift */; }; 44 | 688D9319283FE23F003CA647 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 68B3853E271E780700711D5F /* Assets.xcassets */; }; 45 | 688D931A283FE244003CA647 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 68B3853E271E780700711D5F /* Assets.xcassets */; }; 46 | 68B3854C271E780700711D5F /* LiveKitExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68B3853C271E780600711D5F /* LiveKitExample.swift */; }; 47 | 68B3854D271E780700711D5F /* LiveKitExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68B3853C271E780600711D5F /* LiveKitExample.swift */; }; 48 | 68EA18E827F2E91000F9AE48 /* BroadcastExt.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 683F05F3273F96B20080C7AC /* BroadcastExt.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 49 | 68FBA43F2A38B49C0015853E /* LiveKit in Frameworks */ = {isa = PBXBuildFile; productRef = 68FBA43E2A38B49C0015853E /* LiveKit */; }; 50 | D7AA477B285A0FFC00EB41AE /* SampleHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AA477A285A0FFC00EB41AE /* SampleHandler.swift */; }; 51 | D7FECD462860B42700D04D1C /* LoggingOSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FECD452860B42700D04D1C /* LoggingOSLog.swift */; }; 52 | /* End PBXBuildFile section */ 53 | 54 | /* Begin PBXContainerItemProxy section */ 55 | 68EA18E927F2E91000F9AE48 /* PBXContainerItemProxy */ = { 56 | isa = PBXContainerItemProxy; 57 | containerPortal = 68B38537271E780600711D5F /* Project object */; 58 | proxyType = 1; 59 | remoteGlobalIDString = 683F05F2273F96B20080C7AC; 60 | remoteInfo = BroadcastExt; 61 | }; 62 | /* End PBXContainerItemProxy section */ 63 | 64 | /* Begin PBXCopyFilesBuildPhase section */ 65 | 68EA18EB27F2E91100F9AE48 /* Embed Foundation Extensions */ = { 66 | isa = PBXCopyFilesBuildPhase; 67 | buildActionMask = 2147483647; 68 | dstPath = ""; 69 | dstSubfolderSpec = 13; 70 | files = ( 71 | 68EA18E827F2E91000F9AE48 /* BroadcastExt.appex in Embed Foundation Extensions */, 72 | ); 73 | name = "Embed Foundation Extensions"; 74 | runOnlyForDeploymentPostprocessing = 0; 75 | }; 76 | /* End PBXCopyFilesBuildPhase section */ 77 | 78 | /* Begin PBXFileReference section */ 79 | 6816968D2AF96240008ED486 /* Participant+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Participant+Helpers.swift"; sourceTree = ""; }; 80 | 6816B1A7272D45DF005ADB85 /* ExampleRoomMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleRoomMessage.swift; sourceTree = ""; }; 81 | 6816B1AF272D9198005ADB85 /* ParticipantView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParticipantView.swift; sourceTree = ""; }; 82 | 681E3F38271FC772007BB547 /* RoomContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomContext.swift; sourceTree = ""; }; 83 | 681E3F3E271FC795007BB547 /* Custom.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Custom.swift; sourceTree = ""; }; 84 | 681E3F41271FC7AC007BB547 /* RoomView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomView.swift; sourceTree = ""; }; 85 | 681E3F42271FC7AD007BB547 /* ConnectView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectView.swift; sourceTree = ""; }; 86 | 683720D127A06404007DA986 /* ConnectionHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionHistory.swift; sourceTree = ""; }; 87 | 683F05F3273F96B20080C7AC /* BroadcastExt.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = BroadcastExt.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 88 | 683F05F4273F96B20080C7AC /* ReplayKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReplayKit.framework; path = System/Library/Frameworks/ReplayKit.framework; sourceTree = SDKROOT; }; 89 | 683F05F9273F96B20080C7AC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 90 | 683F0603273FAD690080C7AC /* iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = iOS.entitlements; sourceTree = ""; }; 91 | 683F0604273FADC20080C7AC /* BroadcastExt.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = BroadcastExt.entitlements; sourceTree = ""; }; 92 | 6847616327B44A1A001611BE /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = ""; }; 93 | 6865EA2527513B4500FFAFC3 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 94 | 6865EA2D27513B6D00FFAFC3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 95 | 6867533A27A65652003707B9 /* AppContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppContext.swift; sourceTree = ""; }; 96 | 687230F72B14AE0A0098CCE6 /* PublishOptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublishOptionsView.swift; sourceTree = ""; }; 97 | 68816CC427B4DCD500E24622 /* SecureStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureStore.swift; sourceTree = ""; }; 98 | 6884B77B2750507400732D47 /* ScreenShareSourcePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenShareSourcePickerView.swift; sourceTree = ""; }; 99 | 68B3853C271E780600711D5F /* LiveKitExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveKitExample.swift; sourceTree = ""; }; 100 | 68B3853E271E780700711D5F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 101 | 68B38543271E780700711D5F /* LiveKitExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LiveKitExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 102 | 68B38549271E780700711D5F /* LiveKitExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LiveKitExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 103 | 68B3854B271E780700711D5F /* macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = macOS.entitlements; sourceTree = ""; }; 104 | 68DEF27E2919EEFA00258494 /* ReplayKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReplayKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/ReplayKit.framework; sourceTree = DEVELOPER_DIR; }; 105 | 9E7835E62751A71500559DEC /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.0.sdk/System/Library/Frameworks/CoreGraphics.framework; sourceTree = DEVELOPER_DIR; }; 106 | D7AA477A285A0FFC00EB41AE /* SampleHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleHandler.swift; sourceTree = ""; }; 107 | D7FECD452860B42700D04D1C /* LoggingOSLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingOSLog.swift; sourceTree = ""; }; 108 | /* End PBXFileReference section */ 109 | 110 | /* Begin PBXFrameworksBuildPhase section */ 111 | 683F05F0273F96B20080C7AC /* Frameworks */ = { 112 | isa = PBXFrameworksBuildPhase; 113 | buildActionMask = 2147483647; 114 | files = ( 115 | 681A0AB827D88B190097E3F4 /* ReplayKit.framework in Frameworks */, 116 | 681A0AB727D888D80097E3F4 /* LiveKit in Frameworks */, 117 | ); 118 | runOnlyForDeploymentPostprocessing = 0; 119 | }; 120 | 68B38540271E780700711D5F /* Frameworks */ = { 121 | isa = PBXFrameworksBuildPhase; 122 | buildActionMask = 2147483647; 123 | files = ( 124 | 68FBA43F2A38B49C0015853E /* LiveKit in Frameworks */, 125 | 680FE2F227A8EF7700B6F6DB /* SFSafeSymbols in Frameworks */, 126 | 68816CC127B4D6BC00E24622 /* KeychainAccess in Frameworks */, 127 | ); 128 | runOnlyForDeploymentPostprocessing = 0; 129 | }; 130 | 68B38546271E780700711D5F /* Frameworks */ = { 131 | isa = PBXFrameworksBuildPhase; 132 | buildActionMask = 2147483647; 133 | files = ( 134 | 683332C227D9DC3800378C41 /* (null) in Frameworks */, 135 | 680FE2F427A8EFF700B6F6DB /* SFSafeSymbols in Frameworks */, 136 | 68816CC327B4D94200E24622 /* KeychainAccess in Frameworks */, 137 | 6845DD5627DEBDD90009B5CD /* LiveKit in Frameworks */, 138 | ); 139 | runOnlyForDeploymentPostprocessing = 0; 140 | }; 141 | /* End PBXFrameworksBuildPhase section */ 142 | 143 | /* Begin PBXGroup section */ 144 | 681E3F47271FCB40007BB547 /* Frameworks */ = { 145 | isa = PBXGroup; 146 | children = ( 147 | 68DEF27E2919EEFA00258494 /* ReplayKit.framework */, 148 | 9E7835E62751A71500559DEC /* CoreGraphics.framework */, 149 | 683F05F4273F96B20080C7AC /* ReplayKit.framework */, 150 | ); 151 | name = Frameworks; 152 | sourceTree = ""; 153 | }; 154 | 683720D427A0640D007DA986 /* Support */ = { 155 | isa = PBXGroup; 156 | children = ( 157 | 6816B1A7272D45DF005ADB85 /* ExampleRoomMessage.swift */, 158 | 683720D127A06404007DA986 /* ConnectionHistory.swift */, 159 | 6847616327B44A1A001611BE /* Bundle.swift */, 160 | 68816CC427B4DCD500E24622 /* SecureStore.swift */, 161 | 6816968D2AF96240008ED486 /* Participant+Helpers.swift */, 162 | ); 163 | path = Support; 164 | sourceTree = ""; 165 | }; 166 | 683F05F6273F96B20080C7AC /* BroadcastExt */ = { 167 | isa = PBXGroup; 168 | children = ( 169 | 683F0604273FADC20080C7AC /* BroadcastExt.entitlements */, 170 | 683F05F9273F96B20080C7AC /* Info.plist */, 171 | D7AA477A285A0FFC00EB41AE /* SampleHandler.swift */, 172 | D7FECD452860B42700D04D1C /* LoggingOSLog.swift */, 173 | ); 174 | path = BroadcastExt; 175 | sourceTree = ""; 176 | }; 177 | 685271EA27443907006B4D6A /* Controllers */ = { 178 | isa = PBXGroup; 179 | children = ( 180 | 681E3F38271FC772007BB547 /* RoomContext.swift */, 181 | 6867533A27A65652003707B9 /* AppContext.swift */, 182 | ); 183 | path = Controllers; 184 | sourceTree = ""; 185 | }; 186 | 6865EA2427513B4500FFAFC3 /* iOS */ = { 187 | isa = PBXGroup; 188 | children = ( 189 | 6865EA2527513B4500FFAFC3 /* Info.plist */, 190 | 683F05F6273F96B20080C7AC /* BroadcastExt */, 191 | 683F0603273FAD690080C7AC /* iOS.entitlements */, 192 | ); 193 | path = iOS; 194 | sourceTree = ""; 195 | }; 196 | 6884B77A2750505B00732D47 /* Views */ = { 197 | isa = PBXGroup; 198 | children = ( 199 | 6884B77B2750507400732D47 /* ScreenShareSourcePickerView.swift */, 200 | 687230F72B14AE0A0098CCE6 /* PublishOptionsView.swift */, 201 | ); 202 | path = Views; 203 | sourceTree = ""; 204 | }; 205 | 68B38536271E780600711D5F = { 206 | isa = PBXGroup; 207 | children = ( 208 | 68B3853B271E780600711D5F /* Shared */, 209 | 6865EA2427513B4500FFAFC3 /* iOS */, 210 | 68B3854A271E780700711D5F /* macOS */, 211 | 68B38544271E780700711D5F /* Products */, 212 | 681E3F47271FCB40007BB547 /* Frameworks */, 213 | ); 214 | sourceTree = ""; 215 | }; 216 | 68B3853B271E780600711D5F /* Shared */ = { 217 | isa = PBXGroup; 218 | children = ( 219 | 683720D427A0640D007DA986 /* Support */, 220 | 6884B77A2750505B00732D47 /* Views */, 221 | 685271EA27443907006B4D6A /* Controllers */, 222 | 681E3F3E271FC795007BB547 /* Custom.swift */, 223 | 681E3F42271FC7AD007BB547 /* ConnectView.swift */, 224 | 681E3F41271FC7AC007BB547 /* RoomView.swift */, 225 | 68B3853C271E780600711D5F /* LiveKitExample.swift */, 226 | 6816B1AF272D9198005ADB85 /* ParticipantView.swift */, 227 | 68B3853E271E780700711D5F /* Assets.xcassets */, 228 | ); 229 | path = Shared; 230 | sourceTree = ""; 231 | }; 232 | 68B38544271E780700711D5F /* Products */ = { 233 | isa = PBXGroup; 234 | children = ( 235 | 68B38543271E780700711D5F /* LiveKitExample.app */, 236 | 68B38549271E780700711D5F /* LiveKitExample.app */, 237 | 683F05F3273F96B20080C7AC /* BroadcastExt.appex */, 238 | ); 239 | name = Products; 240 | sourceTree = ""; 241 | }; 242 | 68B3854A271E780700711D5F /* macOS */ = { 243 | isa = PBXGroup; 244 | children = ( 245 | 6865EA2D27513B6D00FFAFC3 /* Info.plist */, 246 | 68B3854B271E780700711D5F /* macOS.entitlements */, 247 | ); 248 | path = macOS; 249 | sourceTree = ""; 250 | }; 251 | /* End PBXGroup section */ 252 | 253 | /* Begin PBXNativeTarget section */ 254 | 683F05F2273F96B20080C7AC /* BroadcastExt */ = { 255 | isa = PBXNativeTarget; 256 | buildConfigurationList = 683F05FD273F96B20080C7AC /* Build configuration list for PBXNativeTarget "BroadcastExt" */; 257 | buildPhases = ( 258 | 683F05EF273F96B20080C7AC /* Sources */, 259 | 683F05F0273F96B20080C7AC /* Frameworks */, 260 | 683F05F1273F96B20080C7AC /* Resources */, 261 | ); 262 | buildRules = ( 263 | ); 264 | dependencies = ( 265 | ); 266 | name = BroadcastExt; 267 | packageProductDependencies = ( 268 | 681A0AB627D888D80097E3F4 /* LiveKit */, 269 | ); 270 | productName = BroadcastExt; 271 | productReference = 683F05F3273F96B20080C7AC /* BroadcastExt.appex */; 272 | productType = "com.apple.product-type.app-extension"; 273 | }; 274 | 68B38542271E780700711D5F /* LiveKitExample (iOS) */ = { 275 | isa = PBXNativeTarget; 276 | buildConfigurationList = 68B38554271E780700711D5F /* Build configuration list for PBXNativeTarget "LiveKitExample (iOS)" */; 277 | buildPhases = ( 278 | 68B3853F271E780700711D5F /* Sources */, 279 | 68B38540271E780700711D5F /* Frameworks */, 280 | 68B38541271E780700711D5F /* Resources */, 281 | 68EA18EB27F2E91100F9AE48 /* Embed Foundation Extensions */, 282 | ); 283 | buildRules = ( 284 | ); 285 | dependencies = ( 286 | 68EA18EA27F2E91000F9AE48 /* PBXTargetDependency */, 287 | ); 288 | name = "LiveKitExample (iOS)"; 289 | packageProductDependencies = ( 290 | 680FE2F127A8EF7700B6F6DB /* SFSafeSymbols */, 291 | 68816CC027B4D6BC00E24622 /* KeychainAccess */, 292 | 68FBA43E2A38B49C0015853E /* LiveKit */, 293 | ); 294 | productName = "Multiplatform-SwiftUI (iOS)"; 295 | productReference = 68B38543271E780700711D5F /* LiveKitExample.app */; 296 | productType = "com.apple.product-type.application"; 297 | }; 298 | 68B38548271E780700711D5F /* LiveKitExample (macOS) */ = { 299 | isa = PBXNativeTarget; 300 | buildConfigurationList = 68B38557271E780700711D5F /* Build configuration list for PBXNativeTarget "LiveKitExample (macOS)" */; 301 | buildPhases = ( 302 | 68B38545271E780700711D5F /* Sources */, 303 | 68B38546271E780700711D5F /* Frameworks */, 304 | 68B38547271E780700711D5F /* Resources */, 305 | ); 306 | buildRules = ( 307 | ); 308 | dependencies = ( 309 | ); 310 | name = "LiveKitExample (macOS)"; 311 | packageProductDependencies = ( 312 | 680FE2F327A8EFF700B6F6DB /* SFSafeSymbols */, 313 | 68816CC227B4D94200E24622 /* KeychainAccess */, 314 | 6845DD5527DEBDD90009B5CD /* LiveKit */, 315 | ); 316 | productName = "Multiplatform-SwiftUI (macOS)"; 317 | productReference = 68B38549271E780700711D5F /* LiveKitExample.app */; 318 | productType = "com.apple.product-type.application"; 319 | }; 320 | /* End PBXNativeTarget section */ 321 | 322 | /* Begin PBXProject section */ 323 | 68B38537271E780600711D5F /* Project object */ = { 324 | isa = PBXProject; 325 | attributes = { 326 | BuildIndependentTargetsInParallel = 1; 327 | LastSwiftUpdateCheck = 1330; 328 | LastUpgradeCheck = 1520; 329 | TargetAttributes = { 330 | 683F05F2273F96B20080C7AC = { 331 | CreatedOnToolsVersion = 13.1; 332 | }; 333 | 68B38542271E780700711D5F = { 334 | CreatedOnToolsVersion = 13.0; 335 | }; 336 | 68B38548271E780700711D5F = { 337 | CreatedOnToolsVersion = 13.0; 338 | }; 339 | }; 340 | }; 341 | buildConfigurationList = 68B3853A271E780600711D5F /* Build configuration list for PBXProject "LiveKitExample-main" */; 342 | compatibilityVersion = "Xcode 12.0"; 343 | developmentRegion = en; 344 | hasScannedForEncodings = 0; 345 | knownRegions = ( 346 | en, 347 | Base, 348 | ); 349 | mainGroup = 68B38536271E780600711D5F; 350 | packageReferences = ( 351 | 685271E727407BBC006B4D6A /* XCRemoteSwiftPackageReference "swift-protobuf" */, 352 | 680FE2F027A8EF7700B6F6DB /* XCRemoteSwiftPackageReference "SFSafeSymbols" */, 353 | 68816CBF27B4D6BC00E24622 /* XCRemoteSwiftPackageReference "KeychainAccess" */, 354 | 68FBA43D2A38B49C0015853E /* XCRemoteSwiftPackageReference "client-sdk-swift" */, 355 | ); 356 | productRefGroup = 68B38544271E780700711D5F /* Products */; 357 | projectDirPath = ""; 358 | projectRoot = ""; 359 | targets = ( 360 | 68B38542271E780700711D5F /* LiveKitExample (iOS) */, 361 | 68B38548271E780700711D5F /* LiveKitExample (macOS) */, 362 | 683F05F2273F96B20080C7AC /* BroadcastExt */, 363 | ); 364 | }; 365 | /* End PBXProject section */ 366 | 367 | /* Begin PBXResourcesBuildPhase section */ 368 | 683F05F1273F96B20080C7AC /* Resources */ = { 369 | isa = PBXResourcesBuildPhase; 370 | buildActionMask = 2147483647; 371 | files = ( 372 | ); 373 | runOnlyForDeploymentPostprocessing = 0; 374 | }; 375 | 68B38541271E780700711D5F /* Resources */ = { 376 | isa = PBXResourcesBuildPhase; 377 | buildActionMask = 2147483647; 378 | files = ( 379 | 688D931A283FE244003CA647 /* Assets.xcassets in Resources */, 380 | ); 381 | runOnlyForDeploymentPostprocessing = 0; 382 | }; 383 | 68B38547271E780700711D5F /* Resources */ = { 384 | isa = PBXResourcesBuildPhase; 385 | buildActionMask = 2147483647; 386 | files = ( 387 | 688D9319283FE23F003CA647 /* Assets.xcassets in Resources */, 388 | ); 389 | runOnlyForDeploymentPostprocessing = 0; 390 | }; 391 | /* End PBXResourcesBuildPhase section */ 392 | 393 | /* Begin PBXSourcesBuildPhase section */ 394 | 683F05EF273F96B20080C7AC /* Sources */ = { 395 | isa = PBXSourcesBuildPhase; 396 | buildActionMask = 2147483647; 397 | files = ( 398 | D7FECD462860B42700D04D1C /* LoggingOSLog.swift in Sources */, 399 | D7AA477B285A0FFC00EB41AE /* SampleHandler.swift in Sources */, 400 | ); 401 | runOnlyForDeploymentPostprocessing = 0; 402 | }; 403 | 68B3853F271E780700711D5F /* Sources */ = { 404 | isa = PBXSourcesBuildPhase; 405 | buildActionMask = 2147483647; 406 | files = ( 407 | 681E3F45271FC7AD007BB547 /* ConnectView.swift in Sources */, 408 | 6867533B27A65652003707B9 /* AppContext.swift in Sources */, 409 | 681E3F39271FC772007BB547 /* RoomContext.swift in Sources */, 410 | 683720D227A06404007DA986 /* ConnectionHistory.swift in Sources */, 411 | 681E3F43271FC7AD007BB547 /* RoomView.swift in Sources */, 412 | 6816B1B0272D9198005ADB85 /* ParticipantView.swift in Sources */, 413 | 68816CC527B4DCD500E24622 /* SecureStore.swift in Sources */, 414 | 6816968E2AF96240008ED486 /* Participant+Helpers.swift in Sources */, 415 | 68B3854C271E780700711D5F /* LiveKitExample.swift in Sources */, 416 | 681E3F3F271FC795007BB547 /* Custom.swift in Sources */, 417 | 6847616427B44A1A001611BE /* Bundle.swift in Sources */, 418 | 687230F82B14AE0A0098CCE6 /* PublishOptionsView.swift in Sources */, 419 | 6816B1A8272D45DF005ADB85 /* ExampleRoomMessage.swift in Sources */, 420 | 6884B77C2750507400732D47 /* ScreenShareSourcePickerView.swift in Sources */, 421 | ); 422 | runOnlyForDeploymentPostprocessing = 0; 423 | }; 424 | 68B38545271E780700711D5F /* Sources */ = { 425 | isa = PBXSourcesBuildPhase; 426 | buildActionMask = 2147483647; 427 | files = ( 428 | 681E3F46271FC7AD007BB547 /* ConnectView.swift in Sources */, 429 | 6867533C27A65652003707B9 /* AppContext.swift in Sources */, 430 | 681E3F3A271FC772007BB547 /* RoomContext.swift in Sources */, 431 | 683720D327A06404007DA986 /* ConnectionHistory.swift in Sources */, 432 | 681E3F44271FC7AD007BB547 /* RoomView.swift in Sources */, 433 | 6816B1B1272D9198005ADB85 /* ParticipantView.swift in Sources */, 434 | 68816CC627B4DCD500E24622 /* SecureStore.swift in Sources */, 435 | 6816968F2AF96240008ED486 /* Participant+Helpers.swift in Sources */, 436 | 68B3854D271E780700711D5F /* LiveKitExample.swift in Sources */, 437 | 681E3F40271FC795007BB547 /* Custom.swift in Sources */, 438 | 6847616527B44A1A001611BE /* Bundle.swift in Sources */, 439 | 687230F92B14AE0A0098CCE6 /* PublishOptionsView.swift in Sources */, 440 | 6816B1A9272D45DF005ADB85 /* ExampleRoomMessage.swift in Sources */, 441 | 6884B77D2750507400732D47 /* ScreenShareSourcePickerView.swift in Sources */, 442 | ); 443 | runOnlyForDeploymentPostprocessing = 0; 444 | }; 445 | /* End PBXSourcesBuildPhase section */ 446 | 447 | /* Begin PBXTargetDependency section */ 448 | 68EA18EA27F2E91000F9AE48 /* PBXTargetDependency */ = { 449 | isa = PBXTargetDependency; 450 | target = 683F05F2273F96B20080C7AC /* BroadcastExt */; 451 | targetProxy = 68EA18E927F2E91000F9AE48 /* PBXContainerItemProxy */; 452 | }; 453 | /* End PBXTargetDependency section */ 454 | 455 | /* Begin XCBuildConfiguration section */ 456 | 683F05FE273F96B20080C7AC /* Debug */ = { 457 | isa = XCBuildConfiguration; 458 | buildSettings = { 459 | CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/iOS/BroadcastExt/BroadcastExt.entitlements"; 460 | CODE_SIGN_STYLE = Automatic; 461 | CURRENT_PROJECT_VERSION = 24; 462 | DEVELOPMENT_TEAM = 76TVFCUKK7; 463 | INFOPLIST_FILE = "$(SRCROOT)/iOS/BroadcastExt/Info.plist"; 464 | INFOPLIST_KEY_CFBundleDisplayName = BroadcastExt; 465 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 466 | LD_RUNPATH_SEARCH_PATHS = ( 467 | "$(inherited)", 468 | "@executable_path/Frameworks", 469 | "@executable_path/../../Frameworks", 470 | ); 471 | MARKETING_VERSION = 1.0.24; 472 | PRODUCT_BUNDLE_IDENTIFIER = io.livekit.example.SwiftSDK.1.BroadcastExt; 473 | PRODUCT_NAME = "$(TARGET_NAME)"; 474 | SDKROOT = iphoneos; 475 | SKIP_INSTALL = YES; 476 | SWIFT_EMIT_LOC_STRINGS = YES; 477 | TARGETED_DEVICE_FAMILY = "1,2"; 478 | }; 479 | name = Debug; 480 | }; 481 | 683F05FF273F96B20080C7AC /* Release */ = { 482 | isa = XCBuildConfiguration; 483 | buildSettings = { 484 | CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/iOS/BroadcastExt/BroadcastExt.entitlements"; 485 | CODE_SIGN_STYLE = Automatic; 486 | CURRENT_PROJECT_VERSION = 24; 487 | DEVELOPMENT_TEAM = 76TVFCUKK7; 488 | INFOPLIST_FILE = "$(SRCROOT)/iOS/BroadcastExt/Info.plist"; 489 | INFOPLIST_KEY_CFBundleDisplayName = BroadcastExt; 490 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 491 | LD_RUNPATH_SEARCH_PATHS = ( 492 | "$(inherited)", 493 | "@executable_path/Frameworks", 494 | "@executable_path/../../Frameworks", 495 | ); 496 | MARKETING_VERSION = 1.0.24; 497 | PRODUCT_BUNDLE_IDENTIFIER = io.livekit.example.SwiftSDK.1.BroadcastExt; 498 | PRODUCT_NAME = "$(TARGET_NAME)"; 499 | SDKROOT = iphoneos; 500 | SKIP_INSTALL = YES; 501 | SWIFT_EMIT_LOC_STRINGS = YES; 502 | TARGETED_DEVICE_FAMILY = "1,2"; 503 | VALIDATE_PRODUCT = YES; 504 | }; 505 | name = Release; 506 | }; 507 | 68B38552271E780700711D5F /* Debug */ = { 508 | isa = XCBuildConfiguration; 509 | buildSettings = { 510 | ALWAYS_SEARCH_USER_PATHS = NO; 511 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = NO; 512 | CLANG_ANALYZER_NONNULL = YES; 513 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 514 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 515 | CLANG_CXX_LIBRARY = "libc++"; 516 | CLANG_ENABLE_MODULES = YES; 517 | CLANG_ENABLE_OBJC_ARC = YES; 518 | CLANG_ENABLE_OBJC_WEAK = YES; 519 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 520 | CLANG_WARN_BOOL_CONVERSION = YES; 521 | CLANG_WARN_COMMA = YES; 522 | CLANG_WARN_CONSTANT_CONVERSION = YES; 523 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 524 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 525 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 526 | CLANG_WARN_EMPTY_BODY = YES; 527 | CLANG_WARN_ENUM_CONVERSION = YES; 528 | CLANG_WARN_INFINITE_RECURSION = YES; 529 | CLANG_WARN_INT_CONVERSION = YES; 530 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 531 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 532 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 533 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 534 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 535 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 536 | CLANG_WARN_STRICT_PROTOTYPES = YES; 537 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 538 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 539 | CLANG_WARN_UNREACHABLE_CODE = YES; 540 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 541 | COPY_PHASE_STRIP = NO; 542 | DEAD_CODE_STRIPPING = YES; 543 | DEBUG_INFORMATION_FORMAT = dwarf; 544 | ENABLE_BITCODE = NO; 545 | ENABLE_STRICT_OBJC_MSGSEND = YES; 546 | ENABLE_TESTABILITY = YES; 547 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 548 | GCC_C_LANGUAGE_STANDARD = gnu11; 549 | GCC_DYNAMIC_NO_PIC = NO; 550 | GCC_NO_COMMON_BLOCKS = YES; 551 | GCC_OPTIMIZATION_LEVEL = 0; 552 | GCC_PREPROCESSOR_DEFINITIONS = ( 553 | "DEBUG=1", 554 | "GL_SILENCE_DEPRECATION=1", 555 | ); 556 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 557 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 558 | GCC_WARN_UNDECLARED_SELECTOR = YES; 559 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 560 | GCC_WARN_UNUSED_FUNCTION = YES; 561 | GCC_WARN_UNUSED_VARIABLE = YES; 562 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 563 | MTL_FAST_MATH = YES; 564 | ONLY_ACTIVE_ARCH = YES; 565 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 566 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 567 | SWIFT_VERSION = 5.0; 568 | }; 569 | name = Debug; 570 | }; 571 | 68B38553271E780700711D5F /* Release */ = { 572 | isa = XCBuildConfiguration; 573 | buildSettings = { 574 | ALWAYS_SEARCH_USER_PATHS = NO; 575 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = NO; 576 | CLANG_ANALYZER_NONNULL = YES; 577 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 578 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 579 | CLANG_CXX_LIBRARY = "libc++"; 580 | CLANG_ENABLE_MODULES = YES; 581 | CLANG_ENABLE_OBJC_ARC = YES; 582 | CLANG_ENABLE_OBJC_WEAK = YES; 583 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 584 | CLANG_WARN_BOOL_CONVERSION = YES; 585 | CLANG_WARN_COMMA = YES; 586 | CLANG_WARN_CONSTANT_CONVERSION = YES; 587 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 588 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 589 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 590 | CLANG_WARN_EMPTY_BODY = YES; 591 | CLANG_WARN_ENUM_CONVERSION = YES; 592 | CLANG_WARN_INFINITE_RECURSION = YES; 593 | CLANG_WARN_INT_CONVERSION = YES; 594 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 595 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 596 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 597 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 598 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 599 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 600 | CLANG_WARN_STRICT_PROTOTYPES = YES; 601 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 602 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 603 | CLANG_WARN_UNREACHABLE_CODE = YES; 604 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 605 | COPY_PHASE_STRIP = NO; 606 | DEAD_CODE_STRIPPING = YES; 607 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 608 | ENABLE_BITCODE = NO; 609 | ENABLE_NS_ASSERTIONS = NO; 610 | ENABLE_STRICT_OBJC_MSGSEND = YES; 611 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 612 | GCC_C_LANGUAGE_STANDARD = gnu11; 613 | GCC_NO_COMMON_BLOCKS = YES; 614 | GCC_PREPROCESSOR_DEFINITIONS = "GL_SILENCE_DEPRECATION=1"; 615 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 616 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 617 | GCC_WARN_UNDECLARED_SELECTOR = YES; 618 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 619 | GCC_WARN_UNUSED_FUNCTION = YES; 620 | GCC_WARN_UNUSED_VARIABLE = YES; 621 | MTL_ENABLE_DEBUG_INFO = NO; 622 | MTL_FAST_MATH = YES; 623 | SWIFT_COMPILATION_MODE = wholemodule; 624 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 625 | SWIFT_VERSION = 5.0; 626 | }; 627 | name = Release; 628 | }; 629 | 68B38555271E780700711D5F /* Debug */ = { 630 | isa = XCBuildConfiguration; 631 | buildSettings = { 632 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 633 | ASSETCATALOG_COMPILER_APPICON_NAME = iOS; 634 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 635 | CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/iOS/iOS.entitlements"; 636 | CODE_SIGN_IDENTITY = "Apple Development"; 637 | CODE_SIGN_STYLE = Automatic; 638 | CURRENT_PROJECT_VERSION = 2024031101; 639 | DEVELOPMENT_TEAM = 76TVFCUKK7; 640 | ENABLE_PREVIEWS = YES; 641 | INFOPLIST_FILE = iOS/Info.plist; 642 | INFOPLIST_KEY_NSCameraUsageDescription = "Please allow Camera access"; 643 | INFOPLIST_KEY_NSMicrophoneUsageDescription = "Please allow Microphone access"; 644 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 645 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 646 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 647 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 648 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 649 | LD_RUNPATH_SEARCH_PATHS = ( 650 | "$(inherited)", 651 | "@executable_path/Frameworks", 652 | ); 653 | MARKETING_VERSION = 2.0.5; 654 | PRODUCT_BUNDLE_IDENTIFIER = io.livekit.example.SwiftSDK.1; 655 | PRODUCT_NAME = LiveKitExample; 656 | PROVISIONING_PROFILE_SPECIFIER = ""; 657 | SDKROOT = iphoneos; 658 | SWIFT_EMIT_LOC_STRINGS = YES; 659 | TARGETED_DEVICE_FAMILY = "1,2"; 660 | }; 661 | name = Debug; 662 | }; 663 | 68B38556271E780700711D5F /* Release */ = { 664 | isa = XCBuildConfiguration; 665 | buildSettings = { 666 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 667 | ASSETCATALOG_COMPILER_APPICON_NAME = iOS; 668 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 669 | CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/iOS/iOS.entitlements"; 670 | CODE_SIGN_IDENTITY = "Apple Development"; 671 | CODE_SIGN_STYLE = Automatic; 672 | CURRENT_PROJECT_VERSION = 2024031101; 673 | DEVELOPMENT_TEAM = 76TVFCUKK7; 674 | ENABLE_PREVIEWS = YES; 675 | INFOPLIST_FILE = iOS/Info.plist; 676 | INFOPLIST_KEY_NSCameraUsageDescription = "Please allow Camera access"; 677 | INFOPLIST_KEY_NSMicrophoneUsageDescription = "Please allow Microphone access"; 678 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 679 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 680 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 681 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 682 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 683 | LD_RUNPATH_SEARCH_PATHS = ( 684 | "$(inherited)", 685 | "@executable_path/Frameworks", 686 | ); 687 | MARKETING_VERSION = 2.0.5; 688 | PRODUCT_BUNDLE_IDENTIFIER = io.livekit.example.SwiftSDK.1; 689 | PRODUCT_NAME = LiveKitExample; 690 | PROVISIONING_PROFILE_SPECIFIER = ""; 691 | SDKROOT = iphoneos; 692 | SWIFT_EMIT_LOC_STRINGS = YES; 693 | TARGETED_DEVICE_FAMILY = "1,2"; 694 | VALIDATE_PRODUCT = YES; 695 | }; 696 | name = Release; 697 | }; 698 | 68B38558271E780700711D5F /* Debug */ = { 699 | isa = XCBuildConfiguration; 700 | buildSettings = { 701 | ASSETCATALOG_COMPILER_APPICON_NAME = MacOS; 702 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 703 | CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/macOS/macOS.entitlements"; 704 | CODE_SIGN_IDENTITY = "Apple Development"; 705 | CODE_SIGN_STYLE = Automatic; 706 | COMBINE_HIDPI_IMAGES = YES; 707 | CURRENT_PROJECT_VERSION = 2024031101; 708 | DEAD_CODE_STRIPPING = YES; 709 | DEVELOPMENT_TEAM = 76TVFCUKK7; 710 | ENABLE_HARDENED_RUNTIME = YES; 711 | ENABLE_PREVIEWS = YES; 712 | INFOPLIST_FILE = "$(SRCROOT)/macOS/Info.plist"; 713 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; 714 | INFOPLIST_KEY_NSCameraUsageDescription = "Please allow Camera access"; 715 | INFOPLIST_KEY_NSMicrophoneUsageDescription = "Please allow Microphone access"; 716 | LD_RUNPATH_SEARCH_PATHS = ( 717 | "$(inherited)", 718 | "@executable_path/../Frameworks", 719 | ); 720 | MACOSX_DEPLOYMENT_TARGET = 11.0; 721 | MARKETING_VERSION = 2.0.5; 722 | PRODUCT_BUNDLE_IDENTIFIER = io.livekit.example.SwiftSDK.1; 723 | PRODUCT_NAME = LiveKitExample; 724 | PROVISIONING_PROFILE_SPECIFIER = ""; 725 | SDKROOT = macosx; 726 | SWIFT_EMIT_LOC_STRINGS = YES; 727 | }; 728 | name = Debug; 729 | }; 730 | 68B38559271E780700711D5F /* Release */ = { 731 | isa = XCBuildConfiguration; 732 | buildSettings = { 733 | ASSETCATALOG_COMPILER_APPICON_NAME = MacOS; 734 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 735 | CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/macOS/macOS.entitlements"; 736 | CODE_SIGN_IDENTITY = "Apple Development"; 737 | CODE_SIGN_STYLE = Automatic; 738 | COMBINE_HIDPI_IMAGES = YES; 739 | CURRENT_PROJECT_VERSION = 2024031101; 740 | DEAD_CODE_STRIPPING = YES; 741 | DEVELOPMENT_TEAM = 76TVFCUKK7; 742 | ENABLE_HARDENED_RUNTIME = YES; 743 | ENABLE_PREVIEWS = YES; 744 | INFOPLIST_FILE = "$(SRCROOT)/macOS/Info.plist"; 745 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; 746 | INFOPLIST_KEY_NSCameraUsageDescription = "Please allow Camera access"; 747 | INFOPLIST_KEY_NSMicrophoneUsageDescription = "Please allow Microphone access"; 748 | LD_RUNPATH_SEARCH_PATHS = ( 749 | "$(inherited)", 750 | "@executable_path/../Frameworks", 751 | ); 752 | MACOSX_DEPLOYMENT_TARGET = 11.0; 753 | MARKETING_VERSION = 2.0.5; 754 | PRODUCT_BUNDLE_IDENTIFIER = io.livekit.example.SwiftSDK.1; 755 | PRODUCT_NAME = LiveKitExample; 756 | PROVISIONING_PROFILE_SPECIFIER = ""; 757 | SDKROOT = macosx; 758 | SWIFT_EMIT_LOC_STRINGS = YES; 759 | }; 760 | name = Release; 761 | }; 762 | /* End XCBuildConfiguration section */ 763 | 764 | /* Begin XCConfigurationList section */ 765 | 683F05FD273F96B20080C7AC /* Build configuration list for PBXNativeTarget "BroadcastExt" */ = { 766 | isa = XCConfigurationList; 767 | buildConfigurations = ( 768 | 683F05FE273F96B20080C7AC /* Debug */, 769 | 683F05FF273F96B20080C7AC /* Release */, 770 | ); 771 | defaultConfigurationIsVisible = 0; 772 | defaultConfigurationName = Release; 773 | }; 774 | 68B3853A271E780600711D5F /* Build configuration list for PBXProject "LiveKitExample-main" */ = { 775 | isa = XCConfigurationList; 776 | buildConfigurations = ( 777 | 68B38552271E780700711D5F /* Debug */, 778 | 68B38553271E780700711D5F /* Release */, 779 | ); 780 | defaultConfigurationIsVisible = 0; 781 | defaultConfigurationName = Release; 782 | }; 783 | 68B38554271E780700711D5F /* Build configuration list for PBXNativeTarget "LiveKitExample (iOS)" */ = { 784 | isa = XCConfigurationList; 785 | buildConfigurations = ( 786 | 68B38555271E780700711D5F /* Debug */, 787 | 68B38556271E780700711D5F /* Release */, 788 | ); 789 | defaultConfigurationIsVisible = 0; 790 | defaultConfigurationName = Release; 791 | }; 792 | 68B38557271E780700711D5F /* Build configuration list for PBXNativeTarget "LiveKitExample (macOS)" */ = { 793 | isa = XCConfigurationList; 794 | buildConfigurations = ( 795 | 68B38558271E780700711D5F /* Debug */, 796 | 68B38559271E780700711D5F /* Release */, 797 | ); 798 | defaultConfigurationIsVisible = 0; 799 | defaultConfigurationName = Release; 800 | }; 801 | /* End XCConfigurationList section */ 802 | 803 | /* Begin XCRemoteSwiftPackageReference section */ 804 | 680FE2F027A8EF7700B6F6DB /* XCRemoteSwiftPackageReference "SFSafeSymbols" */ = { 805 | isa = XCRemoteSwiftPackageReference; 806 | repositoryURL = "https://github.com/SFSafeSymbols/SFSafeSymbols"; 807 | requirement = { 808 | kind = upToNextMajorVersion; 809 | minimumVersion = 4.1.1; 810 | }; 811 | }; 812 | 685271E727407BBC006B4D6A /* XCRemoteSwiftPackageReference "swift-protobuf" */ = { 813 | isa = XCRemoteSwiftPackageReference; 814 | repositoryURL = "https://github.com/apple/swift-protobuf.git"; 815 | requirement = { 816 | kind = upToNextMajorVersion; 817 | minimumVersion = 1.18.0; 818 | }; 819 | }; 820 | 68816CBF27B4D6BC00E24622 /* XCRemoteSwiftPackageReference "KeychainAccess" */ = { 821 | isa = XCRemoteSwiftPackageReference; 822 | repositoryURL = "https://github.com/kishikawakatsumi/KeychainAccess.git"; 823 | requirement = { 824 | kind = upToNextMajorVersion; 825 | minimumVersion = 4.2.2; 826 | }; 827 | }; 828 | 68FBA43D2A38B49C0015853E /* XCRemoteSwiftPackageReference "client-sdk-swift" */ = { 829 | isa = XCRemoteSwiftPackageReference; 830 | repositoryURL = "https://github.com/livekit/client-sdk-swift"; 831 | requirement = { 832 | branch = main; 833 | kind = branch; 834 | }; 835 | }; 836 | /* End XCRemoteSwiftPackageReference section */ 837 | 838 | /* Begin XCSwiftPackageProductDependency section */ 839 | 680FE2F127A8EF7700B6F6DB /* SFSafeSymbols */ = { 840 | isa = XCSwiftPackageProductDependency; 841 | package = 680FE2F027A8EF7700B6F6DB /* XCRemoteSwiftPackageReference "SFSafeSymbols" */; 842 | productName = SFSafeSymbols; 843 | }; 844 | 680FE2F327A8EFF700B6F6DB /* SFSafeSymbols */ = { 845 | isa = XCSwiftPackageProductDependency; 846 | package = 680FE2F027A8EF7700B6F6DB /* XCRemoteSwiftPackageReference "SFSafeSymbols" */; 847 | productName = SFSafeSymbols; 848 | }; 849 | 681A0AB627D888D80097E3F4 /* LiveKit */ = { 850 | isa = XCSwiftPackageProductDependency; 851 | productName = LiveKit; 852 | }; 853 | 6845DD5527DEBDD90009B5CD /* LiveKit */ = { 854 | isa = XCSwiftPackageProductDependency; 855 | productName = LiveKit; 856 | }; 857 | 68816CC027B4D6BC00E24622 /* KeychainAccess */ = { 858 | isa = XCSwiftPackageProductDependency; 859 | package = 68816CBF27B4D6BC00E24622 /* XCRemoteSwiftPackageReference "KeychainAccess" */; 860 | productName = KeychainAccess; 861 | }; 862 | 68816CC227B4D94200E24622 /* KeychainAccess */ = { 863 | isa = XCSwiftPackageProductDependency; 864 | package = 68816CBF27B4D6BC00E24622 /* XCRemoteSwiftPackageReference "KeychainAccess" */; 865 | productName = KeychainAccess; 866 | }; 867 | 68FBA43E2A38B49C0015853E /* LiveKit */ = { 868 | isa = XCSwiftPackageProductDependency; 869 | package = 68FBA43D2A38B49C0015853E /* XCRemoteSwiftPackageReference "client-sdk-swift" */; 870 | productName = LiveKit; 871 | }; 872 | /* End XCSwiftPackageProductDependency section */ 873 | }; 874 | rootObject = 68B38537271E780600711D5F /* Project object */; 875 | } 876 | --------------------------------------------------------------------------------