├── .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 | 
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 |
--------------------------------------------------------------------------------