├── .gitignore ├── .periphery.yml ├── .swift-version ├── .swiftformat ├── LICENSE ├── LiveKitExample-dev.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ ├── WorkspaceSettings.xcsettings │ └── swiftpm │ └── Package.resolved ├── LiveKitExample.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ └── xcschemes │ ├── BroadcastExt.xcscheme │ └── SwiftSDK.1.xcscheme ├── Multiplatform-Info.plist ├── Multiplatform ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── Contents.json │ ├── LKBlue.colorset │ │ └── Contents.json │ ├── LKDarkBlue.colorset │ │ └── Contents.json │ ├── MacOS.appiconset │ │ └── Contents.json │ ├── iOS.appiconset │ │ ├── Contents.json │ │ └── LiveKit.png │ ├── lkDarkRed.colorset │ │ └── Contents.json │ ├── lkGray1.colorset │ │ └── Contents.json │ ├── lkGray2.colorset │ │ └── Contents.json │ ├── lkGray3.colorset │ │ └── Contents.json │ ├── lkRed.colorset │ │ └── Contents.json │ ├── logo.imageset │ │ ├── Contents.json │ │ ├── Livekit Wordmark.png │ │ ├── Livekit Wordmark@2x.png │ │ └── Livekit Wordmark@3x.png │ ├── macOS.appiconset │ │ ├── LiveKit-1024.png │ │ ├── LiveKit-128.png │ │ ├── LiveKit-16.png │ │ ├── LiveKit-256.png │ │ ├── LiveKit-32.png │ │ ├── LiveKit-512.png │ │ └── LiveKit-64.png │ └── visionOS.solidimagestack │ │ ├── Back.solidimagestacklayer │ │ ├── Content.imageset │ │ │ ├── Contents.json │ │ │ └── LiveKit-1024.png │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── Front.solidimagestacklayer │ │ ├── Content.imageset │ │ │ ├── Contents.json │ │ │ └── LiveKit-1024.png │ │ └── Contents.json │ │ └── Middle.solidimagestacklayer │ │ ├── Content.imageset │ │ ├── Contents.json │ │ └── LiveKit-1024.png │ │ └── Contents.json ├── Controllers │ ├── AppContext.swift │ └── RoomContext.swift ├── Extensions │ ├── Binding+OptionSet.swift │ ├── Bundle.swift │ └── CIImage.swift ├── LiveKitExample.swift ├── Support │ ├── ConnectionHistory.swift │ ├── ExampleRoomMessage.swift │ ├── Participant+Helpers.swift │ └── SecureStore.swift ├── SwiftSDK_1.entitlements └── Views │ ├── AudioMixerView.swift │ ├── ConnectView.swift │ ├── ImmersiveView.swift │ ├── ParticipantView.swift │ ├── PublishOptionsView.swift │ ├── RoomContextView.swift │ ├── RoomSwitchView.swift │ ├── RoomView.swift │ ├── ScreenShareSourcePickerView.swift │ └── Shared │ ├── LKButton.swift │ └── LKTextField.swift ├── README.md └── iOS ├── BroadcastExt ├── BroadcastExt.entitlements ├── Info.plist └── SampleHandler.swift ├── Info.plist └── iOS.entitlements /.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 | 93 | # Ignore license files 94 | *.license 95 | -------------------------------------------------------------------------------- /.periphery.yml: -------------------------------------------------------------------------------- 1 | retain_objc_accessible: true 2 | schemes: 3 | - Example (macOS Debug) 4 | targets: 5 | - LiveKitExample (macOS) 6 | workspace: LiveKitExample-dev.xcworkspace 7 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.7 # Xcode 14 2 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | --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 */" 2 | --ifdef no-indent 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LiveKitExample-dev.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /LiveKitExample-dev.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LiveKitExample-dev.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "aac832500a4575ceb352d1e4a7205e95d9cfc77eb2c166554aad476cfd7985b5", 3 | "pins" : [ 4 | { 5 | "identity" : "jwt-kit", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/vapor/jwt-kit.git", 8 | "state" : { 9 | "revision" : "13e7513b3ba0afa13967daf77af2fb4ad087306c", 10 | "version" : "4.13.5" 11 | } 12 | }, 13 | { 14 | "identity" : "swift-asn1", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/apple/swift-asn1.git", 17 | "state" : { 18 | "revision" : "ae33e5941bb88d88538d0a6b19ca0b01e6c76dcf", 19 | "version" : "1.3.1" 20 | } 21 | }, 22 | { 23 | "identity" : "swift-collections", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/apple/swift-collections.git", 26 | "state" : { 27 | "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", 28 | "version" : "1.1.4" 29 | } 30 | }, 31 | { 32 | "identity" : "swift-crypto", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/apple/swift-crypto.git", 35 | "state" : { 36 | "revision" : "45305d32cfb830faebcaa9a7aea66ad342637518", 37 | "version" : "3.11.1" 38 | } 39 | }, 40 | { 41 | "identity" : "swift-docc-plugin", 42 | "kind" : "remoteSourceControl", 43 | "location" : "https://github.com/apple/swift-docc-plugin.git", 44 | "state" : { 45 | "revision" : "85e4bb4e1cd62cec64a4b8e769dcefdf0c5b9d64", 46 | "version" : "1.4.3" 47 | } 48 | }, 49 | { 50 | "identity" : "swift-docc-symbolkit", 51 | "kind" : "remoteSourceControl", 52 | "location" : "https://github.com/swiftlang/swift-docc-symbolkit", 53 | "state" : { 54 | "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", 55 | "version" : "1.0.0" 56 | } 57 | }, 58 | { 59 | "identity" : "swift-log", 60 | "kind" : "remoteSourceControl", 61 | "location" : "https://github.com/apple/swift-log.git", 62 | "state" : { 63 | "revision" : "96a2f8a0fa41e9e09af4585e2724c4e825410b91", 64 | "version" : "1.6.2" 65 | } 66 | }, 67 | { 68 | "identity" : "swift-protobuf", 69 | "kind" : "remoteSourceControl", 70 | "location" : "https://github.com/apple/swift-protobuf.git", 71 | "state" : { 72 | "revision" : "d72aed98f8253ec1aa9ea1141e28150f408cf17f", 73 | "version" : "1.29.0" 74 | } 75 | }, 76 | { 77 | "identity" : "webrtc-xcframework", 78 | "kind" : "remoteSourceControl", 79 | "location" : "https://github.com/livekit/webrtc-xcframework.git", 80 | "state" : { 81 | "revision" : "5e93f981d3af01addbd5c4998e58bd7441c9ae95", 82 | "version" : "125.6422.32" 83 | } 84 | } 85 | ], 86 | "version" : 3 87 | } 88 | -------------------------------------------------------------------------------- /LiveKitExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 68019B9C2D00343700721481 /* LiveKit in Frameworks */ = {isa = PBXBuildFile; productRef = 68698E612C4C218B00221782 /* LiveKit */; }; 11 | 681A0AB727D888D80097E3F4 /* LiveKit in Frameworks */ = {isa = PBXBuildFile; productRef = 681A0AB627D888D80097E3F4 /* LiveKit */; }; 12 | 681A0AB827D88B190097E3F4 /* ReplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 683F05F4273F96B20080C7AC /* ReplayKit.framework */; platformFilter = maccatalyst; }; 13 | 6830E6BE2D5BE5E2001C5E83 /* AudioMixerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6830E6BD2D5BE5DF001C5E83 /* AudioMixerView.swift */; }; 14 | 68698E642C4C219500221782 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 68698E632C4C219500221782 /* KeychainAccess */; }; 15 | 68698E662C4C219A00221782 /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = 68698E652C4C219A00221782 /* SFSafeSymbols */; }; 16 | 6888FBE12C66B7B400AB93C1 /* ImmersiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6888FBE02C66B7B100AB93C1 /* ImmersiveView.swift */; }; 17 | 68A50EDF2C4C1ED500D2DE17 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 68A50ED52C4C1ED500D2DE17 /* Assets.xcassets */; }; 18 | 68A50EE02C4C1ED500D2DE17 /* LiveKitExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68A50ED92C4C1ED500D2DE17 /* LiveKitExample.swift */; }; 19 | 68A50EE22C4C1ED500D2DE17 /* RoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68A50EDB2C4C1ED500D2DE17 /* RoomView.swift */; }; 20 | 68A50EE32C4C1ED500D2DE17 /* PublishOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68A50ED22C4C1ED500D2DE17 /* PublishOptionsView.swift */; }; 21 | 68A50EE42C4C1ED500D2DE17 /* SecureStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68A50ED02C4C1ED500D2DE17 /* SecureStore.swift */; }; 22 | 68A50EE52C4C1ED500D2DE17 /* ConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68A50ED62C4C1ED500D2DE17 /* ConnectView.swift */; }; 23 | 68A50EE62C4C1ED500D2DE17 /* ExampleRoomMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68A50ECE2C4C1ED500D2DE17 /* ExampleRoomMessage.swift */; }; 24 | 68A50EE72C4C1ED500D2DE17 /* RoomContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68A50EC92C4C1ED500D2DE17 /* RoomContext.swift */; }; 25 | 68A50EE82C4C1ED500D2DE17 /* ScreenShareSourcePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68A50ED32C4C1ED500D2DE17 /* ScreenShareSourcePickerView.swift */; }; 26 | 68A50EEA2C4C1ED500D2DE17 /* Participant+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68A50ECF2C4C1ED500D2DE17 /* Participant+Helpers.swift */; }; 27 | 68A50EEB2C4C1ED500D2DE17 /* Binding+OptionSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68A50ECB2C4C1ED500D2DE17 /* Binding+OptionSet.swift */; }; 28 | 68A50EEC2C4C1ED500D2DE17 /* ParticipantView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68A50EDA2C4C1ED500D2DE17 /* ParticipantView.swift */; }; 29 | 68A50EED2C4C1ED500D2DE17 /* ConnectionHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68A50ECD2C4C1ED500D2DE17 /* ConnectionHistory.swift */; }; 30 | 68A50EEF2C4C1ED500D2DE17 /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68A50ECC2C4C1ED500D2DE17 /* Bundle.swift */; }; 31 | 68A50EF02C4C1ED500D2DE17 /* AppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68A50EC82C4C1ED500D2DE17 /* AppContext.swift */; }; 32 | 7BBEBA7A2D79103800586EC4 /* RoomContextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBEBA792D79103800586EC4 /* RoomContextView.swift */; }; 33 | 7BBEBA7C2D79104D00586EC4 /* RoomSwitchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBEBA7B2D79104D00586EC4 /* RoomSwitchView.swift */; }; 34 | 7BBEBA832D791CB300586EC4 /* CIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBEBA822D791CAF00586EC4 /* CIImage.swift */; }; 35 | 7BBEBA892D79219600586EC4 /* LKButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBEBA882D79219600586EC4 /* LKButton.swift */; }; 36 | 7BBEBA8B2D7921AA00586EC4 /* LKTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBEBA8A2D7921AA00586EC4 /* LKTextField.swift */; }; 37 | B5BCF77E2CFE7FDE00BCD4D8 /* BroadcastExt.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 683F05F3273F96B20080C7AC /* BroadcastExt.appex */; platformFilters = (ios, tvos, xros, ); settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 38 | B5BCF7842CFE859A00BCD4D8 /* LiveKit in Frameworks */ = {isa = PBXBuildFile; productRef = B5BCF7832CFE859A00BCD4D8 /* LiveKit */; }; 39 | B5C2EF162D0114C800FAC766 /* LiveKitComponents in Frameworks */ = {isa = PBXBuildFile; productRef = B5C2EF152D0114C800FAC766 /* LiveKitComponents */; }; 40 | D7AA477B285A0FFC00EB41AE /* SampleHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AA477A285A0FFC00EB41AE /* SampleHandler.swift */; }; 41 | /* End PBXBuildFile section */ 42 | 43 | /* Begin PBXContainerItemProxy section */ 44 | B5BCF77B2CFE7FD200BCD4D8 /* PBXContainerItemProxy */ = { 45 | isa = PBXContainerItemProxy; 46 | containerPortal = 68B38537271E780600711D5F /* Project object */; 47 | proxyType = 1; 48 | remoteGlobalIDString = 683F05F2273F96B20080C7AC; 49 | remoteInfo = BroadcastExt; 50 | }; 51 | /* End PBXContainerItemProxy section */ 52 | 53 | /* Begin PBXCopyFilesBuildPhase section */ 54 | B5BCF77D2CFE7FD600BCD4D8 /* Embed Foundation Extensions */ = { 55 | isa = PBXCopyFilesBuildPhase; 56 | buildActionMask = 2147483647; 57 | dstPath = ""; 58 | dstSubfolderSpec = 13; 59 | files = ( 60 | B5BCF77E2CFE7FDE00BCD4D8 /* BroadcastExt.appex in Embed Foundation Extensions */, 61 | ); 62 | name = "Embed Foundation Extensions"; 63 | runOnlyForDeploymentPostprocessing = 0; 64 | }; 65 | /* End PBXCopyFilesBuildPhase section */ 66 | 67 | /* Begin PBXFileReference section */ 68 | 6830E6BD2D5BE5DF001C5E83 /* AudioMixerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioMixerView.swift; sourceTree = ""; }; 69 | 683F05F3273F96B20080C7AC /* BroadcastExt.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = BroadcastExt.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 70 | 683F05F4273F96B20080C7AC /* ReplayKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReplayKit.framework; path = System/Library/Frameworks/ReplayKit.framework; sourceTree = SDKROOT; }; 71 | 683F05F9273F96B20080C7AC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 72 | 683F0603273FAD690080C7AC /* iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = iOS.entitlements; sourceTree = ""; }; 73 | 683F0604273FADC20080C7AC /* BroadcastExt.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = BroadcastExt.entitlements; sourceTree = ""; }; 74 | 6865EA2527513B4500FFAFC3 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 75 | 68698E602C4C204800221782 /* Multiplatform-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Multiplatform-Info.plist"; sourceTree = ""; }; 76 | 6888FBE02C66B7B100AB93C1 /* ImmersiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImmersiveView.swift; sourceTree = ""; }; 77 | 68A50E8F2C4C1C4C00D2DE17 /* LiveKitExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LiveKitExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 78 | 68A50EC82C4C1ED500D2DE17 /* AppContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppContext.swift; sourceTree = ""; }; 79 | 68A50EC92C4C1ED500D2DE17 /* RoomContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomContext.swift; sourceTree = ""; }; 80 | 68A50ECB2C4C1ED500D2DE17 /* Binding+OptionSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Binding+OptionSet.swift"; sourceTree = ""; }; 81 | 68A50ECC2C4C1ED500D2DE17 /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = ""; }; 82 | 68A50ECD2C4C1ED500D2DE17 /* ConnectionHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionHistory.swift; sourceTree = ""; }; 83 | 68A50ECE2C4C1ED500D2DE17 /* ExampleRoomMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleRoomMessage.swift; sourceTree = ""; }; 84 | 68A50ECF2C4C1ED500D2DE17 /* Participant+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Participant+Helpers.swift"; sourceTree = ""; }; 85 | 68A50ED02C4C1ED500D2DE17 /* SecureStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureStore.swift; sourceTree = ""; }; 86 | 68A50ED22C4C1ED500D2DE17 /* PublishOptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublishOptionsView.swift; sourceTree = ""; }; 87 | 68A50ED32C4C1ED500D2DE17 /* ScreenShareSourcePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenShareSourcePickerView.swift; sourceTree = ""; }; 88 | 68A50ED52C4C1ED500D2DE17 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 89 | 68A50ED62C4C1ED500D2DE17 /* ConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectView.swift; sourceTree = ""; }; 90 | 68A50ED92C4C1ED500D2DE17 /* LiveKitExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveKitExample.swift; sourceTree = ""; }; 91 | 68A50EDA2C4C1ED500D2DE17 /* ParticipantView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParticipantView.swift; sourceTree = ""; }; 92 | 68A50EDB2C4C1ED500D2DE17 /* RoomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomView.swift; sourceTree = ""; }; 93 | 68A50EDC2C4C1ED500D2DE17 /* SwiftSDK_1.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SwiftSDK_1.entitlements; sourceTree = ""; }; 94 | 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; }; 95 | 7BBEBA792D79103800586EC4 /* RoomContextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomContextView.swift; sourceTree = ""; }; 96 | 7BBEBA7B2D79104D00586EC4 /* RoomSwitchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSwitchView.swift; sourceTree = ""; }; 97 | 7BBEBA822D791CAF00586EC4 /* CIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImage.swift; sourceTree = ""; }; 98 | 7BBEBA882D79219600586EC4 /* LKButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LKButton.swift; sourceTree = ""; }; 99 | 7BBEBA8A2D7921AA00586EC4 /* LKTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LKTextField.swift; sourceTree = ""; }; 100 | 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; }; 101 | D7AA477A285A0FFC00EB41AE /* SampleHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleHandler.swift; sourceTree = ""; }; 102 | /* End PBXFileReference section */ 103 | 104 | /* Begin PBXFrameworksBuildPhase section */ 105 | 683F05F0273F96B20080C7AC /* Frameworks */ = { 106 | isa = PBXFrameworksBuildPhase; 107 | buildActionMask = 2147483647; 108 | files = ( 109 | 681A0AB827D88B190097E3F4 /* ReplayKit.framework in Frameworks */, 110 | 681A0AB727D888D80097E3F4 /* LiveKit in Frameworks */, 111 | ); 112 | runOnlyForDeploymentPostprocessing = 0; 113 | }; 114 | 68A50E8C2C4C1C4C00D2DE17 /* Frameworks */ = { 115 | isa = PBXFrameworksBuildPhase; 116 | buildActionMask = 2147483647; 117 | files = ( 118 | 68019B9C2D00343700721481 /* LiveKit in Frameworks */, 119 | 68698E662C4C219A00221782 /* SFSafeSymbols in Frameworks */, 120 | 68698E642C4C219500221782 /* KeychainAccess in Frameworks */, 121 | B5C2EF162D0114C800FAC766 /* LiveKitComponents in Frameworks */, 122 | B5BCF7842CFE859A00BCD4D8 /* LiveKit in Frameworks */, 123 | ); 124 | runOnlyForDeploymentPostprocessing = 0; 125 | }; 126 | /* End PBXFrameworksBuildPhase section */ 127 | 128 | /* Begin PBXGroup section */ 129 | 681E3F47271FCB40007BB547 /* Frameworks */ = { 130 | isa = PBXGroup; 131 | children = ( 132 | 68DEF27E2919EEFA00258494 /* ReplayKit.framework */, 133 | 9E7835E62751A71500559DEC /* CoreGraphics.framework */, 134 | 683F05F4273F96B20080C7AC /* ReplayKit.framework */, 135 | ); 136 | name = Frameworks; 137 | sourceTree = ""; 138 | }; 139 | 683F05F6273F96B20080C7AC /* BroadcastExt */ = { 140 | isa = PBXGroup; 141 | children = ( 142 | 683F0604273FADC20080C7AC /* BroadcastExt.entitlements */, 143 | 683F05F9273F96B20080C7AC /* Info.plist */, 144 | D7AA477A285A0FFC00EB41AE /* SampleHandler.swift */, 145 | ); 146 | path = BroadcastExt; 147 | sourceTree = ""; 148 | }; 149 | 6865EA2427513B4500FFAFC3 /* iOS */ = { 150 | isa = PBXGroup; 151 | children = ( 152 | 683F05F6273F96B20080C7AC /* BroadcastExt */, 153 | 6865EA2527513B4500FFAFC3 /* Info.plist */, 154 | 683F0603273FAD690080C7AC /* iOS.entitlements */, 155 | ); 156 | path = iOS; 157 | sourceTree = ""; 158 | }; 159 | 68A50ECA2C4C1ED500D2DE17 /* Controllers */ = { 160 | isa = PBXGroup; 161 | children = ( 162 | 68A50EC82C4C1ED500D2DE17 /* AppContext.swift */, 163 | 68A50EC92C4C1ED500D2DE17 /* RoomContext.swift */, 164 | ); 165 | path = Controllers; 166 | sourceTree = ""; 167 | }; 168 | 68A50ED12C4C1ED500D2DE17 /* Support */ = { 169 | isa = PBXGroup; 170 | children = ( 171 | 68A50ECD2C4C1ED500D2DE17 /* ConnectionHistory.swift */, 172 | 68A50ECE2C4C1ED500D2DE17 /* ExampleRoomMessage.swift */, 173 | 68A50ECF2C4C1ED500D2DE17 /* Participant+Helpers.swift */, 174 | 68A50ED02C4C1ED500D2DE17 /* SecureStore.swift */, 175 | ); 176 | path = Support; 177 | sourceTree = ""; 178 | }; 179 | 68A50ED42C4C1ED500D2DE17 /* Views */ = { 180 | isa = PBXGroup; 181 | children = ( 182 | 7BBEBA842D79213900586EC4 /* Shared */, 183 | 7BBEBA7B2D79104D00586EC4 /* RoomSwitchView.swift */, 184 | 7BBEBA792D79103800586EC4 /* RoomContextView.swift */, 185 | 6830E6BD2D5BE5DF001C5E83 /* AudioMixerView.swift */, 186 | 68A50ED22C4C1ED500D2DE17 /* PublishOptionsView.swift */, 187 | 68A50ED32C4C1ED500D2DE17 /* ScreenShareSourcePickerView.swift */, 188 | 6888FBE02C66B7B100AB93C1 /* ImmersiveView.swift */, 189 | 68A50ED62C4C1ED500D2DE17 /* ConnectView.swift */, 190 | 68A50EDA2C4C1ED500D2DE17 /* ParticipantView.swift */, 191 | 68A50EDB2C4C1ED500D2DE17 /* RoomView.swift */, 192 | ); 193 | path = Views; 194 | sourceTree = ""; 195 | }; 196 | 68A50EDE2C4C1ED500D2DE17 /* Multiplatform */ = { 197 | isa = PBXGroup; 198 | children = ( 199 | 68A50ED92C4C1ED500D2DE17 /* LiveKitExample.swift */, 200 | 68A50ED42C4C1ED500D2DE17 /* Views */, 201 | 68A50ECA2C4C1ED500D2DE17 /* Controllers */, 202 | 68A50ED12C4C1ED500D2DE17 /* Support */, 203 | 7BBEBA802D791BDD00586EC4 /* Extensions */, 204 | 68A50ED52C4C1ED500D2DE17 /* Assets.xcassets */, 205 | 68A50EDC2C4C1ED500D2DE17 /* SwiftSDK_1.entitlements */, 206 | ); 207 | path = Multiplatform; 208 | sourceTree = ""; 209 | }; 210 | 68B38536271E780600711D5F = { 211 | isa = PBXGroup; 212 | children = ( 213 | 68A50EDE2C4C1ED500D2DE17 /* Multiplatform */, 214 | 6865EA2427513B4500FFAFC3 /* iOS */, 215 | 68B38544271E780700711D5F /* Products */, 216 | 681E3F47271FCB40007BB547 /* Frameworks */, 217 | 68698E602C4C204800221782 /* Multiplatform-Info.plist */, 218 | ); 219 | sourceTree = ""; 220 | }; 221 | 68B38544271E780700711D5F /* Products */ = { 222 | isa = PBXGroup; 223 | children = ( 224 | 683F05F3273F96B20080C7AC /* BroadcastExt.appex */, 225 | 68A50E8F2C4C1C4C00D2DE17 /* LiveKitExample.app */, 226 | ); 227 | name = Products; 228 | sourceTree = ""; 229 | }; 230 | 7BBEBA802D791BDD00586EC4 /* Extensions */ = { 231 | isa = PBXGroup; 232 | children = ( 233 | 7BBEBA822D791CAF00586EC4 /* CIImage.swift */, 234 | 68A50ECB2C4C1ED500D2DE17 /* Binding+OptionSet.swift */, 235 | 68A50ECC2C4C1ED500D2DE17 /* Bundle.swift */, 236 | ); 237 | path = Extensions; 238 | sourceTree = ""; 239 | }; 240 | 7BBEBA842D79213900586EC4 /* Shared */ = { 241 | isa = PBXGroup; 242 | children = ( 243 | 7BBEBA8A2D7921AA00586EC4 /* LKTextField.swift */, 244 | 7BBEBA882D79219600586EC4 /* LKButton.swift */, 245 | ); 246 | path = Shared; 247 | sourceTree = ""; 248 | }; 249 | /* End PBXGroup section */ 250 | 251 | /* Begin PBXNativeTarget section */ 252 | 683F05F2273F96B20080C7AC /* BroadcastExt */ = { 253 | isa = PBXNativeTarget; 254 | buildConfigurationList = 683F05FD273F96B20080C7AC /* Build configuration list for PBXNativeTarget "BroadcastExt" */; 255 | buildPhases = ( 256 | 683F05EF273F96B20080C7AC /* Sources */, 257 | 683F05F0273F96B20080C7AC /* Frameworks */, 258 | 683F05F1273F96B20080C7AC /* Resources */, 259 | ); 260 | buildRules = ( 261 | ); 262 | dependencies = ( 263 | ); 264 | name = BroadcastExt; 265 | packageProductDependencies = ( 266 | 681A0AB627D888D80097E3F4 /* LiveKit */, 267 | ); 268 | productName = BroadcastExt; 269 | productReference = 683F05F3273F96B20080C7AC /* BroadcastExt.appex */; 270 | productType = "com.apple.product-type.app-extension"; 271 | }; 272 | 68A50E8E2C4C1C4C00D2DE17 /* LiveKitExample */ = { 273 | isa = PBXNativeTarget; 274 | buildConfigurationList = 68A50E9D2C4C1C4D00D2DE17 /* Build configuration list for PBXNativeTarget "LiveKitExample" */; 275 | buildPhases = ( 276 | 68A50E8B2C4C1C4C00D2DE17 /* Sources */, 277 | 68A50E8C2C4C1C4C00D2DE17 /* Frameworks */, 278 | 68A50E8D2C4C1C4C00D2DE17 /* Resources */, 279 | B5BCF77D2CFE7FD600BCD4D8 /* Embed Foundation Extensions */, 280 | ); 281 | buildRules = ( 282 | ); 283 | dependencies = ( 284 | B5BCF77C2CFE7FD200BCD4D8 /* PBXTargetDependency */, 285 | ); 286 | name = LiveKitExample; 287 | packageProductDependencies = ( 288 | 68698E612C4C218B00221782 /* LiveKit */, 289 | 68698E632C4C219500221782 /* KeychainAccess */, 290 | 68698E652C4C219A00221782 /* SFSafeSymbols */, 291 | B5BCF7832CFE859A00BCD4D8 /* LiveKit */, 292 | B5C2EF152D0114C800FAC766 /* LiveKitComponents */, 293 | ); 294 | productName = SwiftSDK.1; 295 | productReference = 68A50E8F2C4C1C4C00D2DE17 /* LiveKitExample.app */; 296 | productType = "com.apple.product-type.application"; 297 | }; 298 | /* End PBXNativeTarget section */ 299 | 300 | /* Begin PBXProject section */ 301 | 68B38537271E780600711D5F /* Project object */ = { 302 | isa = PBXProject; 303 | attributes = { 304 | BuildIndependentTargetsInParallel = 1; 305 | LastSwiftUpdateCheck = 1600; 306 | LastUpgradeCheck = 1500; 307 | TargetAttributes = { 308 | 683F05F2273F96B20080C7AC = { 309 | CreatedOnToolsVersion = 13.1; 310 | }; 311 | 68A50E8E2C4C1C4C00D2DE17 = { 312 | CreatedOnToolsVersion = 16.0; 313 | }; 314 | }; 315 | }; 316 | buildConfigurationList = 68B3853A271E780600711D5F /* Build configuration list for PBXProject "LiveKitExample" */; 317 | compatibilityVersion = "Xcode 14.0"; 318 | developmentRegion = en; 319 | hasScannedForEncodings = 0; 320 | knownRegions = ( 321 | en, 322 | Base, 323 | ); 324 | mainGroup = 68B38536271E780600711D5F; 325 | packageReferences = ( 326 | 685271E727407BBC006B4D6A /* XCRemoteSwiftPackageReference "swift-protobuf" */, 327 | 680FE2F027A8EF7700B6F6DB /* XCRemoteSwiftPackageReference "SFSafeSymbols" */, 328 | 68816CBF27B4D6BC00E24622 /* XCRemoteSwiftPackageReference "KeychainAccess" */, 329 | B5BCF7852CFE8A7400BCD4D8 /* XCRemoteSwiftPackageReference "client-sdk-swift" */, 330 | B5C2EF142D0114C800FAC766 /* XCRemoteSwiftPackageReference "components-swift" */, 331 | ); 332 | productRefGroup = 68B38544271E780700711D5F /* Products */; 333 | projectDirPath = ""; 334 | projectRoot = ""; 335 | targets = ( 336 | 683F05F2273F96B20080C7AC /* BroadcastExt */, 337 | 68A50E8E2C4C1C4C00D2DE17 /* LiveKitExample */, 338 | ); 339 | }; 340 | /* End PBXProject section */ 341 | 342 | /* Begin PBXResourcesBuildPhase section */ 343 | 683F05F1273F96B20080C7AC /* Resources */ = { 344 | isa = PBXResourcesBuildPhase; 345 | buildActionMask = 2147483647; 346 | files = ( 347 | ); 348 | runOnlyForDeploymentPostprocessing = 0; 349 | }; 350 | 68A50E8D2C4C1C4C00D2DE17 /* Resources */ = { 351 | isa = PBXResourcesBuildPhase; 352 | buildActionMask = 2147483647; 353 | files = ( 354 | 68A50EDF2C4C1ED500D2DE17 /* Assets.xcassets in Resources */, 355 | ); 356 | runOnlyForDeploymentPostprocessing = 0; 357 | }; 358 | /* End PBXResourcesBuildPhase section */ 359 | 360 | /* Begin PBXSourcesBuildPhase section */ 361 | 683F05EF273F96B20080C7AC /* Sources */ = { 362 | isa = PBXSourcesBuildPhase; 363 | buildActionMask = 2147483647; 364 | files = ( 365 | D7AA477B285A0FFC00EB41AE /* SampleHandler.swift in Sources */, 366 | ); 367 | runOnlyForDeploymentPostprocessing = 0; 368 | }; 369 | 68A50E8B2C4C1C4C00D2DE17 /* Sources */ = { 370 | isa = PBXSourcesBuildPhase; 371 | buildActionMask = 2147483647; 372 | files = ( 373 | 68A50EE02C4C1ED500D2DE17 /* LiveKitExample.swift in Sources */, 374 | 6830E6BE2D5BE5E2001C5E83 /* AudioMixerView.swift in Sources */, 375 | 7BBEBA7A2D79103800586EC4 /* RoomContextView.swift in Sources */, 376 | 68A50EE22C4C1ED500D2DE17 /* RoomView.swift in Sources */, 377 | 68A50EE32C4C1ED500D2DE17 /* PublishOptionsView.swift in Sources */, 378 | 68A50EE42C4C1ED500D2DE17 /* SecureStore.swift in Sources */, 379 | 68A50EE52C4C1ED500D2DE17 /* ConnectView.swift in Sources */, 380 | 7BBEBA8B2D7921AA00586EC4 /* LKTextField.swift in Sources */, 381 | 68A50EE62C4C1ED500D2DE17 /* ExampleRoomMessage.swift in Sources */, 382 | 68A50EE72C4C1ED500D2DE17 /* RoomContext.swift in Sources */, 383 | 68A50EE82C4C1ED500D2DE17 /* ScreenShareSourcePickerView.swift in Sources */, 384 | 68A50EEA2C4C1ED500D2DE17 /* Participant+Helpers.swift in Sources */, 385 | 68A50EEB2C4C1ED500D2DE17 /* Binding+OptionSet.swift in Sources */, 386 | 6888FBE12C66B7B400AB93C1 /* ImmersiveView.swift in Sources */, 387 | 68A50EEC2C4C1ED500D2DE17 /* ParticipantView.swift in Sources */, 388 | 7BBEBA832D791CB300586EC4 /* CIImage.swift in Sources */, 389 | 68A50EED2C4C1ED500D2DE17 /* ConnectionHistory.swift in Sources */, 390 | 7BBEBA7C2D79104D00586EC4 /* RoomSwitchView.swift in Sources */, 391 | 68A50EEF2C4C1ED500D2DE17 /* Bundle.swift in Sources */, 392 | 7BBEBA892D79219600586EC4 /* LKButton.swift in Sources */, 393 | 68A50EF02C4C1ED500D2DE17 /* AppContext.swift in Sources */, 394 | ); 395 | runOnlyForDeploymentPostprocessing = 0; 396 | }; 397 | /* End PBXSourcesBuildPhase section */ 398 | 399 | /* Begin PBXTargetDependency section */ 400 | B5BCF77C2CFE7FD200BCD4D8 /* PBXTargetDependency */ = { 401 | isa = PBXTargetDependency; 402 | platformFilters = ( 403 | ios, 404 | tvos, 405 | xros, 406 | ); 407 | target = 683F05F2273F96B20080C7AC /* BroadcastExt */; 408 | targetProxy = B5BCF77B2CFE7FD200BCD4D8 /* PBXContainerItemProxy */; 409 | }; 410 | /* End PBXTargetDependency section */ 411 | 412 | /* Begin XCBuildConfiguration section */ 413 | 683F05FE273F96B20080C7AC /* Debug */ = { 414 | isa = XCBuildConfiguration; 415 | buildSettings = { 416 | CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/iOS/BroadcastExt/BroadcastExt.entitlements"; 417 | CURRENT_PROJECT_VERSION = 2025032401; 418 | DEVELOPMENT_TEAM = 76TVFCUKK7; 419 | ENABLE_HARDENED_RUNTIME = NO; 420 | INFOPLIST_FILE = "$(SRCROOT)/iOS/BroadcastExt/Info.plist"; 421 | INFOPLIST_KEY_CFBundleDisplayName = BroadcastExt; 422 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 423 | LD_RUNPATH_SEARCH_PATHS = ( 424 | "$(inherited)", 425 | "@executable_path/Frameworks", 426 | "@executable_path/../../Frameworks", 427 | ); 428 | PRODUCT_BUNDLE_IDENTIFIER = "io.livekit.example.SwiftSDK.1.broadcast-ext"; 429 | PRODUCT_NAME = "$(TARGET_NAME)"; 430 | REGISTER_APP_GROUPS = NO; 431 | SDKROOT = iphoneos; 432 | SKIP_INSTALL = YES; 433 | SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator xros xrsimulator"; 434 | SUPPORTS_MACCATALYST = NO; 435 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 436 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; 437 | SWIFT_EMIT_LOC_STRINGS = YES; 438 | TARGETED_DEVICE_FAMILY = "1,2,3,7"; 439 | }; 440 | name = Debug; 441 | }; 442 | 683F05FF273F96B20080C7AC /* Release */ = { 443 | isa = XCBuildConfiguration; 444 | buildSettings = { 445 | CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/iOS/BroadcastExt/BroadcastExt.entitlements"; 446 | CURRENT_PROJECT_VERSION = 2025032401; 447 | DEVELOPMENT_TEAM = 76TVFCUKK7; 448 | ENABLE_HARDENED_RUNTIME = NO; 449 | INFOPLIST_FILE = "$(SRCROOT)/iOS/BroadcastExt/Info.plist"; 450 | INFOPLIST_KEY_CFBundleDisplayName = BroadcastExt; 451 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 452 | LD_RUNPATH_SEARCH_PATHS = ( 453 | "$(inherited)", 454 | "@executable_path/Frameworks", 455 | "@executable_path/../../Frameworks", 456 | ); 457 | PRODUCT_BUNDLE_IDENTIFIER = "io.livekit.example.SwiftSDK.1.broadcast-ext"; 458 | PRODUCT_NAME = "$(TARGET_NAME)"; 459 | REGISTER_APP_GROUPS = NO; 460 | SDKROOT = iphoneos; 461 | SKIP_INSTALL = YES; 462 | SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator xros xrsimulator"; 463 | SUPPORTS_MACCATALYST = NO; 464 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 465 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; 466 | SWIFT_EMIT_LOC_STRINGS = YES; 467 | TARGETED_DEVICE_FAMILY = "1,2,3,7"; 468 | VALIDATE_PRODUCT = YES; 469 | }; 470 | name = Release; 471 | }; 472 | 68A50E9B2C4C1C4D00D2DE17 /* Debug */ = { 473 | isa = XCBuildConfiguration; 474 | buildSettings = { 475 | ASSETCATALOG_COMPILER_APPICON_NAME = ""; 476 | "ASSETCATALOG_COMPILER_APPICON_NAME[sdk=iphoneos*]" = iOS; 477 | "ASSETCATALOG_COMPILER_APPICON_NAME[sdk=macosx*]" = macOS; 478 | "ASSETCATALOG_COMPILER_APPICON_NAME[sdk=xros*]" = visionOS; 479 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 480 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 481 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 482 | CODE_SIGN_ENTITLEMENTS = Multiplatform/SwiftSDK_1.entitlements; 483 | CODE_SIGN_STYLE = Automatic; 484 | CURRENT_PROJECT_VERSION = 2025032401; 485 | DEVELOPMENT_TEAM = 76TVFCUKK7; 486 | ENABLE_HARDENED_RUNTIME = YES; 487 | ENABLE_PREVIEWS = YES; 488 | GCC_C_LANGUAGE_STANDARD = gnu17; 489 | GCC_PREPROCESSOR_DEFINITIONS = ( 490 | "DEBUG=1", 491 | "$(inherited)", 492 | ); 493 | GENERATE_INFOPLIST_FILE = YES; 494 | INFOPLIST_FILE = "Multiplatform-Info.plist"; 495 | INFOPLIST_KEY_NSCameraUsageDescription = "Please allow for camera access"; 496 | INFOPLIST_KEY_NSMicrophoneUsageDescription = "Please allow for microphone access"; 497 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 498 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 499 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 500 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 501 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 502 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 503 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 504 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 505 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 506 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 507 | INFOPLIST_KEY_UIUserInterfaceStyle = Dark; 508 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 509 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 510 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 511 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 512 | MACOSX_DEPLOYMENT_TARGET = 12.0; 513 | PRODUCT_BUNDLE_IDENTIFIER = io.livekit.example.SwiftSDK.1; 514 | PRODUCT_NAME = "$(TARGET_NAME)"; 515 | REGISTER_APP_GROUPS = NO; 516 | SDKROOT = auto; 517 | SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx xros xrsimulator"; 518 | SUPPORTS_MACCATALYST = YES; 519 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 520 | SWIFT_EMIT_LOC_STRINGS = YES; 521 | TARGETED_DEVICE_FAMILY = "1,2,3,7"; 522 | TVOS_DEPLOYMENT_TARGET = 17.0; 523 | XROS_DEPLOYMENT_TARGET = 1.0; 524 | }; 525 | name = Debug; 526 | }; 527 | 68A50E9C2C4C1C4D00D2DE17 /* Release */ = { 528 | isa = XCBuildConfiguration; 529 | buildSettings = { 530 | ASSETCATALOG_COMPILER_APPICON_NAME = ""; 531 | "ASSETCATALOG_COMPILER_APPICON_NAME[sdk=iphoneos*]" = iOS; 532 | "ASSETCATALOG_COMPILER_APPICON_NAME[sdk=macosx*]" = macOS; 533 | "ASSETCATALOG_COMPILER_APPICON_NAME[sdk=xros*]" = visionOS; 534 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 535 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 536 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 537 | CODE_SIGN_ENTITLEMENTS = Multiplatform/SwiftSDK_1.entitlements; 538 | CODE_SIGN_STYLE = Automatic; 539 | CURRENT_PROJECT_VERSION = 2025032401; 540 | DEVELOPMENT_TEAM = 76TVFCUKK7; 541 | ENABLE_HARDENED_RUNTIME = YES; 542 | ENABLE_PREVIEWS = YES; 543 | GCC_C_LANGUAGE_STANDARD = gnu17; 544 | GENERATE_INFOPLIST_FILE = YES; 545 | INFOPLIST_FILE = "Multiplatform-Info.plist"; 546 | INFOPLIST_KEY_NSCameraUsageDescription = "Please allow for camera access"; 547 | INFOPLIST_KEY_NSMicrophoneUsageDescription = "Please allow for microphone access"; 548 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 549 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 550 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 551 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 552 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 553 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 554 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 555 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 556 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 557 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 558 | INFOPLIST_KEY_UIUserInterfaceStyle = Dark; 559 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 560 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 561 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 562 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 563 | MACOSX_DEPLOYMENT_TARGET = 12.0; 564 | PRODUCT_BUNDLE_IDENTIFIER = io.livekit.example.SwiftSDK.1; 565 | PRODUCT_NAME = "$(TARGET_NAME)"; 566 | REGISTER_APP_GROUPS = NO; 567 | SDKROOT = auto; 568 | SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx xros xrsimulator"; 569 | SUPPORTS_MACCATALYST = YES; 570 | SWIFT_EMIT_LOC_STRINGS = YES; 571 | TARGETED_DEVICE_FAMILY = "1,2,3,7"; 572 | TVOS_DEPLOYMENT_TARGET = 17.0; 573 | XROS_DEPLOYMENT_TARGET = 1.0; 574 | }; 575 | name = Release; 576 | }; 577 | 68B38552271E780700711D5F /* Debug */ = { 578 | isa = XCBuildConfiguration; 579 | buildSettings = { 580 | ALWAYS_SEARCH_USER_PATHS = NO; 581 | CLANG_ANALYZER_NONNULL = YES; 582 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 583 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 584 | CLANG_CXX_LIBRARY = "libc++"; 585 | CLANG_ENABLE_MODULES = YES; 586 | CLANG_ENABLE_OBJC_ARC = YES; 587 | CLANG_ENABLE_OBJC_WEAK = YES; 588 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 589 | CLANG_WARN_BOOL_CONVERSION = YES; 590 | CLANG_WARN_COMMA = YES; 591 | CLANG_WARN_CONSTANT_CONVERSION = YES; 592 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 593 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 594 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 595 | CLANG_WARN_EMPTY_BODY = YES; 596 | CLANG_WARN_ENUM_CONVERSION = YES; 597 | CLANG_WARN_INFINITE_RECURSION = YES; 598 | CLANG_WARN_INT_CONVERSION = YES; 599 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 600 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 601 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 602 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 603 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 604 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 605 | CLANG_WARN_STRICT_PROTOTYPES = YES; 606 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 607 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 608 | CLANG_WARN_UNREACHABLE_CODE = YES; 609 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 610 | COPY_PHASE_STRIP = NO; 611 | DEAD_CODE_STRIPPING = YES; 612 | DEBUG_INFORMATION_FORMAT = dwarf; 613 | ENABLE_BITCODE = NO; 614 | ENABLE_STRICT_OBJC_MSGSEND = YES; 615 | ENABLE_TESTABILITY = YES; 616 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 617 | GCC_C_LANGUAGE_STANDARD = gnu11; 618 | GCC_DYNAMIC_NO_PIC = NO; 619 | GCC_NO_COMMON_BLOCKS = YES; 620 | GCC_OPTIMIZATION_LEVEL = 0; 621 | GCC_PREPROCESSOR_DEFINITIONS = ( 622 | "DEBUG=1", 623 | "GL_SILENCE_DEPRECATION=1", 624 | ); 625 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 626 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 627 | GCC_WARN_UNDECLARED_SELECTOR = YES; 628 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 629 | GCC_WARN_UNUSED_FUNCTION = YES; 630 | GCC_WARN_UNUSED_VARIABLE = YES; 631 | MARKETING_VERSION = 2.5.0; 632 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 633 | MTL_FAST_MATH = YES; 634 | ONLY_ACTIVE_ARCH = YES; 635 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 636 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 637 | SWIFT_STRICT_CONCURRENCY = complete; 638 | SWIFT_VERSION = 6.0; 639 | }; 640 | name = Debug; 641 | }; 642 | 68B38553271E780700711D5F /* Release */ = { 643 | isa = XCBuildConfiguration; 644 | buildSettings = { 645 | ALWAYS_SEARCH_USER_PATHS = NO; 646 | CLANG_ANALYZER_NONNULL = YES; 647 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 648 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 649 | CLANG_CXX_LIBRARY = "libc++"; 650 | CLANG_ENABLE_MODULES = YES; 651 | CLANG_ENABLE_OBJC_ARC = YES; 652 | CLANG_ENABLE_OBJC_WEAK = YES; 653 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 654 | CLANG_WARN_BOOL_CONVERSION = YES; 655 | CLANG_WARN_COMMA = YES; 656 | CLANG_WARN_CONSTANT_CONVERSION = YES; 657 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 658 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 659 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 660 | CLANG_WARN_EMPTY_BODY = YES; 661 | CLANG_WARN_ENUM_CONVERSION = YES; 662 | CLANG_WARN_INFINITE_RECURSION = YES; 663 | CLANG_WARN_INT_CONVERSION = YES; 664 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 665 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 666 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 667 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 668 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 669 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 670 | CLANG_WARN_STRICT_PROTOTYPES = YES; 671 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 672 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 673 | CLANG_WARN_UNREACHABLE_CODE = YES; 674 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 675 | COPY_PHASE_STRIP = NO; 676 | DEAD_CODE_STRIPPING = YES; 677 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 678 | ENABLE_BITCODE = NO; 679 | ENABLE_NS_ASSERTIONS = NO; 680 | ENABLE_STRICT_OBJC_MSGSEND = YES; 681 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 682 | GCC_C_LANGUAGE_STANDARD = gnu11; 683 | GCC_NO_COMMON_BLOCKS = YES; 684 | GCC_PREPROCESSOR_DEFINITIONS = "GL_SILENCE_DEPRECATION=1"; 685 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 686 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 687 | GCC_WARN_UNDECLARED_SELECTOR = YES; 688 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 689 | GCC_WARN_UNUSED_FUNCTION = YES; 690 | GCC_WARN_UNUSED_VARIABLE = YES; 691 | MARKETING_VERSION = 2.5.0; 692 | MTL_ENABLE_DEBUG_INFO = NO; 693 | MTL_FAST_MATH = YES; 694 | SWIFT_COMPILATION_MODE = wholemodule; 695 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 696 | SWIFT_STRICT_CONCURRENCY = complete; 697 | SWIFT_VERSION = 6.0; 698 | }; 699 | name = Release; 700 | }; 701 | /* End XCBuildConfiguration section */ 702 | 703 | /* Begin XCConfigurationList section */ 704 | 683F05FD273F96B20080C7AC /* Build configuration list for PBXNativeTarget "BroadcastExt" */ = { 705 | isa = XCConfigurationList; 706 | buildConfigurations = ( 707 | 683F05FE273F96B20080C7AC /* Debug */, 708 | 683F05FF273F96B20080C7AC /* Release */, 709 | ); 710 | defaultConfigurationIsVisible = 0; 711 | defaultConfigurationName = Release; 712 | }; 713 | 68A50E9D2C4C1C4D00D2DE17 /* Build configuration list for PBXNativeTarget "LiveKitExample" */ = { 714 | isa = XCConfigurationList; 715 | buildConfigurations = ( 716 | 68A50E9B2C4C1C4D00D2DE17 /* Debug */, 717 | 68A50E9C2C4C1C4D00D2DE17 /* Release */, 718 | ); 719 | defaultConfigurationIsVisible = 0; 720 | defaultConfigurationName = Release; 721 | }; 722 | 68B3853A271E780600711D5F /* Build configuration list for PBXProject "LiveKitExample" */ = { 723 | isa = XCConfigurationList; 724 | buildConfigurations = ( 725 | 68B38552271E780700711D5F /* Debug */, 726 | 68B38553271E780700711D5F /* Release */, 727 | ); 728 | defaultConfigurationIsVisible = 0; 729 | defaultConfigurationName = Release; 730 | }; 731 | /* End XCConfigurationList section */ 732 | 733 | /* Begin XCRemoteSwiftPackageReference section */ 734 | 680FE2F027A8EF7700B6F6DB /* XCRemoteSwiftPackageReference "SFSafeSymbols" */ = { 735 | isa = XCRemoteSwiftPackageReference; 736 | repositoryURL = "https://github.com/SFSafeSymbols/SFSafeSymbols"; 737 | requirement = { 738 | kind = upToNextMajorVersion; 739 | minimumVersion = 4.1.1; 740 | }; 741 | }; 742 | 685271E727407BBC006B4D6A /* XCRemoteSwiftPackageReference "swift-protobuf" */ = { 743 | isa = XCRemoteSwiftPackageReference; 744 | repositoryURL = "https://github.com/apple/swift-protobuf.git"; 745 | requirement = { 746 | kind = upToNextMajorVersion; 747 | minimumVersion = 1.26.0; 748 | }; 749 | }; 750 | 68816CBF27B4D6BC00E24622 /* XCRemoteSwiftPackageReference "KeychainAccess" */ = { 751 | isa = XCRemoteSwiftPackageReference; 752 | repositoryURL = "https://github.com/kishikawakatsumi/KeychainAccess.git"; 753 | requirement = { 754 | kind = upToNextMajorVersion; 755 | minimumVersion = 4.2.2; 756 | }; 757 | }; 758 | B5BCF7852CFE8A7400BCD4D8 /* XCRemoteSwiftPackageReference "client-sdk-swift" */ = { 759 | isa = XCRemoteSwiftPackageReference; 760 | repositoryURL = "https://github.com/livekit/client-sdk-swift"; 761 | requirement = { 762 | kind = exactVersion; 763 | version = 2.6.0; 764 | }; 765 | }; 766 | B5C2EF142D0114C800FAC766 /* XCRemoteSwiftPackageReference "components-swift" */ = { 767 | isa = XCRemoteSwiftPackageReference; 768 | repositoryURL = "https://github.com/livekit/components-swift"; 769 | requirement = { 770 | kind = upToNextMajorVersion; 771 | minimumVersion = 0.1.3; 772 | }; 773 | }; 774 | /* End XCRemoteSwiftPackageReference section */ 775 | 776 | /* Begin XCSwiftPackageProductDependency section */ 777 | 681A0AB627D888D80097E3F4 /* LiveKit */ = { 778 | isa = XCSwiftPackageProductDependency; 779 | productName = LiveKit; 780 | }; 781 | 68698E612C4C218B00221782 /* LiveKit */ = { 782 | isa = XCSwiftPackageProductDependency; 783 | productName = LiveKit; 784 | }; 785 | 68698E632C4C219500221782 /* KeychainAccess */ = { 786 | isa = XCSwiftPackageProductDependency; 787 | package = 68816CBF27B4D6BC00E24622 /* XCRemoteSwiftPackageReference "KeychainAccess" */; 788 | productName = KeychainAccess; 789 | }; 790 | 68698E652C4C219A00221782 /* SFSafeSymbols */ = { 791 | isa = XCSwiftPackageProductDependency; 792 | package = 680FE2F027A8EF7700B6F6DB /* XCRemoteSwiftPackageReference "SFSafeSymbols" */; 793 | productName = SFSafeSymbols; 794 | }; 795 | B5BCF7832CFE859A00BCD4D8 /* LiveKit */ = { 796 | isa = XCSwiftPackageProductDependency; 797 | productName = LiveKit; 798 | }; 799 | B5C2EF152D0114C800FAC766 /* LiveKitComponents */ = { 800 | isa = XCSwiftPackageProductDependency; 801 | package = B5C2EF142D0114C800FAC766 /* XCRemoteSwiftPackageReference "components-swift" */; 802 | productName = LiveKitComponents; 803 | }; 804 | /* End XCSwiftPackageProductDependency section */ 805 | }; 806 | rootObject = 68B38537271E780600711D5F /* Project object */; 807 | } 808 | -------------------------------------------------------------------------------- /LiveKitExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LiveKitExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LiveKitExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "13374db9287c214b22866feada045d9d68da94732a6ae5329b848f959271a91c", 3 | "pins" : [ 4 | { 5 | "identity" : "client-sdk-swift", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/livekit/client-sdk-swift", 8 | "state" : { 9 | "revision" : "d8995b7758cf2174488a4b143b0a2ef08e315719", 10 | "version" : "2.6.0" 11 | } 12 | }, 13 | { 14 | "identity" : "components-swift", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/livekit/components-swift", 17 | "state" : { 18 | "revision" : "8376284cd357829dd16a8b25ffd809a1dfcce9ff", 19 | "version" : "0.1.3" 20 | } 21 | }, 22 | { 23 | "identity" : "keychainaccess", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/kishikawakatsumi/KeychainAccess.git", 26 | "state" : { 27 | "revision" : "84e546727d66f1adc5439debad16270d0fdd04e7", 28 | "version" : "4.2.2" 29 | } 30 | }, 31 | { 32 | "identity" : "sfsafesymbols", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/SFSafeSymbols/SFSafeSymbols", 35 | "state" : { 36 | "revision" : "7cca2d60925876b5953a2cf7341cd80fbeac983c", 37 | "version" : "4.1.1" 38 | } 39 | }, 40 | { 41 | "identity" : "swift-collections", 42 | "kind" : "remoteSourceControl", 43 | "location" : "https://github.com/apple/swift-collections.git", 44 | "state" : { 45 | "revision" : "c1805596154bb3a265fd91b8ac0c4433b4348fb0", 46 | "version" : "1.2.0" 47 | } 48 | }, 49 | { 50 | "identity" : "swift-log", 51 | "kind" : "remoteSourceControl", 52 | "location" : "https://github.com/apple/swift-log.git", 53 | "state" : { 54 | "revision" : "3d8596ed08bd13520157f0355e35caed215ffbfa", 55 | "version" : "1.6.3" 56 | } 57 | }, 58 | { 59 | "identity" : "swift-protobuf", 60 | "kind" : "remoteSourceControl", 61 | "location" : "https://github.com/apple/swift-protobuf.git", 62 | "state" : { 63 | "revision" : "d72aed98f8253ec1aa9ea1141e28150f408cf17f", 64 | "version" : "1.29.0" 65 | } 66 | }, 67 | { 68 | "identity" : "webrtc-xcframework", 69 | "kind" : "remoteSourceControl", 70 | "location" : "https://github.com/livekit/webrtc-xcframework.git", 71 | "state" : { 72 | "revision" : "f2c6bd1e65a7ef59c4cd7ee6ba4eb2f96fee2b8a", 73 | "version" : "125.6422.29" 74 | } 75 | } 76 | ], 77 | "version" : 3 78 | } 79 | -------------------------------------------------------------------------------- /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.xcodeproj/xcshareddata/xcschemes/SwiftSDK.1.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Multiplatform-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ITSAppUsesNonExemptEncryption 6 | 7 | NSEnterpriseMCAMUsageDescription 8 | Please allow for main camera access 9 | NSHandsTrackingUsageDescription 10 | Please allow for main camera access 11 | NSWorldSensingUsageDescription 12 | Please allow for main camera access 13 | UIApplicationSceneManifest 14 | 15 | UIApplicationPreferredDefaultSceneSessionRole 16 | UIWindowSceneSessionRoleApplication 17 | UIApplicationSupportsMultipleScenes 18 | 19 | UISceneConfigurations 20 | 21 | 22 | UIUserInterfaceStyle 23 | Dark 24 | UIBackgroundModes 25 | 26 | audio 27 | voip 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Multiplatform/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 | -------------------------------------------------------------------------------- /Multiplatform/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Multiplatform/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 | -------------------------------------------------------------------------------- /Multiplatform/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 | -------------------------------------------------------------------------------- /Multiplatform/Assets.xcassets/MacOS.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "LiveKit-16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "LiveKit-32.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "LiveKit-32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "LiveKit-64.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "LiveKit-128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "LiveKit-256.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "LiveKit-256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "LiveKit-512.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "LiveKit-512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "LiveKit-1024.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Multiplatform/Assets.xcassets/iOS.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "LiveKit.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Multiplatform/Assets.xcassets/iOS.appiconset/LiveKit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/swift-example/211a84531cbf147304751a7730f6a647c84f4b4d/Multiplatform/Assets.xcassets/iOS.appiconset/LiveKit.png -------------------------------------------------------------------------------- /Multiplatform/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 | -------------------------------------------------------------------------------- /Multiplatform/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 | -------------------------------------------------------------------------------- /Multiplatform/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 | -------------------------------------------------------------------------------- /Multiplatform/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 | -------------------------------------------------------------------------------- /Multiplatform/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 | -------------------------------------------------------------------------------- /Multiplatform/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 | -------------------------------------------------------------------------------- /Multiplatform/Assets.xcassets/logo.imageset/Livekit Wordmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/swift-example/211a84531cbf147304751a7730f6a647c84f4b4d/Multiplatform/Assets.xcassets/logo.imageset/Livekit Wordmark.png -------------------------------------------------------------------------------- /Multiplatform/Assets.xcassets/logo.imageset/Livekit Wordmark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/swift-example/211a84531cbf147304751a7730f6a647c84f4b4d/Multiplatform/Assets.xcassets/logo.imageset/Livekit Wordmark@2x.png -------------------------------------------------------------------------------- /Multiplatform/Assets.xcassets/logo.imageset/Livekit Wordmark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/swift-example/211a84531cbf147304751a7730f6a647c84f4b4d/Multiplatform/Assets.xcassets/logo.imageset/Livekit Wordmark@3x.png -------------------------------------------------------------------------------- /Multiplatform/Assets.xcassets/macOS.appiconset/LiveKit-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/swift-example/211a84531cbf147304751a7730f6a647c84f4b4d/Multiplatform/Assets.xcassets/macOS.appiconset/LiveKit-1024.png -------------------------------------------------------------------------------- /Multiplatform/Assets.xcassets/macOS.appiconset/LiveKit-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/swift-example/211a84531cbf147304751a7730f6a647c84f4b4d/Multiplatform/Assets.xcassets/macOS.appiconset/LiveKit-128.png -------------------------------------------------------------------------------- /Multiplatform/Assets.xcassets/macOS.appiconset/LiveKit-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/swift-example/211a84531cbf147304751a7730f6a647c84f4b4d/Multiplatform/Assets.xcassets/macOS.appiconset/LiveKit-16.png -------------------------------------------------------------------------------- /Multiplatform/Assets.xcassets/macOS.appiconset/LiveKit-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/swift-example/211a84531cbf147304751a7730f6a647c84f4b4d/Multiplatform/Assets.xcassets/macOS.appiconset/LiveKit-256.png -------------------------------------------------------------------------------- /Multiplatform/Assets.xcassets/macOS.appiconset/LiveKit-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/swift-example/211a84531cbf147304751a7730f6a647c84f4b4d/Multiplatform/Assets.xcassets/macOS.appiconset/LiveKit-32.png -------------------------------------------------------------------------------- /Multiplatform/Assets.xcassets/macOS.appiconset/LiveKit-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/swift-example/211a84531cbf147304751a7730f6a647c84f4b4d/Multiplatform/Assets.xcassets/macOS.appiconset/LiveKit-512.png -------------------------------------------------------------------------------- /Multiplatform/Assets.xcassets/macOS.appiconset/LiveKit-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/swift-example/211a84531cbf147304751a7730f6a647c84f4b4d/Multiplatform/Assets.xcassets/macOS.appiconset/LiveKit-64.png -------------------------------------------------------------------------------- /Multiplatform/Assets.xcassets/visionOS.solidimagestack/Back.solidimagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "LiveKit-1024.png", 5 | "idiom" : "vision", 6 | "scale" : "2x" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Multiplatform/Assets.xcassets/visionOS.solidimagestack/Back.solidimagestacklayer/Content.imageset/LiveKit-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/swift-example/211a84531cbf147304751a7730f6a647c84f4b4d/Multiplatform/Assets.xcassets/visionOS.solidimagestack/Back.solidimagestacklayer/Content.imageset/LiveKit-1024.png -------------------------------------------------------------------------------- /Multiplatform/Assets.xcassets/visionOS.solidimagestack/Back.solidimagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Multiplatform/Assets.xcassets/visionOS.solidimagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "layers" : [ 7 | { 8 | "filename" : "Front.solidimagestacklayer" 9 | }, 10 | { 11 | "filename" : "Middle.solidimagestacklayer" 12 | }, 13 | { 14 | "filename" : "Back.solidimagestacklayer" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /Multiplatform/Assets.xcassets/visionOS.solidimagestack/Front.solidimagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "LiveKit-1024.png", 5 | "idiom" : "vision", 6 | "scale" : "2x" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Multiplatform/Assets.xcassets/visionOS.solidimagestack/Front.solidimagestacklayer/Content.imageset/LiveKit-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/swift-example/211a84531cbf147304751a7730f6a647c84f4b4d/Multiplatform/Assets.xcassets/visionOS.solidimagestack/Front.solidimagestacklayer/Content.imageset/LiveKit-1024.png -------------------------------------------------------------------------------- /Multiplatform/Assets.xcassets/visionOS.solidimagestack/Front.solidimagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Multiplatform/Assets.xcassets/visionOS.solidimagestack/Middle.solidimagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "LiveKit-1024.png", 5 | "idiom" : "vision", 6 | "scale" : "2x" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Multiplatform/Assets.xcassets/visionOS.solidimagestack/Middle.solidimagestacklayer/Content.imageset/LiveKit-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/swift-example/211a84531cbf147304751a7730f6a647c84f4b4d/Multiplatform/Assets.xcassets/visionOS.solidimagestack/Middle.solidimagestacklayer/Content.imageset/LiveKit-1024.png -------------------------------------------------------------------------------- /Multiplatform/Assets.xcassets/visionOS.solidimagestack/Middle.solidimagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Multiplatform/Controllers/AppContext.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 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 | @MainActor 23 | final class AppContext: ObservableObject { 24 | private let store: ValueStore 25 | 26 | @Published var videoViewVisible: Bool = true { 27 | didSet { store.value.videoViewVisible = videoViewVisible } 28 | } 29 | 30 | @Published var showInformationOverlay: Bool = false { 31 | didSet { store.value.showInformationOverlay = showInformationOverlay } 32 | } 33 | 34 | @Published var preferSampleBufferRendering: Bool = false { 35 | didSet { store.value.preferSampleBufferRendering = preferSampleBufferRendering } 36 | } 37 | 38 | @Published var videoViewMode: VideoView.LayoutMode = .fit { 39 | didSet { store.value.videoViewMode = videoViewMode } 40 | } 41 | 42 | @Published var videoViewMirrored: Bool = false { 43 | didSet { store.value.videoViewMirrored = videoViewMirrored } 44 | } 45 | 46 | @Published var videoViewPinchToZoomOptions: VideoView.PinchToZoomOptions = [] 47 | 48 | @Published var connectionHistory: Set = [] { 49 | didSet { store.value.connectionHistory = connectionHistory } 50 | } 51 | 52 | @Published var outputDevices: [AudioDevice] = [] 53 | @Published var outputDevice: AudioDevice = AudioManager.shared.defaultOutputDevice { 54 | didSet { 55 | guard oldValue != outputDevice else { return } 56 | print("didSet outputDevice: \(String(describing: outputDevice))") 57 | AudioManager.shared.outputDevice = outputDevice 58 | } 59 | } 60 | 61 | @Published var inputDevices: [AudioDevice] = [] 62 | @Published var inputDevice: AudioDevice = AudioManager.shared.defaultInputDevice { 63 | didSet { 64 | guard oldValue != inputDevice else { return } 65 | print("didSet inputDevice: \(String(describing: inputDevice))") 66 | AudioManager.shared.inputDevice = inputDevice 67 | } 68 | } 69 | 70 | #if os(iOS) || os(visionOS) || os(tvOS) 71 | @Published var preferSpeakerOutput: Bool = true { 72 | didSet { AudioManager.shared.isSpeakerOutputPreferred = preferSpeakerOutput } 73 | } 74 | #endif 75 | 76 | @Published var isVoiceProcessingBypassed: Bool = false { 77 | didSet { AudioManager.shared.isVoiceProcessingBypassed = isVoiceProcessingBypassed } 78 | } 79 | 80 | @Published var micMuteMode: MicrophoneMuteMode = .voiceProcessing { 81 | didSet { 82 | do { 83 | try AudioManager.shared.set(microphoneMuteMode: micMuteMode) 84 | } catch { 85 | print("Failed to set mic mute mode: \(error)") 86 | } 87 | } 88 | } 89 | 90 | @Published var micVolume: Float = 1.0 { 91 | didSet { AudioManager.shared.mixer.micVolume = micVolume } 92 | } 93 | 94 | @Published var appVolume: Float = 1.0 { 95 | didSet { AudioManager.shared.mixer.appVolume = appVolume } 96 | } 97 | 98 | public init(store: ValueStore) { 99 | self.store = store 100 | 101 | videoViewVisible = store.value.videoViewVisible 102 | showInformationOverlay = store.value.showInformationOverlay 103 | preferSampleBufferRendering = store.value.preferSampleBufferRendering 104 | videoViewMode = store.value.videoViewMode 105 | videoViewMirrored = store.value.videoViewMirrored 106 | connectionHistory = store.value.connectionHistory 107 | 108 | AudioManager.shared.onDeviceUpdate = { [weak self] _ in 109 | guard let self else { return } 110 | // force UI update for outputDevice / inputDevice 111 | Task { @MainActor [weak self] in 112 | guard let self else { return } 113 | self.outputDevices = AudioManager.shared.outputDevices 114 | self.inputDevices = AudioManager.shared.inputDevices 115 | self.outputDevice = AudioManager.shared.outputDevice 116 | self.inputDevice = AudioManager.shared.inputDevice 117 | } 118 | } 119 | 120 | outputDevices = AudioManager.shared.outputDevices 121 | inputDevices = AudioManager.shared.inputDevices 122 | outputDevice = AudioManager.shared.outputDevice 123 | inputDevice = AudioManager.shared.inputDevice 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Multiplatform/Controllers/RoomContext.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 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 | @MainActor 22 | final class RoomContext: ObservableObject { 23 | let jsonEncoder = JSONEncoder() 24 | let jsonDecoder = JSONDecoder() 25 | 26 | private let store: ValueStore 27 | 28 | // Used to show connection error dialog 29 | // private var didClose: Bool = false 30 | @Published var shouldShowDisconnectReason: Bool = false 31 | public var latestError: LiveKitError? 32 | 33 | public let room = Room() 34 | 35 | @Published var url: String = "" { 36 | didSet { store.value.url = url } 37 | } 38 | 39 | @Published var token: String = "" { 40 | didSet { store.value.token = token } 41 | } 42 | 43 | @Published var e2eeKey: String = "" { 44 | didSet { store.value.e2eeKey = e2eeKey } 45 | } 46 | 47 | @Published var isE2eeEnabled: Bool = false { 48 | didSet { 49 | store.value.isE2eeEnabled = isE2eeEnabled 50 | // room.set(isE2eeEnabled: isE2eeEnabled) 51 | } 52 | } 53 | 54 | // RoomOptions 55 | @Published var simulcast: Bool = true { 56 | didSet { store.value.simulcast = simulcast } 57 | } 58 | 59 | @Published var adaptiveStream: Bool = false { 60 | didSet { store.value.adaptiveStream = adaptiveStream } 61 | } 62 | 63 | @Published var dynacast: Bool = false { 64 | didSet { store.value.dynacast = dynacast } 65 | } 66 | 67 | @Published var reportStats: Bool = false { 68 | didSet { store.value.reportStats = reportStats } 69 | } 70 | 71 | // ConnectOptions 72 | @Published var autoSubscribe: Bool = true { 73 | didSet { store.value.autoSubscribe = autoSubscribe } 74 | } 75 | 76 | @Published var focusParticipant: Participant? 77 | 78 | @Published var showMessagesView: Bool = false 79 | @Published var messages: [ExampleRoomMessage] = [] 80 | 81 | @Published var textFieldString: String = "" 82 | 83 | @Published var isVideoProcessingEnabled: Bool = false { 84 | didSet { 85 | if let track = room.localParticipant.firstCameraVideoTrack as? LocalVideoTrack { 86 | track.capturer.processor = isVideoProcessingEnabled ? self : nil 87 | } 88 | } 89 | } 90 | 91 | var _connectTask: Task? 92 | 93 | public init(store: ValueStore) { 94 | self.store = store 95 | room.add(delegate: self) 96 | 97 | url = store.value.url 98 | token = store.value.token 99 | isE2eeEnabled = store.value.isE2eeEnabled 100 | e2eeKey = store.value.e2eeKey 101 | simulcast = store.value.simulcast 102 | adaptiveStream = store.value.adaptiveStream 103 | dynacast = store.value.dynacast 104 | reportStats = store.value.reportStats 105 | autoSubscribe = store.value.autoSubscribe 106 | 107 | #if os(iOS) 108 | UIApplication.shared.isIdleTimerDisabled = true 109 | #endif 110 | } 111 | 112 | deinit { 113 | #if os(iOS) 114 | Task { @MainActor in 115 | UIApplication.shared.isIdleTimerDisabled = false 116 | } 117 | #endif 118 | print("RoomContext.deinit") 119 | } 120 | 121 | func cancelConnect() { 122 | _connectTask?.cancel() 123 | } 124 | 125 | func connect(entry: ConnectionHistory? = nil) async throws -> Room { 126 | if let entry { 127 | url = entry.url 128 | token = entry.token 129 | isE2eeEnabled = entry.e2ee 130 | e2eeKey = entry.e2eeKey 131 | } 132 | 133 | let connectOptions = ConnectOptions( 134 | autoSubscribe: autoSubscribe 135 | ) 136 | 137 | var e2eeOptions: E2EEOptions? = nil 138 | if isE2eeEnabled { 139 | let keyProvider = BaseKeyProvider(isSharedKey: true) 140 | keyProvider.setKey(key: e2eeKey) 141 | e2eeOptions = E2EEOptions(keyProvider: keyProvider) 142 | } 143 | 144 | let roomOptions = RoomOptions( 145 | defaultCameraCaptureOptions: CameraCaptureOptions( 146 | dimensions: .h1080_169 147 | ), 148 | defaultScreenShareCaptureOptions: ScreenShareCaptureOptions( 149 | dimensions: .h1080_169, 150 | appAudio: true, 151 | useBroadcastExtension: true 152 | ), 153 | defaultVideoPublishOptions: VideoPublishOptions( 154 | simulcast: simulcast 155 | ), 156 | adaptiveStream: true, 157 | dynacast: dynacast, 158 | // isE2eeEnabled: isE2eeEnabled, 159 | e2eeOptions: e2eeOptions, 160 | reportRemoteTrackStatistics: true 161 | ) 162 | 163 | let connectTask = Task.detached { [weak self] in 164 | guard let self else { return } 165 | try await self.room.connect(url: self.url, 166 | token: self.token, 167 | connectOptions: connectOptions, 168 | roomOptions: roomOptions) 169 | } 170 | 171 | _connectTask = connectTask 172 | try await connectTask.value 173 | 174 | return room 175 | } 176 | 177 | func disconnect() async { 178 | await room.disconnect() 179 | } 180 | 181 | func sendMessage() { 182 | // Make sure the message is not empty 183 | guard !textFieldString.isEmpty else { return } 184 | 185 | let roomMessage = ExampleRoomMessage(messageId: UUID().uuidString, 186 | senderSid: room.localParticipant.sid, 187 | senderIdentity: room.localParticipant.identity, 188 | text: textFieldString) 189 | textFieldString = "" 190 | messages.append(roomMessage) 191 | 192 | Task.detached { [weak self] in 193 | guard let self else { return } 194 | do { 195 | let json = try self.jsonEncoder.encode(roomMessage) 196 | try await self.room.localParticipant.publish(data: json) 197 | } catch { 198 | print("Failed to encode data \(error)") 199 | } 200 | } 201 | } 202 | 203 | #if os(macOS) 204 | weak var screenShareTrack: LocalTrackPublication? 205 | 206 | @available(macOS 12.3, *) 207 | func setScreenShareMacOS(isEnabled: Bool, screenShareSource: MacOSScreenCaptureSource? = nil) async throws { 208 | if isEnabled, let screenShareSource { 209 | let track = LocalVideoTrack.createMacOSScreenShareTrack(source: screenShareSource, options: ScreenShareCaptureOptions(appAudio: true)) 210 | let options = VideoPublishOptions(preferredCodec: VideoCodec.h264) 211 | screenShareTrack = try await room.localParticipant.publish(videoTrack: track, options: options) 212 | } 213 | 214 | if !isEnabled, let screenShareTrack { 215 | try await room.localParticipant.unpublish(publication: screenShareTrack) 216 | } 217 | } 218 | #endif 219 | 220 | #if os(visionOS) && compiler(>=6.0) 221 | weak var arCameraTrack: LocalTrackPublication? 222 | 223 | func setARCamera(isEnabled: Bool) async throws { 224 | if #available(visionOS 2.0, *) { 225 | if isEnabled { 226 | let track = LocalVideoTrack.createARCameraTrack() 227 | arCameraTrack = try await room.localParticipant.publish(videoTrack: track) 228 | } 229 | } 230 | 231 | if !isEnabled, let arCameraTrack { 232 | try await room.localParticipant.unpublish(publication: arCameraTrack) 233 | self.arCameraTrack = nil 234 | } 235 | } 236 | #endif 237 | } 238 | 239 | extension RoomContext: RoomDelegate { 240 | func room(_: Room, track publication: TrackPublication, didUpdateE2EEState e2eeState: E2EEState) { 241 | print("Did update e2eeState = [\(String(describing: e2eeState))] for publication \(publication.sid)") 242 | } 243 | 244 | nonisolated func room(_ room: Room, didUpdateConnectionState connectionState: ConnectionState, from oldValue: ConnectionState) { 245 | print("Did update connectionState \(oldValue) -> \(connectionState)") 246 | 247 | if case .disconnected = connectionState, 248 | let error = room.disconnectError, 249 | error.type != .cancelled 250 | { 251 | Task { @MainActor [weak self] in 252 | guard let self else { return } 253 | latestError = room.disconnectError 254 | self.shouldShowDisconnectReason = true 255 | // Reset state 256 | self.focusParticipant = nil 257 | self.showMessagesView = false 258 | self.textFieldString = "" 259 | self.messages.removeAll() 260 | // self.objectWillChange.send() 261 | } 262 | } 263 | } 264 | 265 | nonisolated func room(_: Room, participantDidDisconnect participant: RemoteParticipant) { 266 | Task { @MainActor [weak self] in 267 | guard let self else { return } 268 | if let focusParticipant = self.focusParticipant, focusParticipant.identity == participant.identity { 269 | self.focusParticipant = nil 270 | } 271 | } 272 | } 273 | 274 | nonisolated func room(_: Room, participant _: RemoteParticipant?, didReceiveData data: Data, forTopic _: String) { 275 | do { 276 | let roomMessage = try jsonDecoder.decode(ExampleRoomMessage.self, from: data) 277 | // Update UI from main queue 278 | Task { @MainActor [weak self] in 279 | guard let self else { return } 280 | 281 | withAnimation { 282 | // Add messages to the @Published messages property 283 | // which will trigger the UI to update 284 | self.messages.append(roomMessage) 285 | // Show the messages view when new messages arrive 286 | self.showMessagesView = true 287 | } 288 | } 289 | 290 | } catch { 291 | print("Failed to decode data \(error)") 292 | } 293 | } 294 | 295 | nonisolated func room(_: Room, participant _: Participant, trackPublication _: TrackPublication, didReceiveTranscriptionSegments segments: [TranscriptionSegment]) { 296 | print("didReceiveTranscriptionSegments: \(segments.map { "(\($0.id): \($0.text), \($0.firstReceivedTime)-\($0.lastReceivedTime), \($0.isFinal))" }.joined(separator: ", "))") 297 | } 298 | 299 | nonisolated func room(_: Room, trackPublication _: TrackPublication, didUpdateE2EEState state: E2EEState) { 300 | print("didUpdateE2EEState: \(state)") 301 | } 302 | 303 | nonisolated func room(_: Room, participant _: LocalParticipant, didPublishTrack publication: LocalTrackPublication) { 304 | print("didPublishTrack: \(publication)") 305 | guard let localVideoTrack = publication.track as? LocalVideoTrack, localVideoTrack.source == .camera else { return } 306 | 307 | Task { @MainActor in 308 | localVideoTrack.capturer.processor = isVideoProcessingEnabled ? self : nil 309 | } 310 | } 311 | } 312 | 313 | extension RoomContext: VideoProcessor { 314 | nonisolated func process(frame: VideoFrame) -> VideoFrame? { 315 | guard let pixelBuffer = frame.toCVPixelBuffer() else { 316 | print("Failed to get pixel buffer") 317 | return nil 318 | } 319 | 320 | // Do something with pixel buffer. 321 | guard let newPixelBuffer = processPixelBuffer(pixelBuffer) else { 322 | print("Failed to proces the pixel buffer") 323 | return nil 324 | } 325 | 326 | // Re-construct a VideoFrame 327 | return VideoFrame(dimensions: frame.dimensions, 328 | rotation: frame.rotation, 329 | timeStampNs: frame.timeStampNs, 330 | buffer: CVPixelVideoBuffer(pixelBuffer: newPixelBuffer)) 331 | } 332 | } 333 | 334 | // Processing example 335 | func processPixelBuffer(_ pixelBuffer: CVPixelBuffer) -> CVPixelBuffer? { 336 | // Lock the buffer for reading 337 | CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly) 338 | 339 | // Create CIImage from the pixel buffer 340 | let ciImage = CIImage(cvPixelBuffer: pixelBuffer) 341 | 342 | // Create Core Image context 343 | let device = MTLCreateSystemDefaultDevice()! 344 | let context = CIContext(mtlDevice: device, options: nil) 345 | 346 | // Apply dramatic filters 347 | 348 | // 1. Gaussian blur effect 349 | let blurFilter = CIFilter(name: "CIGaussianBlur")! 350 | blurFilter.setValue(ciImage, forKey: kCIInputImageKey) 351 | blurFilter.setValue(8.0, forKey: kCIInputRadiusKey) // Larger radius = more blur 352 | 353 | // 2. Color inversion 354 | // let colorInvertFilter = CIFilter(name: "CIColorInvert")! 355 | // colorInvertFilter.setValue(blurFilter.outputImage, forKey: kCIInputImageKey) 356 | 357 | // 3. Add a sepia tone effect 358 | // let sepiaFilter = CIFilter(name: "CISepiaTone")! 359 | // sepiaFilter.setValue(ciImage, forKey: kCIInputImageKey) 360 | // sepiaFilter.setValue(0.8, forKey: kCIInputIntensityKey) 361 | 362 | let pixelBufferAttributes: [String: Any] = [ 363 | kCVPixelBufferMetalCompatibilityKey as String: true, 364 | ] 365 | 366 | // Create output pixel buffer 367 | var outputPixelBuffer: CVPixelBuffer? 368 | let status = CVPixelBufferCreate( 369 | kCFAllocatorDefault, 370 | CVPixelBufferGetWidth(pixelBuffer), 371 | CVPixelBufferGetHeight(pixelBuffer), 372 | CVPixelBufferGetPixelFormatType(pixelBuffer), 373 | pixelBufferAttributes as CFDictionary, 374 | &outputPixelBuffer 375 | ) 376 | 377 | guard status == kCVReturnSuccess, let outputBuffer = outputPixelBuffer else { 378 | CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly) 379 | return nil 380 | } 381 | 382 | // Render the processed image to the output buffer 383 | context.render( 384 | blurFilter.outputImage!, 385 | to: outputBuffer, 386 | bounds: ciImage.extent, 387 | colorSpace: CGColorSpaceCreateDeviceRGB() 388 | ) 389 | 390 | // Unlock the original buffer 391 | CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly) 392 | 393 | return outputBuffer 394 | } 395 | -------------------------------------------------------------------------------- /Multiplatform/Extensions/Binding+OptionSet.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 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 & Sendable, 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 | -------------------------------------------------------------------------------- /Multiplatform/Extensions/Bundle.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 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 | -------------------------------------------------------------------------------- /Multiplatform/Extensions/CIImage.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 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 CIImage { 20 | // helper to create a `CIImage` for both platforms 21 | convenience init(named name: String) { 22 | #if !os(macOS) 23 | self.init(cgImage: UIImage(named: name)!.cgImage!) 24 | #else 25 | self.init(data: NSImage(named: name)!.tiffRepresentation!)! 26 | #endif 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Multiplatform/LiveKitExample.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 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 | @MainActor let sync = ValueStore(store: Keychain(service: "io.livekit.example.SwiftSDK.1"), 23 | key: "preferences", 24 | default: Preferences()) 25 | 26 | @main 27 | struct LiveKitExample: App { 28 | @StateObject var appCtx = AppContext(store: sync) 29 | 30 | #if os(visionOS) 31 | @Environment(\.openWindow) var openWindow 32 | #endif 33 | 34 | var body: some Scene { 35 | WindowGroup { 36 | RoomContextView() 37 | .environmentObject(appCtx) 38 | } 39 | #if !os(tvOS) 40 | .handlesExternalEvents(matching: Set(arrayLiteral: "*")) 41 | #endif 42 | #if os(macOS) 43 | .windowStyle(.hiddenTitleBar) 44 | .windowToolbarStyle(.unifiedCompact) 45 | #endif 46 | 47 | #if os(visionOS) 48 | ImmersiveSpace(id: "ImmersiveSpace") { 49 | ImmersiveView() 50 | } 51 | .immersionStyle(selection: .constant(.full), in: .full) 52 | #endif 53 | } 54 | 55 | init() { 56 | LoggingSystem.bootstrap { 57 | var logHandler = StreamLogHandler.standardOutput(label: $0) 58 | logHandler.logLevel = .debug 59 | return logHandler 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Multiplatform/Support/ConnectionHistory.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 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 | -------------------------------------------------------------------------------- /Multiplatform/Support/ExampleRoomMessage.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 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 | -------------------------------------------------------------------------------- /Multiplatform/Support/Participant+Helpers.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 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 | -------------------------------------------------------------------------------- /Multiplatform/Support/SecureStore.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 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 | @MainActor 51 | final class ValueStore { 52 | private let store: Keychain 53 | private let key: String 54 | private let message = "" 55 | private var syncTask: Task? 56 | 57 | public var value: T { 58 | didSet { 59 | guard oldValue != value else { return } 60 | lazySync() 61 | } 62 | } 63 | 64 | private var storeWithOptions: Keychain { 65 | store 66 | .accessibility(.whenUnlocked) 67 | .synchronizable(true) 68 | } 69 | 70 | public init(store: Keychain, key: String, default: T) { 71 | self.store = store 72 | self.key = key 73 | value = `default` 74 | 75 | if let data = try? storeWithOptions.getData(key), 76 | let result = try? decoder.decode(T.self, from: data) 77 | { 78 | value = result 79 | } 80 | } 81 | 82 | deinit { 83 | syncTask?.cancel() 84 | } 85 | 86 | public func lazySync() { 87 | syncTask?.cancel() 88 | syncTask = Task { 89 | try? await Task.sleep(nanoseconds: 1 * NSEC_PER_SEC) 90 | guard !Task.isCancelled else { return } 91 | sync() 92 | } 93 | } 94 | 95 | public func sync() { 96 | do { 97 | let data = try encoder.encode(value) 98 | try storeWithOptions.set(data, key: key) 99 | } catch { 100 | print("Failed to write in Keychain, error: \(error)") 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Multiplatform/SwiftSDK_1.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.networking.multipath 6 | 7 | com.apple.security.app-sandbox 8 | 9 | com.apple.security.application-groups 10 | 11 | group.io.livekit.example.SwiftSDK.1 12 | 13 | com.apple.security.device.audio-input 14 | 15 | com.apple.security.device.camera 16 | 17 | com.apple.security.files.user-selected.read-only 18 | 19 | com.apple.security.network.client 20 | 21 | com.apple.security.network.server 22 | 23 | keychain-access-groups 24 | 25 | $(AppIdentifierPrefix)io.livekit.example.SwiftSDK.1 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Multiplatform/Views/AudioMixerView.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 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 | #if !os(tvOS) 20 | struct AudioMixerView: View { 21 | @EnvironmentObject var appCtx: AppContext 22 | 23 | var body: some View { 24 | Text("Mic audio mixer") 25 | HStack { 26 | Text("Mic") 27 | Slider(value: $appCtx.micVolume, in: 0.0 ... 1.0) 28 | } 29 | HStack { 30 | Text("App") 31 | Slider(value: $appCtx.appVolume, in: 0.0 ... 1.0) 32 | } 33 | } 34 | } 35 | #endif 36 | -------------------------------------------------------------------------------- /Multiplatform/Views/ConnectView.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 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: .secret) 46 | LKTextField(title: "E2EE Key", text: $roomCtx.e2eeKey, type: .secret) 47 | 48 | HStack { 49 | Menu { 50 | Toggle("Auto-Subscribe", isOn: $roomCtx.autoSubscribe) 51 | Toggle("Enable E2EE", isOn: $roomCtx.isE2eeEnabled) 52 | } label: { 53 | Image(systemSymbol: .boltFill) 54 | .renderingMode(.original) 55 | Text("Connect Options") 56 | } 57 | #if os(macOS) 58 | .menuIndicator(.visible) 59 | .menuStyle(BorderlessButtonMenuStyle()) 60 | #elseif os(iOS) 61 | .menuStyle(BorderlessButtonMenuStyle()) 62 | #endif 63 | .fixedSize() 64 | 65 | Menu { 66 | Toggle("Simulcast", isOn: $roomCtx.simulcast) 67 | Toggle("AdaptiveStream", isOn: $roomCtx.adaptiveStream) 68 | Toggle("Dynacast", isOn: $roomCtx.dynacast) 69 | Toggle("Report stats", isOn: $roomCtx.reportStats) 70 | } label: { 71 | Image(systemSymbol: .gear) 72 | .renderingMode(.original) 73 | Text("Room Options") 74 | } 75 | #if os(macOS) 76 | .menuIndicator(.visible) 77 | .menuStyle(BorderlessButtonMenuStyle()) 78 | #elseif os(iOS) 79 | .menuStyle(BorderlessButtonMenuStyle()) 80 | #endif 81 | .fixedSize() 82 | } 83 | }.frame(maxWidth: 350) 84 | 85 | if case .connecting = room.connectionState { 86 | HStack(alignment: .center) { 87 | ProgressView() 88 | 89 | LKButton(title: "Cancel") { 90 | roomCtx.cancelConnect() 91 | } 92 | } 93 | } else { 94 | HStack(alignment: .center) { 95 | Spacer() 96 | 97 | LKButton(title: "Connect") { 98 | Task { @MainActor in 99 | let room = try await roomCtx.connect() 100 | appCtx.connectionHistory.update(room: room, e2ee: roomCtx.isE2eeEnabled, e2eeKey: roomCtx.e2eeKey) 101 | } 102 | } 103 | 104 | if !appCtx.connectionHistory.isEmpty { 105 | Menu { 106 | ForEach(appCtx.connectionHistory.sortedByUpdated) { entry in 107 | Button { 108 | Task { @MainActor in 109 | let room = try await roomCtx.connect(entry: entry) 110 | appCtx.connectionHistory.update(room: room, e2ee: roomCtx.isE2eeEnabled, e2eeKey: roomCtx.e2eeKey) 111 | } 112 | } label: { 113 | Image(systemSymbol: .boltFill) 114 | .renderingMode(.original) 115 | Text(String(describing: entry)) 116 | } 117 | } 118 | 119 | Divider() 120 | 121 | Button { 122 | appCtx.connectionHistory.removeAll() 123 | } label: { 124 | Image(systemSymbol: .xmarkCircleFill) 125 | .renderingMode(.original) 126 | Text("Clear history") 127 | } 128 | 129 | } label: { 130 | Image(systemSymbol: .clockFill) 131 | .renderingMode(.original) 132 | Text("Recent") 133 | } 134 | #if os(macOS) 135 | .menuIndicator(.visible) 136 | .menuStyle(BorderlessButtonMenuStyle()) 137 | #elseif os(iOS) 138 | .menuStyle(BorderlessButtonMenuStyle()) 139 | #endif 140 | .fixedSize() 141 | } 142 | 143 | Spacer() 144 | } 145 | } 146 | } 147 | .padding() 148 | .frame(width: geometry.size.width) // Make the scroll view full-width 149 | .frame(minHeight: geometry.size.height) // Set the content’s min height to the parent 150 | } 151 | } 152 | #if os(macOS) 153 | .frame(minWidth: 500, minHeight: 500) 154 | #endif 155 | .alert(isPresented: $roomCtx.shouldShowDisconnectReason) { 156 | Alert(title: Text("Disconnected"), 157 | message: Text("Reason: " + String(describing: roomCtx.latestError))) 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /Multiplatform/Views/ImmersiveView.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 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 | #if os(visionOS) 18 | 19 | import RealityKit 20 | import SwiftUI 21 | 22 | struct ImmersiveView: View { 23 | var body: some View { 24 | ZStack { 25 | RealityView { content in 26 | 27 | let entity = Entity() 28 | entity.components.set(ModelComponent( 29 | mesh: .generateSphere(radius: 1000), 30 | materials: [] 31 | )) 32 | 33 | entity.scale *= SIMD3(repeating: -1) 34 | content.add(entity) 35 | } 36 | } 37 | } 38 | } 39 | #endif 40 | -------------------------------------------------------------------------------- /Multiplatform/Views/ParticipantView.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 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 LiveKitComponents 19 | import SFSafeSymbols 20 | import SwiftUI 21 | 22 | struct ParticipantView: View { 23 | @ObservedObject var participant: Participant 24 | @EnvironmentObject var appCtx: AppContext 25 | 26 | var videoViewMode: VideoView.LayoutMode = .fill 27 | var onTap: ((_ participant: Participant) -> Void)? 28 | 29 | @State private var isRendering: Bool = false 30 | 31 | func bgView(systemSymbol: SFSymbol, geometry: GeometryProxy) -> some View { 32 | Image(systemSymbol: systemSymbol) 33 | .resizable() 34 | .aspectRatio(contentMode: .fit) 35 | .foregroundColor(Color.lkGray2) 36 | .frame(width: min(geometry.size.width, geometry.size.height) * 0.3) 37 | .frame( 38 | maxWidth: .infinity, 39 | maxHeight: .infinity 40 | ) 41 | } 42 | 43 | var body: some View { 44 | GeometryReader { geometry in 45 | 46 | ZStack(alignment: .bottom) { 47 | // Background color 48 | Color.lkGray1 49 | .ignoresSafeArea() 50 | 51 | // VideoView for the Participant 52 | if let publication = participant.mainVideoPublication, 53 | !publication.isMuted, 54 | let track = publication.track as? VideoTrack, 55 | appCtx.videoViewVisible 56 | { 57 | ZStack(alignment: .topLeading) { 58 | SwiftUIVideoView(track, 59 | layoutMode: videoViewMode, 60 | mirrorMode: appCtx.videoViewMirrored ? .mirror : .auto, 61 | renderMode: appCtx.preferSampleBufferRendering ? .sampleBuffer : .auto, 62 | pinchToZoomOptions: appCtx.videoViewPinchToZoomOptions, 63 | isDebugMode: appCtx.showInformationOverlay, 64 | isRendering: $isRendering) 65 | 66 | if !isRendering { 67 | ProgressView().progressViewStyle(CircularProgressViewStyle()) 68 | .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) 69 | } 70 | } 71 | } else if let publication = participant.mainVideoPublication as? RemoteTrackPublication, 72 | case .notAllowed = publication.subscriptionState 73 | { 74 | // Show no permission icon 75 | bgView(systemSymbol: .exclamationmarkCircle, geometry: geometry) 76 | } else if let publication = participant.firstAudioPublication, !publication.isMuted, let track = publication.track as? AudioTrack { 77 | BarAudioVisualizer(audioTrack: track) 78 | } else { 79 | // Show no camera icon 80 | bgView(systemSymbol: .videoSlashFill, geometry: geometry) 81 | } 82 | 83 | if appCtx.showInformationOverlay { 84 | VStack(alignment: .leading, spacing: 5) { 85 | // Video stats 86 | if let publication = participant.mainVideoPublication, 87 | !publication.isMuted, 88 | let track = publication.track as? VideoTrack 89 | { 90 | StatsView(track: track) 91 | } 92 | // Audio stats 93 | if let publication = participant.firstAudioPublication, 94 | !publication.isMuted, 95 | let track = publication.track as? AudioTrack 96 | { 97 | StatsView(track: track) 98 | } 99 | } 100 | .padding(8) 101 | .frame( 102 | minWidth: 0, 103 | maxWidth: .infinity, 104 | minHeight: 0, 105 | maxHeight: .infinity, 106 | alignment: .topLeading 107 | ) 108 | } 109 | 110 | VStack(alignment: .trailing, spacing: 0) { 111 | // Show the sub-video view 112 | if let subVideoTrack = participant.subVideoTrack { 113 | SwiftUIVideoView(subVideoTrack, 114 | layoutMode: .fill, 115 | mirrorMode: appCtx.videoViewMirrored ? .mirror : .auto) 116 | .background(Color.black) 117 | .aspectRatio(contentMode: .fit) 118 | .frame(width: min(geometry.size.width, geometry.size.height) * 0.3) 119 | .cornerRadius(8) 120 | .padding() 121 | } 122 | 123 | // Bottom user info bar 124 | HStack { 125 | if let identity = participant.identity { 126 | Text(String(describing: identity)) 127 | .lineLimit(1) 128 | .truncationMode(.tail) 129 | } 130 | 131 | if let publication = participant.mainVideoPublication, 132 | !publication.isMuted 133 | { 134 | // is remote 135 | if let remotePub = publication as? RemoteTrackPublication { 136 | Menu { 137 | if case .subscribed = remotePub.subscriptionState { 138 | Button("Unsubscribe") { 139 | Task { try await remotePub.set(subscribed: false) } 140 | } 141 | } else if case .unsubscribed = remotePub.subscriptionState { 142 | Button("Subscribe") { 143 | Task { try await remotePub.set(subscribed: true) } 144 | } 145 | } 146 | } label: { 147 | if case .subscribed = remotePub.subscriptionState { 148 | Image(systemSymbol: .videoFill) 149 | .foregroundColor(Color.green) 150 | } else if case .notAllowed = remotePub.subscriptionState { 151 | Image(systemSymbol: .exclamationmarkCircle) 152 | .foregroundColor(Color.red) 153 | } else { 154 | Image(systemSymbol: .videoSlashFill) 155 | } 156 | } 157 | #if os(macOS) 158 | .menuIndicator(.visible) 159 | .menuStyle(BorderlessButtonMenuStyle()) 160 | #elseif os(iOS) 161 | .menuStyle(BorderlessButtonMenuStyle()) 162 | #endif 163 | .fixedSize() 164 | } else { 165 | // local 166 | Image(systemSymbol: .videoFill) 167 | .foregroundColor(Color.green) 168 | } 169 | 170 | } else { 171 | Image(systemSymbol: .videoSlashFill) 172 | .foregroundColor(Color.white) 173 | } 174 | 175 | if let publication = participant.firstAudioPublication, 176 | !publication.isMuted 177 | { 178 | // is remote 179 | if let remotePub = publication as? RemoteTrackPublication { 180 | Menu { 181 | if case .subscribed = remotePub.subscriptionState { 182 | Button("Unsubscribe") { 183 | Task { try await remotePub.set(subscribed: false) } 184 | } 185 | } else if case .unsubscribed = remotePub.subscriptionState { 186 | Button("Subscribe") { 187 | Task { try await remotePub.set(subscribed: true) } 188 | } 189 | } 190 | } label: { 191 | if case .subscribed = remotePub.subscriptionState { 192 | Image(systemSymbol: .micFill) 193 | .foregroundColor(Color.orange) 194 | } else if case .notAllowed = remotePub.subscriptionState { 195 | Image(systemSymbol: .exclamationmarkCircle) 196 | .foregroundColor(Color.red) 197 | } else { 198 | Image(systemSymbol: .micSlashFill) 199 | } 200 | } 201 | #if os(macOS) 202 | .menuIndicator(.visible) 203 | .menuStyle(BorderlessButtonMenuStyle()) 204 | #elseif os(iOS) 205 | .menuStyle(BorderlessButtonMenuStyle()) 206 | #endif 207 | .fixedSize() 208 | } else { 209 | // local 210 | Image(systemSymbol: .micFill) 211 | .foregroundColor(Color.orange) 212 | } 213 | 214 | } else { 215 | Image(systemSymbol: .micSlashFill) 216 | .foregroundColor(Color.white) 217 | } 218 | 219 | if participant.connectionQuality == .excellent { 220 | Image(systemSymbol: .wifi) 221 | .foregroundColor(.green) 222 | } else if participant.connectionQuality == .good { 223 | Image(systemSymbol: .wifi) 224 | .foregroundColor(Color.orange) 225 | } else if participant.connectionQuality == .poor { 226 | Image(systemSymbol: .wifiExclamationmark) 227 | .foregroundColor(Color.red) 228 | } 229 | 230 | if participant.firstTrackEncryptionType == .none { 231 | Image(systemSymbol: .lockOpenFill) 232 | .foregroundColor(.red) 233 | } else { 234 | Image(systemSymbol: .lockFill) 235 | .foregroundColor(.green) 236 | } 237 | 238 | }.padding(5) 239 | .frame(minWidth: 0, maxWidth: .infinity) 240 | .background(Color.black.opacity(0.5)) 241 | } 242 | } 243 | .cornerRadius(8) 244 | // Glow the border when the participant is speaking 245 | .overlay( 246 | participant.isSpeaking ? 247 | RoundedRectangle(cornerRadius: 5) 248 | .stroke(Color.blue, lineWidth: 5.0) 249 | : nil 250 | ) 251 | }.gesture(TapGesture() 252 | .onEnded { _ in 253 | // Pass the tap event 254 | onTap?(participant) 255 | }) 256 | } 257 | } 258 | 259 | struct StatsView: View { 260 | private let track: Track 261 | @ObservedObject private var observer: TrackDelegateObserver 262 | 263 | init(track: Track) { 264 | self.track = track 265 | observer = TrackDelegateObserver(track: track) 266 | } 267 | 268 | var body: some View { 269 | HStack(alignment: .top, spacing: 5) { 270 | VStack(alignment: .leading, spacing: 5) { 271 | if track is VideoTrack { 272 | HStack(spacing: 3) { 273 | Image(systemSymbol: .videoFill) 274 | Text("Video").fontWeight(.bold) 275 | if let dimensions = observer.dimensions { 276 | Text("\(dimensions.width)×\(dimensions.height)") 277 | } 278 | } 279 | } else if track is AudioTrack { 280 | HStack(spacing: 3) { 281 | Image(systemSymbol: .micFill) 282 | Text("Audio").fontWeight(.bold) 283 | } 284 | } else { 285 | Text("Unknown").fontWeight(.bold) 286 | } 287 | 288 | // if let trackStats = viewModel.statistics { 289 | ForEach(observer.allStatisticts, id: \.self) { trackStats in 290 | ForEach(trackStats.outboundRtpStream.sortedByRidIndex()) { stream in 291 | 292 | HStack(spacing: 3) { 293 | Image(systemSymbol: .arrowUp) 294 | 295 | if let codec = trackStats.codec.first(where: { $0.id == stream.codecId }) { 296 | Text(codec.mimeType ?? "?") 297 | } 298 | 299 | if let rid = stream.rid, !rid.isEmpty { 300 | Text(rid.uppercased()) 301 | } 302 | 303 | Text(stream.formattedBps()) 304 | 305 | if let reason = stream.qualityLimitationReason, reason != QualityLimitationReason.none { 306 | Image(systemSymbol: .exclamationmarkTriangleFill) 307 | Text(reason.rawValue.capitalized) 308 | } 309 | } 310 | } 311 | ForEach(trackStats.inboundRtpStream) { stream in 312 | 313 | HStack(spacing: 3) { 314 | Image(systemSymbol: .arrowDown) 315 | 316 | if let codec = trackStats.codec.first(where: { $0.id == stream.codecId }) { 317 | Text(codec.mimeType ?? "?") 318 | } 319 | 320 | Text(stream.formattedBps()) 321 | } 322 | } 323 | } 324 | } 325 | .font(.system(size: 10)) 326 | .foregroundColor(Color.white) 327 | .padding(5) 328 | .background(Color.black.opacity(0.5)) 329 | .cornerRadius(8) 330 | } 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /Multiplatform/Views/PublishOptionsView.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 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 | @preconcurrency import AVFoundation 18 | import LiveKit 19 | import SwiftUI 20 | 21 | extension AVCaptureDevice: Swift.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 | @State private var presetDimensions: Dimensions? = .h1080_169 36 | @State private var customWidth: String = "1920" 37 | @State private var customHeight: String = "1080" 38 | 39 | private let providedPublishOptions: VideoPublishOptions 40 | private let onPublish: OnPublish 41 | 42 | init(publishOptions: VideoPublishOptions, _ onPublish: @escaping OnPublish) { 43 | providedPublishOptions = publishOptions 44 | self.onPublish = onPublish 45 | 46 | simulcast = publishOptions.simulcast 47 | preferredVideoCodec = publishOptions.preferredCodec 48 | preferredBackupVideoCodec = publishOptions.preferredBackupCodec 49 | } 50 | 51 | var body: some View { 52 | VStack(alignment: .center, spacing: 10) { 53 | Text("Publish options") 54 | .fontWeight(.bold) 55 | Form { 56 | Picker("Device", selection: $device) { 57 | Text("Auto").tag(nil as AVCaptureDevice?) 58 | ForEach(devices) { 59 | Text($0.localizedName).tag($0 as AVCaptureDevice?) 60 | } 61 | } 62 | 63 | Picker("Codec", selection: $preferredVideoCodec) { 64 | Text("Auto").tag(nil as VideoCodec?) 65 | ForEach(VideoCodec.all) { 66 | Text($0.id.uppercased()).tag($0 as VideoCodec?) 67 | } 68 | }.onChange(of: preferredVideoCodec) { newValue in 69 | if newValue?.isSVC ?? false { 70 | preferredBackupVideoCodec = .vp8 71 | } else { 72 | preferredBackupVideoCodec = nil 73 | } 74 | } 75 | 76 | Picker("Backup Codec", selection: $preferredBackupVideoCodec) { 77 | Text("Off").tag(nil as VideoCodec?) 78 | ForEach(VideoCodec.allBackup.filter { $0 != preferredVideoCodec }) { 79 | Text($0.id.uppercased()).tag($0 as VideoCodec?) 80 | } 81 | }.disabled(!(preferredVideoCodec?.isSVC ?? false)) 82 | 83 | Picker("Max FPS", selection: $maxFPS) { 84 | ForEach(1 ... 30, id: \.self) { 85 | Text("\($0)").tag($0) 86 | } 87 | } 88 | 89 | Picker("Dimensions", selection: $presetDimensions) { 90 | let items: [Dimensions?] = [Dimensions.h2160_169, 91 | Dimensions.h1440_169, 92 | Dimensions.h1080_169, 93 | Dimensions.h720_169, 94 | Dimensions.h540_169, 95 | Dimensions.h360_169, 96 | Dimensions.h216_169, 97 | Dimensions.h180_169, 98 | Dimensions.h90_169, 99 | nil] 100 | ForEach(items, id: \.self) { 101 | if let dimensions = $0 { 102 | Text("\(dimensions.width)x\(dimensions.height)").tag(dimensions) 103 | } else { 104 | Text("Custom").tag(nil as Dimensions?) 105 | } 106 | } 107 | } 108 | 109 | if presetDimensions == nil { 110 | TextField("Width", text: Binding( 111 | get: { customWidth }, 112 | set: { customWidth = $0.filter { "0123456789".contains($0) }})) 113 | #if !os(tvOS) 114 | .textFieldStyle(RoundedBorderTextFieldStyle()) 115 | #endif 116 | TextField("Height", text: Binding( 117 | get: { customHeight }, 118 | set: { customHeight = $0.filter { "0123456789".contains($0) }})) 119 | #if !os(tvOS) 120 | .textFieldStyle(RoundedBorderTextFieldStyle()) 121 | #endif 122 | } 123 | 124 | Toggle("Simulcast", isOn: $simulcast) 125 | } 126 | 127 | Button("Publish") { 128 | let captureOptions = CameraCaptureOptions( 129 | device: device, 130 | dimensions: presetDimensions ?? Dimensions(width: Int32(customWidth) ?? 1920, 131 | height: Int32(customHeight) ?? 1080) 132 | ) 133 | 134 | let publishOptions = VideoPublishOptions( 135 | name: providedPublishOptions.name, 136 | encoding: VideoEncoding(maxBitrate: VideoParameters.presetH1080_169.encoding.maxBitrate, maxFps: maxFPS), 137 | screenShareEncoding: providedPublishOptions.screenShareEncoding, 138 | simulcast: simulcast, 139 | simulcastLayers: providedPublishOptions.simulcastLayers, 140 | screenShareSimulcastLayers: providedPublishOptions.screenShareSimulcastLayers, 141 | preferredCodec: preferredVideoCodec, 142 | preferredBackupCodec: preferredBackupVideoCodec 143 | ) 144 | 145 | onPublish(captureOptions, publishOptions) 146 | } 147 | #if !os(tvOS) 148 | .keyboardShortcut(.defaultAction) 149 | #endif 150 | 151 | Spacer() 152 | } 153 | .onAppear(perform: { 154 | Task { 155 | devices = try await CameraCapturer.captureDevices() 156 | #if !os(macOS) 157 | .singleDeviceforEachPosition() 158 | #endif 159 | } 160 | }) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /Multiplatform/Views/RoomContextView.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 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 | // Attaches RoomContext and Room to the environment 20 | struct RoomContextView: View { 21 | @EnvironmentObject var appCtx: AppContext 22 | @StateObject var roomCtx = RoomContext(store: sync) 23 | 24 | var body: some View { 25 | RoomSwitchView() 26 | .environmentObject(roomCtx) 27 | .environmentObject(roomCtx.room) 28 | .foregroundColor(Color.white) 29 | .onDisappear { 30 | print("\(String(describing: type(of: self))) onDisappear") 31 | Task { 32 | await roomCtx.disconnect() 33 | } 34 | } 35 | .onOpenURL(perform: { url in 36 | 37 | guard let urlComponent = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return } 38 | guard let host = url.host else { return } 39 | 40 | let secureValue = urlComponent.queryItems?.first(where: { $0.name == "secure" })?.value?.lowercased() 41 | let secure = ["true", "1"].contains { $0 == secureValue } 42 | 43 | let tokenValue = urlComponent.queryItems?.first(where: { $0.name == "token" })?.value ?? "" 44 | 45 | let e2ee = ["true", "1"].contains { $0 == secureValue } 46 | let e2eeKey = urlComponent.queryItems?.first(where: { $0.name == "e2eeKey" })?.value ?? "" 47 | 48 | var builder = URLComponents() 49 | builder.scheme = secure ? "wss" : "ws" 50 | builder.host = host 51 | builder.port = url.port 52 | 53 | guard let builtUrl = builder.url?.absoluteString else { return } 54 | 55 | print("built URL: \(builtUrl), token: \(tokenValue)") 56 | 57 | Task { @MainActor in 58 | roomCtx.url = builtUrl 59 | roomCtx.token = tokenValue 60 | roomCtx.isE2eeEnabled = e2ee 61 | roomCtx.e2eeKey = e2eeKey 62 | if !roomCtx.token.isEmpty { 63 | let room = try await roomCtx.connect() 64 | appCtx.connectionHistory.update(room: room, e2ee: e2ee, e2eeKey: e2eeKey) 65 | } 66 | } 67 | }) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Multiplatform/Views/RoomSwitchView.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 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 RoomSwitchView: View { 21 | @EnvironmentObject var appCtx: AppContext 22 | @EnvironmentObject var roomCtx: RoomContext 23 | @EnvironmentObject var room: Room 24 | 25 | #if os(visionOS) 26 | @Environment(\.openImmersiveSpace) var openImmersiveSpace 27 | @Environment(\.dismissImmersiveSpace) var dismissImmersiveSpace 28 | #endif 29 | 30 | var shouldShowRoomView: Bool { 31 | room.connectionState == .connected || room.connectionState == .reconnecting 32 | } 33 | 34 | private var navigatonTitle: String { 35 | guard shouldShowRoomView else { return "LiveKit" } 36 | return [ 37 | room.name, 38 | room.localParticipant.name, 39 | room.localParticipant.identity.map(\.stringValue), 40 | ] 41 | .compactMap { $0 } 42 | .joined(separator: " ") 43 | } 44 | 45 | var body: some View { 46 | ZStack { 47 | Color.black 48 | .ignoresSafeArea() 49 | 50 | if shouldShowRoomView { 51 | RoomView() 52 | } else { 53 | ConnectView() 54 | } 55 | } 56 | .preferredColorScheme(.dark) 57 | .navigationTitle(navigatonTitle) 58 | .onChange(of: shouldShowRoomView) { newValue in 59 | #if os(visionOS) 60 | Task { 61 | if newValue { 62 | await openImmersiveSpace(id: "ImmersiveSpace") 63 | } else { 64 | await dismissImmersiveSpace() 65 | } 66 | } 67 | #endif 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Multiplatform/Views/RoomView.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 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) && !os(tvOS) 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 ToolbarItemPlacement: @unchecked Swift.Sendable {} 30 | 31 | #if os(macOS) 32 | // keeps weak reference to NSWindow 33 | @MainActor 34 | final class WindowAccess: ObservableObject { 35 | private weak var window: NSWindow? 36 | 37 | deinit { 38 | // reset changed properties 39 | DispatchQueue.main.async { [weak window] in 40 | window?.level = .normal 41 | } 42 | } 43 | 44 | @Published public var pinned: Bool = false { 45 | didSet { 46 | guard oldValue != pinned else { return } 47 | level = pinned ? .floating : .normal 48 | } 49 | } 50 | 51 | private var level: NSWindow.Level { 52 | get { window?.level ?? .normal } 53 | set { 54 | Task { @MainActor in 55 | window?.level = newValue 56 | objectWillChange.send() 57 | } 58 | } 59 | } 60 | 61 | public func set(window: NSWindow?) { 62 | self.window = window 63 | Task { @MainActor in 64 | objectWillChange.send() 65 | } 66 | } 67 | } 68 | #endif 69 | 70 | struct RoomView: View { 71 | @EnvironmentObject var appCtx: AppContext 72 | @EnvironmentObject var roomCtx: RoomContext 73 | @EnvironmentObject var room: Room 74 | 75 | @State var isCameraPublishingBusy = false 76 | @State var isMicrophonePublishingBusy = false 77 | @State var isScreenSharePublishingBusy = false 78 | @State var isARCameraPublishingBusy = false 79 | 80 | @State private var screenPickerPresented = false 81 | @State private var publishOptionsPickerPresented = false 82 | @State private var audioMixerOptionsPresented = false 83 | 84 | @State private var cameraPublishOptions = VideoPublishOptions() 85 | 86 | #if os(macOS) 87 | @ObservedObject private var windowAccess = WindowAccess() 88 | #endif 89 | 90 | @State private var showConnectionTime = true 91 | @State private var canSwitchCameraPosition = false 92 | 93 | func messageView(_ message: ExampleRoomMessage) -> some View { 94 | let isMe = message.senderSid == room.localParticipant.sid 95 | 96 | return HStack { 97 | if isMe { 98 | Spacer() 99 | } 100 | 101 | // VStack(alignment: isMe ? .trailing : .leading) { 102 | // Text(message.identity) 103 | Text(message.text) 104 | .padding(8) 105 | .background(isMe ? Color.lkRed : Color.lkGray3) 106 | .foregroundColor(Color.white) 107 | .cornerRadius(18) 108 | // } 109 | if !isMe { 110 | Spacer() 111 | } 112 | }.padding(.vertical, 5) 113 | .padding(.horizontal, 10) 114 | } 115 | 116 | func scrollToBottom(_ scrollView: ScrollViewProxy) { 117 | guard let last = roomCtx.messages.last else { return } 118 | withAnimation { 119 | scrollView.scrollTo(last.id) 120 | } 121 | } 122 | 123 | func messagesView(geometry: GeometryProxy) -> some View { 124 | VStack(spacing: 0) { 125 | ScrollViewReader { scrollView in 126 | ScrollView(.vertical, showsIndicators: true) { 127 | LazyVStack(alignment: .center, spacing: 0) { 128 | ForEach(roomCtx.messages) { 129 | messageView($0) 130 | } 131 | } 132 | .padding(.vertical, 12) 133 | .padding(.horizontal, 7) 134 | } 135 | .onAppear(perform: { 136 | // Scroll to bottom when first showing the messages list 137 | scrollToBottom(scrollView) 138 | }) 139 | .onChange(of: roomCtx.messages, perform: { _ in 140 | // Scroll to bottom when there is a new message 141 | scrollToBottom(scrollView) 142 | }) 143 | .frame( 144 | minWidth: 0, 145 | maxWidth: .infinity, 146 | minHeight: 0, 147 | maxHeight: .infinity, 148 | alignment: .topLeading 149 | ) 150 | } 151 | HStack(spacing: 0) { 152 | TextField("Enter message", text: $roomCtx.textFieldString) 153 | .textFieldStyle(PlainTextFieldStyle()) 154 | .disableAutocorrection(true) 155 | // TODO: add iOS unique view modifiers 156 | // #if os(iOS) 157 | // .autocapitalization(.none) 158 | // .keyboardType(type.toiOSType()) 159 | // #endif 160 | 161 | // .overlay(RoundedRectangle(cornerRadius: 10.0) 162 | // .strokeBorder(Color.white.opacity(0.3), 163 | // style: StrokeStyle(lineWidth: 1.0))) 164 | 165 | Button { 166 | roomCtx.sendMessage() 167 | } label: { 168 | Image(systemSymbol: .paperplaneFill) 169 | .foregroundColor(roomCtx.textFieldString.isEmpty ? nil : Color.lkRed) 170 | } 171 | .buttonStyle(.borderless) 172 | } 173 | .padding() 174 | .background(Color.lkGray2) 175 | } 176 | .background(Color.lkGray1) 177 | .cornerRadius(8) 178 | .frame( 179 | minWidth: 0, 180 | maxWidth: geometry.isTall ? .infinity : 320 181 | ) 182 | } 183 | 184 | func sortedParticipants() -> [Participant] { 185 | room.allParticipants.values.sorted { p1, p2 in 186 | if p1 is LocalParticipant { return true } 187 | if p2 is LocalParticipant { return false } 188 | return (p1.joinedAt ?? Date()) < (p2.joinedAt ?? Date()) 189 | } 190 | } 191 | 192 | func content(geometry: GeometryProxy) -> some View { 193 | VStack { 194 | if showConnectionTime { 195 | Text("Connected (\([room.serverRegion, room.serverNodeId, "\(String(describing: room.connectStopwatch.total().rounded(to: 2)))s"].compactMap { $0 }.joined(separator: ", ")))") 196 | .multilineTextAlignment(.center) 197 | .foregroundColor(.white) 198 | .padding() 199 | } 200 | 201 | if case .connecting = room.connectionState { 202 | Text("Re-connecting...") 203 | .multilineTextAlignment(.center) 204 | .foregroundColor(.white) 205 | .padding() 206 | } 207 | 208 | HorVStack(axis: geometry.isTall ? .vertical : .horizontal, spacing: 5) { 209 | Group { 210 | if let focusParticipant = roomCtx.focusParticipant { 211 | ZStack(alignment: .bottomTrailing) { 212 | ParticipantView(participant: focusParticipant, 213 | videoViewMode: appCtx.videoViewMode) 214 | { _ in 215 | roomCtx.focusParticipant = nil 216 | } 217 | .overlay(RoundedRectangle(cornerRadius: 5) 218 | .stroke(Color.lkRed.opacity(0.7), lineWidth: 5.0)) 219 | Text("SELECTED") 220 | .font(.system(size: 10)) 221 | .fontWeight(.bold) 222 | .foregroundColor(Color.white) 223 | .padding(.horizontal, 5) 224 | .padding(.vertical, 2) 225 | .background(Color.lkRed.opacity(0.7)) 226 | .cornerRadius(8) 227 | .padding(.vertical, 35) 228 | .padding(.horizontal, 10) 229 | } 230 | 231 | } else { 232 | // Array([room.allParticipants.values, room.allParticipants.values].joined()) 233 | ParticipantLayout(sortedParticipants(), spacing: 5) { participant in 234 | ParticipantView(participant: participant, 235 | videoViewMode: appCtx.videoViewMode) 236 | { participant in 237 | roomCtx.focusParticipant = participant 238 | } 239 | } 240 | } 241 | } 242 | .frame( 243 | minWidth: 0, 244 | maxWidth: .infinity, 245 | minHeight: 0, 246 | maxHeight: .infinity 247 | ) 248 | // Show messages view if enabled 249 | if roomCtx.showMessagesView { 250 | messagesView(geometry: geometry) 251 | } 252 | } 253 | } 254 | .padding(5) 255 | } 256 | 257 | var body: some View { 258 | GeometryReader { geometry in 259 | content(geometry: geometry) 260 | } 261 | .toolbar { 262 | ToolbarItemGroup(placement: toolbarPlacement) { 263 | // Insufficient space on iOS bar 264 | #if os(macOS) 265 | Group { 266 | if let name = room.name { 267 | Text(name) 268 | .fontWeight(.bold) 269 | } 270 | 271 | if let identity = room.localParticipant.identity { 272 | Text(String(describing: identity)) 273 | } 274 | 275 | Spacer() 276 | 277 | Picker("Mode", selection: $appCtx.videoViewMode) { 278 | Text("Fit").tag(VideoView.LayoutMode.fit) 279 | Text("Fill").tag(VideoView.LayoutMode.fill) 280 | } 281 | .pickerStyle(SegmentedPickerStyle()) 282 | } 283 | #else 284 | Spacer() 285 | #endif 286 | 287 | Group { 288 | let isCameraEnabled = room.localParticipant.isCameraEnabled() 289 | let isMicrophoneEnabled = room.localParticipant.isMicrophoneEnabled() 290 | let isScreenShareEnabled = room.localParticipant.isScreenShareEnabled() 291 | 292 | Group { 293 | #if os(visionOS) && compiler(>=6.0) 294 | // Toggle camera enabled 295 | Button(action: { 296 | Task { 297 | isARCameraPublishingBusy = true 298 | defer { Task { @MainActor in isARCameraPublishingBusy = false } } 299 | try await roomCtx.setARCamera(isEnabled: true) 300 | } 301 | 302 | }, 303 | label: { 304 | Image(systemSymbol: .eyeglasses) 305 | .renderingMode(isCameraEnabled ? .original : .template) 306 | }) 307 | // disable while publishing/un-publishing 308 | .disabled(isARCameraPublishingBusy) 309 | #endif 310 | 311 | if isCameraEnabled, canSwitchCameraPosition { 312 | Menu { 313 | Button("Switch position") { 314 | Task { 315 | isCameraPublishingBusy = true 316 | defer { Task { @MainActor in isCameraPublishingBusy = false } } 317 | if let track = room.localParticipant.firstCameraVideoTrack as? LocalVideoTrack, 318 | let cameraCapturer = track.capturer as? CameraCapturer 319 | { 320 | try await cameraCapturer.switchCameraPosition() 321 | } 322 | } 323 | } 324 | 325 | Button("Disable") { 326 | Task { 327 | isCameraPublishingBusy = true 328 | defer { Task { @MainActor in isCameraPublishingBusy = false } } 329 | try await room.localParticipant.setCamera(enabled: !isCameraEnabled) 330 | } 331 | } 332 | } label: { 333 | Image(systemSymbol: .videoFill) 334 | .renderingMode(.original) 335 | } 336 | // disable while publishing/un-publishing 337 | .disabled(isCameraPublishingBusy) 338 | } else { 339 | // Toggle camera enabled 340 | Button(action: { 341 | if isCameraEnabled { 342 | Task { 343 | isCameraPublishingBusy = true 344 | defer { Task { @MainActor in isCameraPublishingBusy = false } } 345 | try await room.localParticipant.setCamera(enabled: false) 346 | } 347 | } else { 348 | publishOptionsPickerPresented = true 349 | } 350 | }, 351 | label: { 352 | Image(systemSymbol: .videoFill) 353 | .renderingMode(isCameraEnabled ? .original : .template) 354 | }) 355 | // disable while publishing/un-publishing 356 | .disabled(isCameraPublishingBusy) 357 | } 358 | } 359 | #if !os(tvOS) 360 | .popover(isPresented: $publishOptionsPickerPresented) { 361 | PublishOptionsView(publishOptions: cameraPublishOptions) { captureOptions, publishOptions in 362 | publishOptionsPickerPresented = false 363 | isCameraPublishingBusy = true 364 | cameraPublishOptions = publishOptions 365 | Task { 366 | defer { Task { @MainActor in isCameraPublishingBusy = false } } 367 | try await room.localParticipant.setCamera(enabled: true, 368 | captureOptions: captureOptions, 369 | publishOptions: publishOptions) 370 | } 371 | } 372 | .padding() 373 | } 374 | #endif 375 | 376 | // Toggle microphone enabled 377 | Button(action: { 378 | Task { 379 | isMicrophonePublishingBusy = true 380 | defer { Task { @MainActor in isMicrophonePublishingBusy = false } } 381 | let options = AudioCaptureOptions(noiseSuppression: false, highpassFilter: false) 382 | try await room.localParticipant.setMicrophone(enabled: !isMicrophoneEnabled, captureOptions: options) 383 | } 384 | }, 385 | label: { 386 | Image(systemSymbol: .micFill) 387 | .renderingMode(isMicrophoneEnabled ? .original : .template) 388 | }) 389 | // disable while publishing/un-publishing 390 | .disabled(isMicrophonePublishingBusy) 391 | 392 | Button { 393 | audioMixerOptionsPresented = true 394 | } label: { 395 | Image(systemSymbol: .switch2) 396 | } 397 | .disabled(!isMicrophoneEnabled) 398 | #if !os(tvOS) 399 | .popover(isPresented: $audioMixerOptionsPresented) { 400 | AudioMixerView() 401 | .padding() 402 | } 403 | #endif 404 | 405 | #if os(iOS) 406 | Button(action: { 407 | Task { 408 | isScreenSharePublishingBusy = true 409 | defer { Task { @MainActor in isScreenSharePublishingBusy = false } } 410 | try await room.localParticipant.setScreenShare(enabled: !isScreenShareEnabled) 411 | } 412 | }, 413 | label: { 414 | Image(systemSymbol: .rectangleFillOnRectangleFill) 415 | .renderingMode(isScreenShareEnabled ? .original : .template) 416 | }) 417 | // disable while publishing/un-publishing 418 | .disabled(isScreenSharePublishingBusy) 419 | #elseif os(macOS) 420 | Button(action: { 421 | if #available(macOS 12.3, *) { 422 | if isScreenShareEnabled { 423 | // Turn off screen share 424 | Task { 425 | isScreenSharePublishingBusy = true 426 | defer { Task { @MainActor in isScreenSharePublishingBusy = false } } 427 | try await roomCtx.setScreenShareMacOS(isEnabled: false) 428 | } 429 | } else { 430 | screenPickerPresented = true 431 | } 432 | } 433 | }, 434 | label: { 435 | Image(systemSymbol: .rectangleFillOnRectangleFill) 436 | .renderingMode(isScreenShareEnabled ? .original : .template) 437 | .foregroundColor(isScreenShareEnabled ? Color.green : Color.white) 438 | }).popover(isPresented: $screenPickerPresented) { 439 | if #available(macOS 12.3, *) { 440 | ScreenShareSourcePickerView { source in 441 | Task { 442 | isScreenSharePublishingBusy = true 443 | defer { Task { @MainActor in isScreenSharePublishingBusy = false } } 444 | try await roomCtx.setScreenShareMacOS(isEnabled: true, screenShareSource: source) 445 | } 446 | screenPickerPresented = false 447 | }.padding() 448 | } 449 | } 450 | .disabled(isScreenSharePublishingBusy) 451 | #endif 452 | 453 | // Toggle messages view (chat example) 454 | Button(action: { 455 | withAnimation { 456 | roomCtx.showMessagesView.toggle() 457 | } 458 | }, 459 | label: { 460 | Image(systemSymbol: .messageFill) 461 | .renderingMode(roomCtx.showMessagesView ? .original : .template) 462 | }) 463 | } 464 | 465 | // Spacer() 466 | 467 | #if os(iOS) 468 | SwiftUIAudioRoutePickerButton() 469 | #endif 470 | 471 | Menu { 472 | #if os(macOS) 473 | Button("New window") { 474 | if let url = URL(string: "livekit://") { 475 | NSWorkspace.shared.open(url) 476 | } 477 | } 478 | 479 | Divider() 480 | 481 | #endif 482 | 483 | Toggle("Show info overlay", isOn: $appCtx.showInformationOverlay) 484 | 485 | Group { 486 | Toggle("VideoView visible", isOn: $appCtx.videoViewVisible) 487 | Toggle("VideoView flip", isOn: $appCtx.videoViewMirrored) 488 | Toggle("VideoView renderMode: .sampleBuffer", isOn: $appCtx.preferSampleBufferRendering) 489 | #if os(iOS) 490 | Menu("Pinch to zoom") { 491 | Toggle("Zoom In", isOn: $appCtx.videoViewPinchToZoomOptions.bind(.zoomIn)) 492 | Toggle("Zoom Out", isOn: $appCtx.videoViewPinchToZoomOptions.bind(.zoomOut)) 493 | Toggle("Auto Reset", isOn: $appCtx.videoViewPinchToZoomOptions.bind(.resetOnRelease)) 494 | } 495 | #endif 496 | Divider() 497 | } 498 | 499 | Group { 500 | Toggle("Video processing", isOn: $roomCtx.isVideoProcessingEnabled) 501 | } 502 | 503 | #if os(macOS) 504 | Group { 505 | Picker("Output device", selection: $appCtx.outputDevice) { 506 | ForEach($appCtx.outputDevices) { device in 507 | Text(device.wrappedValue.isDefault ? "Default (\(device.wrappedValue.name))" : "\(device.wrappedValue.name)").tag(device.wrappedValue) 508 | } 509 | } 510 | Picker("Input device", selection: $appCtx.inputDevice) { 511 | ForEach($appCtx.inputDevices) { device in 512 | Text(device.wrappedValue.isDefault ? "Default (\(device.wrappedValue.name))" : "\(device.wrappedValue.name)").tag(device.wrappedValue) 513 | } 514 | } 515 | } 516 | #endif 517 | 518 | Group { 519 | Divider() 520 | 521 | Button("Unpublish all") { 522 | Task { await room.localParticipant.unpublishAll() } 523 | } 524 | 525 | Divider() 526 | 527 | Menu("Simulate scenario") { 528 | Button("Quick reconnect") { 529 | Task { try await room.debug_simulate(scenario: .quickReconnect) } 530 | } 531 | Button("Full reconnect") { 532 | Task { try await room.debug_simulate(scenario: .fullReconnect) } 533 | } 534 | Button("Node failure") { 535 | Task { try await room.debug_simulate(scenario: .nodeFailure) } 536 | } 537 | Button("Server leave") { 538 | Task { try await room.debug_simulate(scenario: .serverLeave) } 539 | } 540 | Button("Migration") { 541 | Task { try await room.debug_simulate(scenario: .migration) } 542 | } 543 | Button("Speaker update") { 544 | Task { try await room.debug_simulate(scenario: .speakerUpdate(seconds: 3)) } 545 | } 546 | Button("Force TCP") { 547 | Task { try await room.debug_simulate(scenario: .forceTCP) } 548 | } 549 | Button("Force TLS") { 550 | Task { try await room.debug_simulate(scenario: .forceTLS) } 551 | } 552 | } 553 | } 554 | 555 | Group { 556 | Menu("Track permissions") { 557 | Button("Allow all") { 558 | Task { 559 | try await room.localParticipant 560 | .setTrackSubscriptionPermissions(allParticipantsAllowed: true) 561 | } 562 | } 563 | Button("Disallow all") { 564 | Task { 565 | try await room.localParticipant 566 | .setTrackSubscriptionPermissions(allParticipantsAllowed: false) 567 | } 568 | } 569 | } 570 | 571 | #if os(iOS) || os(visionOS) || os(tvOS) 572 | Toggle("Prefer speaker output", isOn: $appCtx.preferSpeakerOutput) 573 | #endif 574 | 575 | Toggle("Bypass voice processing", isOn: $appCtx.isVoiceProcessingBypassed) 576 | 577 | Picker("Mic mute mode", selection: $appCtx.micMuteMode) { 578 | ForEach([MicrophoneMuteMode.voiceProcessing, 579 | MicrophoneMuteMode.restart, 580 | MicrophoneMuteMode.inputMixer], id: \.self) 581 | { mode in 582 | Text("Mute: \(mode)").tag(mode) 583 | } 584 | } 585 | 586 | Toggle("E2EE enabled", isOn: $roomCtx.isE2eeEnabled) 587 | } 588 | 589 | } label: { 590 | Image(systemSymbol: .gear) 591 | .renderingMode(.original) 592 | } 593 | 594 | // Disconnect 595 | Button(action: { 596 | Task { 597 | await roomCtx.disconnect() 598 | } 599 | }, 600 | label: { 601 | Image(systemSymbol: .xmarkCircleFill) 602 | .renderingMode(.original) 603 | }) 604 | } 605 | } 606 | // #if os(macOS) 607 | // .withHostingWindow { self.windowAccess.set(window: $0) } 608 | // #endif 609 | .onAppear { 610 | Task { @MainActor in 611 | canSwitchCameraPosition = try await CameraCapturer.canSwitchPosition() 612 | } 613 | Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { _ in 614 | Task { @MainActor in 615 | withAnimation { 616 | showConnectionTime = false 617 | } 618 | } 619 | } 620 | } 621 | } 622 | } 623 | 624 | struct ParticipantLayout: View { 625 | let views: [AnyView] 626 | let spacing: CGFloat 627 | 628 | init( 629 | _ data: Data, 630 | id: KeyPath = \.self, 631 | spacing: CGFloat, 632 | @ViewBuilder content: @escaping (Data.Element) -> Content 633 | ) { 634 | self.spacing = spacing 635 | views = data.map { AnyView(content($0[keyPath: id])) } 636 | } 637 | 638 | func computeColumn(with geometry: GeometryProxy) -> (x: Int, y: Int) { 639 | let sqr = Double(views.count).squareRoot() 640 | let r: [Int] = [Int(sqr.rounded()), Int(sqr.rounded(.up))] 641 | let c = geometry.isTall ? r : r.reversed() 642 | return (x: c[0], y: c[1]) 643 | } 644 | 645 | func grid(axis: Axis, geometry: GeometryProxy) -> some View { 646 | ScrollView([axis == .vertical ? .vertical : .horizontal]) { 647 | HorVGrid(axis: axis, columns: [GridItem(.flexible())], spacing: spacing) { 648 | ForEach(0 ..< views.count, id: \.self) { i in 649 | views[i] 650 | .aspectRatio(1, contentMode: .fill) 651 | } 652 | } 653 | .padding(axis == .horizontal ? [.leading, .trailing] : [.top, .bottom], 654 | max(0, ((axis == .horizontal ? geometry.size.width : geometry.size.height) 655 | - ((axis == .horizontal ? geometry.size.height : geometry.size.width) * CGFloat(views.count)) - (spacing * CGFloat(views.count - 1))) / 2)) 656 | } 657 | } 658 | 659 | var body: some View { 660 | GeometryReader { geometry in 661 | if views.isEmpty { 662 | EmptyView() 663 | } else if geometry.size.width <= 300 { 664 | grid(axis: .vertical, geometry: geometry) 665 | } else if geometry.size.height <= 300 { 666 | grid(axis: .horizontal, geometry: geometry) 667 | } else { 668 | let verticalWhenTall: Axis = geometry.isTall ? .vertical : .horizontal 669 | let horizontalWhenTall: Axis = geometry.isTall ? .horizontal : .vertical 670 | 671 | switch views.count { 672 | // simply return first view 673 | case 1: views[0] 674 | case 3: HorVStack(axis: verticalWhenTall, spacing: spacing) { 675 | views[0] 676 | HorVStack(axis: horizontalWhenTall, spacing: spacing) { 677 | views[1] 678 | views[2] 679 | } 680 | } 681 | case 5: HorVStack(axis: verticalWhenTall, spacing: spacing) { 682 | views[0] 683 | if geometry.isTall { 684 | HStack(spacing: spacing) { 685 | views[1] 686 | views[2] 687 | } 688 | HStack(spacing: spacing) { 689 | views[3] 690 | views[4] 691 | } 692 | } else { 693 | VStack(spacing: spacing) { 694 | views[1] 695 | views[3] 696 | } 697 | VStack(spacing: spacing) { 698 | views[2] 699 | views[4] 700 | } 701 | } 702 | } 703 | // case 6: 704 | // if geometry.isTall { 705 | // VStack { 706 | // HStack { 707 | // views[0] 708 | // views[1] 709 | // } 710 | // HStack { 711 | // views[2] 712 | // views[3] 713 | // } 714 | // HStack { 715 | // views[4] 716 | // views[5] 717 | // } 718 | // } 719 | // } else { 720 | // VStack { 721 | // HStack { 722 | // views[0] 723 | // views[1] 724 | // views[2] 725 | // } 726 | // HStack { 727 | // views[3] 728 | // views[4] 729 | // views[5] 730 | // } 731 | // } 732 | // } 733 | default: 734 | let c = computeColumn(with: geometry) 735 | VStack(spacing: spacing) { 736 | ForEach(0 ... (c.y - 1), id: \.self) { y in 737 | HStack(spacing: spacing) { 738 | ForEach(0 ... (c.x - 1), id: \.self) { x in 739 | let index = (y * c.x) + x 740 | if index < views.count { 741 | views[index] 742 | } 743 | } 744 | } 745 | } 746 | } 747 | } 748 | } 749 | } 750 | } 751 | } 752 | 753 | struct HorVStack: View { 754 | let axis: Axis 755 | let horizontalAlignment: HorizontalAlignment 756 | let verticalAlignment: VerticalAlignment 757 | let spacing: CGFloat? 758 | let content: () -> Content 759 | 760 | init(axis: Axis = .horizontal, 761 | horizontalAlignment: HorizontalAlignment = .center, 762 | verticalAlignment: VerticalAlignment = .center, 763 | spacing: CGFloat? = nil, 764 | @ViewBuilder content: @escaping () -> Content) 765 | { 766 | self.axis = axis 767 | self.horizontalAlignment = horizontalAlignment 768 | self.verticalAlignment = verticalAlignment 769 | self.spacing = spacing 770 | self.content = content 771 | } 772 | 773 | var body: some View { 774 | Group { 775 | if axis == .vertical { 776 | VStack(alignment: horizontalAlignment, spacing: spacing, content: content) 777 | } else { 778 | HStack(alignment: verticalAlignment, spacing: spacing, content: content) 779 | } 780 | } 781 | } 782 | } 783 | 784 | struct HorVGrid: View { 785 | let axis: Axis 786 | let spacing: CGFloat? 787 | let content: () -> Content 788 | let columns: [GridItem] 789 | 790 | init(axis: Axis = .horizontal, 791 | columns: [GridItem], 792 | spacing: CGFloat? = nil, 793 | @ViewBuilder content: @escaping () -> Content) 794 | { 795 | self.axis = axis 796 | self.spacing = spacing 797 | self.columns = columns 798 | self.content = content 799 | } 800 | 801 | var body: some View { 802 | Group { 803 | if axis == .vertical { 804 | LazyVGrid(columns: columns, spacing: spacing, content: content) 805 | } else { 806 | LazyHGrid(rows: columns, spacing: spacing, content: content) 807 | } 808 | } 809 | } 810 | } 811 | 812 | extension GeometryProxy { 813 | public var isTall: Bool { 814 | size.height > size.width 815 | } 816 | 817 | var isWide: Bool { 818 | size.width > size.height 819 | } 820 | } 821 | -------------------------------------------------------------------------------- /Multiplatform/Views/ScreenShareSourcePickerView.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 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 | @MainActor 25 | @available(macOS 12.3, *) 26 | final class ScreenShareSourcePickerCtrl: ObservableObject { 27 | @Published var tracks = [LocalVideoTrack]() 28 | @Published var mode: ScreenShareSourcePickerView.Mode = .display { 29 | didSet { 30 | guard oldValue != mode else { return } 31 | Task { [weak self] in 32 | guard let self else { return } 33 | try await self.restartTracks() 34 | } 35 | } 36 | } 37 | 38 | init() { 39 | Task { 40 | try await restartTracks() 41 | } 42 | } 43 | 44 | nonisolated func stopTracks() async throws { 45 | // stop in parallel 46 | await withThrowingTaskGroup(of: Void.self) { group in 47 | for track in await tracks { 48 | group.addTask { 49 | try await track.stop() 50 | } 51 | } 52 | } 53 | } 54 | 55 | private nonisolated func restartTracks() async throws { 56 | try await stopTracks() 57 | 58 | let sources = try await MacOSScreenCapturer.sources(for: mode == .display ? .display : .window) 59 | let options = ScreenShareCaptureOptions(dimensions: .h360_43, fps: 5) 60 | let _newTracks = sources.map { LocalVideoTrack.createMacOSScreenShareTrack(source: $0, options: options) } 61 | 62 | Task { @MainActor in 63 | self.tracks = _newTracks 64 | } 65 | 66 | // start in parallel 67 | await withThrowingTaskGroup(of: Void.self) { group in 68 | for track in _newTracks { 69 | group.addTask { 70 | try await track.start() 71 | } 72 | } 73 | } 74 | } 75 | } 76 | 77 | typealias OnPickScreenShareSource = (MacOSScreenCaptureSource) -> Void 78 | 79 | @available(macOS 12.3, *) 80 | struct ScreenShareSourcePickerView: View { 81 | public enum Mode: Sendable { 82 | case display 83 | case window 84 | } 85 | 86 | @ObservedObject var ctrl = ScreenShareSourcePickerCtrl() 87 | 88 | let onPickScreenShareSource: OnPickScreenShareSource? 89 | 90 | private var columns = [ 91 | GridItem(.fixed(250)), 92 | GridItem(.fixed(250)), 93 | ] 94 | 95 | init(onPickScreenShareSource: OnPickScreenShareSource? = nil) { 96 | self.onPickScreenShareSource = onPickScreenShareSource 97 | } 98 | 99 | var body: some View { 100 | VStack { 101 | Picker("", selection: $ctrl.mode) { 102 | Text("Entire Screen").tag(ScreenShareSourcePickerView.Mode.display) 103 | Text("Application Window").tag(ScreenShareSourcePickerView.Mode.window) 104 | } 105 | .pickerStyle(SegmentedPickerStyle()) 106 | 107 | ScrollView(.vertical, showsIndicators: true) { 108 | LazyVGrid(columns: columns, 109 | alignment: .center, 110 | spacing: 10) 111 | { 112 | ForEach(ctrl.tracks) { track in 113 | ZStack { 114 | SwiftUIVideoView(track, layoutMode: .fit) 115 | .aspectRatio(1, contentMode: .fit) 116 | .onTapGesture { 117 | guard let capturer = track.capturer as? MacOSScreenCapturer, 118 | let source = capturer.captureSource else { return } 119 | onPickScreenShareSource?(source) 120 | } 121 | 122 | if let capturer = track.capturer as? MacOSScreenCapturer, 123 | let source = capturer.captureSource as? MacOSWindow, 124 | let appName = source.owningApplication?.applicationName 125 | { 126 | Text(appName) 127 | .shadow(color: .black, radius: 1) 128 | } 129 | } 130 | } 131 | } 132 | } 133 | .frame(minHeight: 350) 134 | } 135 | .onDisappear { 136 | Task { 137 | try? await ctrl.stopTracks() 138 | } 139 | } 140 | } 141 | } 142 | 143 | #endif 144 | -------------------------------------------------------------------------------- /Multiplatform/Views/Shared/LKButton.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 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 | // Default button style for this example 20 | struct LKButton: View { 21 | let title: String 22 | let action: () -> Void 23 | 24 | var body: some View { 25 | Button(action: action, 26 | label: { 27 | Text(title.uppercased()) 28 | .fontWeight(.bold) 29 | .padding(.horizontal, 12) 30 | .padding(.vertical, 10) 31 | }) 32 | .background(Color.lkRed) 33 | .cornerRadius(8) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Multiplatform/Views/Shared/LKTextField.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 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 | struct LKTextField: View { 20 | enum `Type` { 21 | case `default` 22 | case URL 23 | case ascii 24 | case secret 25 | } 26 | 27 | let title: String 28 | @Binding var text: String 29 | var type: Type = .default 30 | 31 | var body: some View { 32 | VStack(alignment: .leading, spacing: 10.0) { 33 | Text(title) 34 | .fontWeight(.bold) 35 | 36 | Group { 37 | if type == .secret { 38 | SecureField("", text: $text) 39 | } else { 40 | TextField("", text: $text) 41 | } 42 | } 43 | .textFieldStyle(.plain) 44 | .disableAutocorrection(true) 45 | .padding() 46 | .overlay(RoundedRectangle(cornerRadius: 10.0) 47 | .strokeBorder(Color.white.opacity(0.3), 48 | style: StrokeStyle(lineWidth: 1.0))) 49 | #if os(iOS) 50 | .autocapitalization(.none) 51 | .keyboardType(type.toiOSType()) 52 | #endif 53 | 54 | }.frame(maxWidth: .infinity) 55 | } 56 | } 57 | 58 | #if os(iOS) 59 | extension LKTextField.`Type` { 60 | func toiOSType() -> UIKeyboardType { 61 | switch self { 62 | case .URL: return .URL 63 | case .ascii: return .asciiCapable 64 | default: return .default 65 | } 66 | } 67 | } 68 | #endif 69 | 70 | #if os(macOS) 71 | // Avoid showing focus border around textfield for macOS 72 | extension NSTextField { 73 | override open var focusRingType: NSFocusRingType { 74 | get { .none } 75 | set {} 76 | } 77 | } 78 | #endif 79 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | 33 | 34 | -------------------------------------------------------------------------------- /iOS/BroadcastExt/SampleHandler.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 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 | #if os(iOS) 18 | import LiveKit 19 | 20 | @available(macCatalyst 13.1, *) 21 | class SampleHandler: LKSampleHandler { 22 | override var enableLogging: Bool { true } 23 | } 24 | #endif 25 | -------------------------------------------------------------------------------- /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 | 53 | UIApplicationSceneManifest 54 | 55 | UIApplicationSupportsMultipleScenes 56 | 57 | 58 | UIApplicationSupportsIndirectInputEvents 59 | 60 | UIBackgroundModes 61 | 62 | audio 63 | voip 64 | 65 | UILaunchScreen 66 | 67 | UIRequiredDeviceCapabilities 68 | 69 | armv7 70 | 71 | UISupportedInterfaceOrientations 72 | 73 | UIInterfaceOrientationLandscapeLeft 74 | UIInterfaceOrientationLandscapeRight 75 | UIInterfaceOrientationPortrait 76 | UIInterfaceOrientationPortraitUpsideDown 77 | 78 | UIUserInterfaceStyle 79 | Dark 80 | 81 | 82 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------